Compare commits
39 Commits
key-user-e
...
audit-log-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08a3157f1d | ||
|
|
c9ecef4e21 | ||
|
|
46f1be4395 | ||
|
|
b932637400 | ||
|
|
3c6d49b88e | ||
|
|
f4f02f6fe5 | ||
|
|
68c2504fc4 | ||
|
|
51a8762738 | ||
|
|
ff533754b4 | ||
|
|
f6b4d534fc | ||
|
|
3c5107ba42 | ||
|
|
dbc561d2e3 | ||
|
|
f21488fd71 | ||
|
|
0bce8d9ce0 | ||
|
|
0f03ffd3eb | ||
|
|
bc03cae550 | ||
|
|
8197559065 | ||
|
|
6928ae907a | ||
|
|
7accf1ae23 | ||
|
|
2ffa1721b3 | ||
|
|
37811381e7 | ||
|
|
74f9d5cc1e | ||
|
|
7c0769e059 | ||
|
|
10dcd6aadd | ||
|
|
f8c23f4c54 | ||
|
|
ae6be30aa0 | ||
|
|
e6baa95f74 | ||
|
|
aa28804a51 | ||
|
|
ea0773071e | ||
|
|
1a71a6d28a | ||
|
|
50791bed80 | ||
|
|
d58a71af7f | ||
|
|
c7c979236e | ||
|
|
0675793c87 | ||
|
|
91bca922c4 | ||
|
|
30403d239a | ||
|
|
589e282288 | ||
|
|
a8345122ea | ||
|
|
788a101f41 |
@@ -8,7 +8,7 @@
|
|||||||
"mocha": true
|
"mocha": true
|
||||||
},
|
},
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2018
|
"ecmaVersion": 2020
|
||||||
},
|
},
|
||||||
"globals": {
|
"globals": {
|
||||||
"SELECT": true,
|
"SELECT": true,
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
"require-atomic-updates": "off",
|
"require-atomic-updates": "off",
|
||||||
"require-await":"warn"
|
"require-await":"warn",
|
||||||
|
"no-unused-vars": ["warn", { "argsIgnorePattern": "_" }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
.github/workflows/node.js.yml
vendored
2
.github/workflows/node.js.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [12.x, 14.x]
|
node-version: [16.x, 14.x, 12.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -13,5 +13,6 @@
|
|||||||
"**/cds/lib/req/cls.js",
|
"**/cds/lib/req/cls.js",
|
||||||
"**/odata-v4/okra/**"
|
"**/odata-v4/okra/**"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"mochaExplorer.parallel": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,4 +83,4 @@ In case you've a question, find a bug, or otherwise need support, use our [commu
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (c) 2021 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](LICENSE.txt) file.
|
Copyright (c) 2022 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](LICENSE) file.
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
// Incorporate pre-build extensions from...
|
|
||||||
using from '@capire/common';
|
|
||||||
24
bookshop/db/init.js
Normal file
24
bookshop/db/init.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* In order to keep basic bookshop sample as simple as possible, we don't add
|
||||||
|
* reuse dependencies. This db/init.js ensures we still have a minimum set of
|
||||||
|
* currencies, if not obtained through @capire/common.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = async (db)=>{
|
||||||
|
|
||||||
|
const has_common = db.model.definitions['sap.common.Currencies'].elements.numcode
|
||||||
|
if (has_common) return
|
||||||
|
|
||||||
|
const already_filled = await db.exists('sap.common.Currencies',{code:'EUR'})
|
||||||
|
if (already_filled) return
|
||||||
|
|
||||||
|
await INSERT.into ('sap.common.Currencies') .columns (
|
||||||
|
'code','symbol','name'
|
||||||
|
) .rows (
|
||||||
|
[ 'EUR','€','Euro' ],
|
||||||
|
[ 'USD','$','US Dollar' ],
|
||||||
|
[ 'GBP','£','British Pound' ],
|
||||||
|
[ 'ILS','₪','Shekel' ],
|
||||||
|
[ 'JPY','¥','Yen' ],
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using { Currency, managed, sap, extensible } from '@sap/cds/common';
|
using { Currency, managed, sap } from '@sap/cds/common';
|
||||||
namespace sap.capire.bookshop;
|
namespace sap.capire.bookshop;
|
||||||
|
|
||||||
entity Books : managed, extensible {
|
entity Books : managed {
|
||||||
key ID : Integer;
|
key ID : Integer;
|
||||||
title : localized String(111);
|
title : localized String(111);
|
||||||
descr : localized String(1111);
|
descr : localized String(1111);
|
||||||
@@ -13,7 +13,7 @@ entity Books : managed, extensible {
|
|||||||
image : LargeBinary @Core.MediaType : 'image/png';
|
image : LargeBinary @Core.MediaType : 'image/png';
|
||||||
}
|
}
|
||||||
|
|
||||||
entity Authors : managed, extensible {
|
entity Authors : managed {
|
||||||
key ID : Integer;
|
key ID : Integer;
|
||||||
name : String(111);
|
name : String(111);
|
||||||
dateOfBirth : Date;
|
dateOfBirth : Date;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "A simple self-contained bookshop service.",
|
"description": "A simple self-contained bookshop service.",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capire/common": "*",
|
|
||||||
"@sap/cds": "^5.0.4",
|
"@sap/cds": "^5.0.4",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"passport": "0.4.1"
|
"passport": "0.4.1"
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ class CatalogService extends cds.ApplicationService { init(){
|
|||||||
this.on ('submitOrder', async req => {
|
this.on ('submitOrder', async req => {
|
||||||
const {book,quantity} = req.data
|
const {book,quantity} = req.data
|
||||||
if (quantity < 1) return req.reject (400,`quantity has to be 1 or more`)
|
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 (!b) return req.error (404,`Book #${book} doesn't exist`)
|
||||||
|
let {stock} = b
|
||||||
if (quantity > stock) return req.reject (409,`${quantity} exceeds stock for book #${book}`)
|
if (quantity > stock) return req.reject (409,`${quantity} exceeds stock for book #${book}`)
|
||||||
await UPDATE (Books,book) .with ({ stock: stock -= quantity })
|
await UPDATE (Books,book) .with ({ stock: stock -= quantity })
|
||||||
await this.emit ('OrderedBook', { book, quantity, buyer:req.user.id })
|
await this.emit ('OrderedBook', { book, quantity, buyer:req.user.id })
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"@capire/reviews": "*",
|
"@capire/reviews": "*",
|
||||||
"@capire/orders": "*",
|
"@capire/orders": "*",
|
||||||
"@capire/common": "*",
|
"@capire/common": "*",
|
||||||
|
"@capire/data-viewer": "*",
|
||||||
"@sap/cds": "^5",
|
"@sap/cds": "^5",
|
||||||
"express": "^4.17.1"
|
"express": "^4.17.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ cds.once('bootstrap',(app)=>{
|
|||||||
app.serve ('/bookshop') .from ('@capire/bookshop','app/vue')
|
app.serve ('/bookshop') .from ('@capire/bookshop','app/vue')
|
||||||
app.serve ('/reviews') .from ('@capire/reviews','app/vue')
|
app.serve ('/reviews') .from ('@capire/reviews','app/vue')
|
||||||
app.serve ('/orders') .from('@capire/orders','app/orders')
|
app.serve ('/orders') .from('@capire/orders','app/orders')
|
||||||
|
app.serve ('/data') .from('@capire/data-viewer','app/viewer')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add Swagger UI
|
// Add Swagger UI
|
||||||
|
|||||||
@@ -30,3 +30,9 @@ extend Orders with {
|
|||||||
|
|
||||||
// Add orders fiori app (in case of embedded orders service)
|
// Add orders fiori app (in case of embedded orders service)
|
||||||
using from '@capire/orders/app/fiori';
|
using from '@capire/orders/app/fiori';
|
||||||
|
|
||||||
|
// Add data browser
|
||||||
|
using from '@capire/data-viewer';
|
||||||
|
|
||||||
|
// Incorporate pre-build extensions from...
|
||||||
|
using from '@capire/common';
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ extend sap.common.Currencies with {
|
|||||||
* annotate sap.common.Countries with @cds.persistence.skip:false;
|
* annotate sap.common.Countries with @cds.persistence.skip:false;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
context sap.common_countries {
|
context sap.common.countries {
|
||||||
|
|
||||||
extend sap.common.Countries {
|
extend sap.common.Countries {
|
||||||
regions : Composition of many Regions on regions._parent = $self.code;
|
regions : Composition of many Regions on regions._parent = $self.code;
|
||||||
|
|||||||
119
data-viewer/app/viewer/app.js
Normal file
119
data-viewer/app/viewer/app.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
/* global Vue axios */ //> from vue.html
|
||||||
|
const GET = (url) => axios.get('/-data'+url)
|
||||||
|
const storageGet = (key, def) => localStorage.getItem('data-viewer:'+key) || def
|
||||||
|
const storageSet = (key, val) => localStorage.setItem('data-viewer:'+key, val)
|
||||||
|
const columnKeysFirst = (c1, c2) => {
|
||||||
|
if (c1.isKey && !c2.isKey) return -1
|
||||||
|
if (!c1.isKey && c2.isKey) return 1
|
||||||
|
if (c1.isKey && c2.isKey) return c1.name.localeCompare(c2.name)
|
||||||
|
return 0 // retain natural order of normal columns
|
||||||
|
}
|
||||||
|
|
||||||
|
const vue = Vue.createApp ({
|
||||||
|
|
||||||
|
data() { return {
|
||||||
|
error: undefined,
|
||||||
|
dataSource: storageGet('data-source', 'db'),
|
||||||
|
skip: storageGet('skip', 0),
|
||||||
|
top: storageGet('top', 20),
|
||||||
|
entity: storageGet('entity') ? JSON.parse(storageGet('entity')) : undefined,
|
||||||
|
entities: [],
|
||||||
|
columns: [],
|
||||||
|
data: [],
|
||||||
|
rowDetails: {},
|
||||||
|
rowKey: storageGet('rowKey')
|
||||||
|
}},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
dataSource: (v) => { storageSet('data-source', v); vue.fetchEntities() },
|
||||||
|
skip: (v) => { storageSet('skip', v); if (vue.entity) vue.fetchData() },
|
||||||
|
top: (v) => { storageSet('top', v); if (vue.entity) vue.fetchData() },
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
async fetchEntities () {
|
||||||
|
let url = `/Entities`
|
||||||
|
if (vue.dataSource === 'db') url += `?dataSource=db`
|
||||||
|
const {data} = await GET(url)
|
||||||
|
vue.entities = data.value
|
||||||
|
vue.entities.forEach(entity => entity.columns.sort(columnKeysFirst))
|
||||||
|
const entity = vue.entity && vue.entities.find(e => e.name === vue.entity.name)
|
||||||
|
if (entity) { // restore selection from previous fetch
|
||||||
|
vue.columns = entity.columns
|
||||||
|
await vue.fetchData(entity)
|
||||||
|
} else {
|
||||||
|
vue.entity = undefined
|
||||||
|
vue.columns = []
|
||||||
|
vue.data = []
|
||||||
|
vue.rowDetails = {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async inspectEntity (eve) {
|
||||||
|
const entity = vue.entity = vue.entities [eve.currentTarget.rowIndex-1]
|
||||||
|
storageSet('entity', JSON.stringify(entity))
|
||||||
|
vue.columns = vue.entities.find(e => e.name === entity.name).columns
|
||||||
|
return await this.fetchData()
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchData () {
|
||||||
|
let url = `/Data?entity=${vue.entity.name}&$skip=${vue.skip}&$top=${vue.top}`
|
||||||
|
if (vue.dataSource === 'db') url += `&dataSource=db`
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {data} = await GET(url)
|
||||||
|
// sort data along column order
|
||||||
|
const columnIndexes = {}
|
||||||
|
vue.columns.forEach((col, i) => columnIndexes[col.name] = i)
|
||||||
|
vue.data = data.value.map(d => d.record
|
||||||
|
.sort((r1, r2) => columnIndexes[r1.column] - columnIndexes[r2.column])
|
||||||
|
.map(r => r.data)
|
||||||
|
)
|
||||||
|
const row = vue.data.find(data => vue._makeRowKey(data) === vue.rowKey)
|
||||||
|
if (row) vue._setRowDetails(row)
|
||||||
|
else vue.rowDetails = {}
|
||||||
|
vue.error = undefined
|
||||||
|
} catch (err) {
|
||||||
|
vue.data = []
|
||||||
|
vue.rowDetails = {}
|
||||||
|
if (err.response?.data?.error) {
|
||||||
|
vue.error = err.response.data.error
|
||||||
|
} else {
|
||||||
|
vue.error = { code:err.code, message:err.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
inspectRow (eve) {
|
||||||
|
vue.rowDetails = {}
|
||||||
|
const selectedRow = eve.currentTarget.rowIndex-1
|
||||||
|
vue.rowKey = vue._makeRowKey(vue.data[selectedRow])
|
||||||
|
storageSet('rowKey', vue.rowKey)
|
||||||
|
vue._setRowDetails(vue.data[selectedRow])
|
||||||
|
},
|
||||||
|
|
||||||
|
_setRowDetails(row) {
|
||||||
|
vue.rowDetails = {}
|
||||||
|
row.forEach((line, colIndex) => {
|
||||||
|
vue.rowDetails[vue.columns[colIndex].name] = line
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
_makeRowKey(row) {
|
||||||
|
// to identify a row, build a key string out of all key columns' values
|
||||||
|
return row
|
||||||
|
.filter((_, colIndex) => vue.columns[colIndex] && vue.columns[colIndex].isKey)
|
||||||
|
.reduce(((prev, next) => prev += next), '')
|
||||||
|
},
|
||||||
|
|
||||||
|
isActiveRow(row) {
|
||||||
|
return vue._makeRowKey(row) === vue.rowKey
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.mount('#app')
|
||||||
|
|
||||||
|
vue.fetchEntities()
|
||||||
95
data-viewer/app/viewer/index.html
Normal file
95
data-viewer/app/viewer/index.html
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Data Browser</title>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/primitive-ui/dist/css/main.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
|
||||||
|
<script src="app.js" defer></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
th { position: sticky; top:0; z-index: 2; background-color: white; }
|
||||||
|
.noscroll { overflow: hidden; }
|
||||||
|
.hovering tr:hover td { background: #ebeefc; cursor: pointer; }
|
||||||
|
.highlight { background: #ebeefc !important; }
|
||||||
|
.rating-stars { color:teal }
|
||||||
|
.succeeded { color:teal }
|
||||||
|
.failed { color:red }
|
||||||
|
.condensed { max-width: 100px; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.key { font-weight: bold }
|
||||||
|
.not-key { font-weight: lighter;}
|
||||||
|
.with-sidebar { display: flex; flex-wrap: wrap; gap: 1rem; }
|
||||||
|
.sidebar { flex-basis: 20rem; flex-grow: 1; }
|
||||||
|
.sidebar-main { height: 100vh; overflow-y: scroll; }
|
||||||
|
.not-sidebar { flex-basis: 0; flex-grow: 999; min-inline-size: 50%; align-items: stretch;}
|
||||||
|
.not-sidebar-main { max-height: 40vh; overflow-y: scroll; }
|
||||||
|
.not-sidebar-sub { max-height: 40vh; overflow-y: scroll; }
|
||||||
|
.horizontal label { display: inline; }
|
||||||
|
.horizontal input { width: initial; display: inline; }
|
||||||
|
.error { color: red; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="noscroll">
|
||||||
|
<div id='app' class="full-container">
|
||||||
|
|
||||||
|
<h1>Data Browser – {{ entity ? entity.name : '' }}</h1>
|
||||||
|
|
||||||
|
<div class="with-sidebar">
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="horizontal" style="padding: 0.75rem 0;">
|
||||||
|
<label>Datasource:</label>
|
||||||
|
<input type="radio" id="dataSource-db" value="db" v-model="dataSource">
|
||||||
|
<label for="dataSource-db">Database</label>
|
||||||
|
<input type="radio" id="dataSource-srv" value="service" v-model="dataSource">
|
||||||
|
<label for="dataSource-srv">Service</label>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-main">
|
||||||
|
<table id='entities' class="hovering">
|
||||||
|
<thead>
|
||||||
|
<th>Entity Name</th>
|
||||||
|
</thead>
|
||||||
|
<tr v-for="e in entities" :key="e.name" @click="inspectEntity" :class="{'highlight': (entity && e.name === entity.name)}">
|
||||||
|
<td>{{ e.name }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="not-sidebar">
|
||||||
|
<div class="horizontal">
|
||||||
|
<label for="skip">Skip:</label>
|
||||||
|
<input id="skip" v-model.lazy="skip" title="No. of entries to skip" type="number" min="0">
|
||||||
|
<label for="top">Top:</label>
|
||||||
|
<input id="top" v-model.lazy="top" title="No. of entries to read" type="number" min="0">
|
||||||
|
</div>
|
||||||
|
<div v-if="data" class="not-sidebar-main">
|
||||||
|
<table id='data' class="hovering striped-table condensed">
|
||||||
|
<thead>
|
||||||
|
<th v-for="col in columns" :title="col.type" :class="[col.isKey ? 'key' : 'not-key']">{{ col.name }} </th>
|
||||||
|
</thead>
|
||||||
|
<tr v-for="row in data" @click="inspectRow" :class="{'highlight': isActiveRow(row)}">
|
||||||
|
<td v-for="d in row" :title="d">{{ d }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div v-if="error" class="not-sidebar-main error">
|
||||||
|
Error: {{ error.code ? error.code + ' – ' + error.message : error.message }}
|
||||||
|
</div>
|
||||||
|
<p></p>
|
||||||
|
<div v-if="rowDetails" class="not-sidebar-sub">
|
||||||
|
<table id='rowDetails'>
|
||||||
|
<tr v-for="(key, value) in rowDetails" >
|
||||||
|
<td class="key">{{ value }}</td>
|
||||||
|
<td>{{ key }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
1
data-viewer/index.cds
Normal file
1
data-viewer/index.cds
Normal file
@@ -0,0 +1 @@
|
|||||||
|
using from './srv/data-service';
|
||||||
13
data-viewer/package.json
Normal file
13
data-viewer/package.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "@capire/data-viewer",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "A generic browser for data",
|
||||||
|
"dependencies": {
|
||||||
|
"@sap/cds": "^5.0.4"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"app",
|
||||||
|
"srv",
|
||||||
|
"index.cds"
|
||||||
|
]
|
||||||
|
}
|
||||||
29
data-viewer/srv/data-service.cds
Normal file
29
data-viewer/srv/data-service.cds
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Exposes data + entity metadata
|
||||||
|
*/
|
||||||
|
@requires:'authenticated-user'
|
||||||
|
service DataService @( path:'-data' ) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata like name and columns/elements
|
||||||
|
*/
|
||||||
|
entity Entities {
|
||||||
|
key name : String;
|
||||||
|
columns: Composition of many {
|
||||||
|
name : String;
|
||||||
|
type : String;
|
||||||
|
isKey: Boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The actual data, organized by column name
|
||||||
|
*/
|
||||||
|
entity Data {
|
||||||
|
record : array of {
|
||||||
|
column : String;
|
||||||
|
data : String;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
58
data-viewer/srv/data-service.js
Normal file
58
data-viewer/srv/data-service.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
const cds = require('@sap/cds')
|
||||||
|
const log = cds.log('data')
|
||||||
|
|
||||||
|
class DataService extends cds.ApplicationService { init(){
|
||||||
|
|
||||||
|
this.on ('READ', 'Entities', req => {
|
||||||
|
const { dataSource } = req.req.query
|
||||||
|
const srvPrefixes = cds.db.model.all('service').map(srv => srv.name+'.')
|
||||||
|
const dataSourceFilter = dataSource === 'db'
|
||||||
|
? e => e['@cds.persistence.skip'] !== true // for DB, excl. entities w/o persistence
|
||||||
|
: e => !!srvPrefixes.find(srvName => e.name.startsWith(srvName)) // only entities reachable from a service
|
||||||
|
|
||||||
|
return cds.db.model.all('entity')
|
||||||
|
.filter (e => req.data && req.data.name ? e.name === req.data.name : true) // honor name filter from request, if any
|
||||||
|
.filter (e => !e.name.startsWith('DRAFT.')) // exclude synthetic stuff
|
||||||
|
.filter (e => !e.name.startsWith('DataService.')) // exclude this service
|
||||||
|
.filter (dataSourceFilter)
|
||||||
|
.sort((e1, e2) => e1.name.localeCompare(e2.name))
|
||||||
|
.map(e => {
|
||||||
|
const columns = Object.entries(e.elements)
|
||||||
|
.filter(([_, el]) => !(el instanceof cds.Association)) // exclude assocs+compositions
|
||||||
|
.map(([name, el]) => { return { name, type: el.type, isKey:!!el.key }})
|
||||||
|
return { name: e.name, columns }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.on ('READ', 'Data', async req => {
|
||||||
|
const { entity: entityName, dataSource: dataSourceName } = req.req.query
|
||||||
|
if (!entityName) return req.reject(400, `Must provide 'entity' query`)
|
||||||
|
const entity = cds.db.model.definitions[entityName]
|
||||||
|
if (!entity) return req.reject(404, 'No such entity: ' + entityName)
|
||||||
|
|
||||||
|
const query = SELECT.from(entity)
|
||||||
|
query.SELECT.limit = req.query.SELECT.limit // forward $skip / $top
|
||||||
|
|
||||||
|
const dataSource = findDataSource(dataSourceName, entityName)
|
||||||
|
const res = await dataSource.run(query)
|
||||||
|
return res.map((line) => {
|
||||||
|
const record = Object.entries(line).map(([column, data]) => {return {column, data}})
|
||||||
|
return { record }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return super.init()
|
||||||
|
}}
|
||||||
|
|
||||||
|
module.exports = { DataService }
|
||||||
|
|
||||||
|
function findDataSource(dataSourceName, entityName) {
|
||||||
|
for (let srv of Object.values(cds.services)) { // all connected services
|
||||||
|
if (!srv.name) continue // FIXME intermediate/pending in cds.services ?
|
||||||
|
if (dataSourceName === srv.name || entityName.startsWith(srv.name+'.')) {
|
||||||
|
log._debug && log.debug(`using ${srv.name} as data source`)
|
||||||
|
return srv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cds.services.db // fallback
|
||||||
|
}
|
||||||
261
etc/bookshop.drawio.svg
Normal file
261
etc/bookshop.drawio.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 67 KiB |
172
etc/dark.drawio.svg
Normal file
172
etc/dark.drawio.svg
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="347px" height="279px" viewBox="-0.5 -0.5 347 279" content="<mxfile><diagram id="QQJxv4aCTC7ZgE7HHOvM" name="Page-1">7Vlbb+I4FP41vCLfL4/T7szuy0oj9WGfczEkasCRMYXur98TEufeqpoBCtUGCezv+Hq+z/bBWdDHzfFPF5XZ3zY1xYKg9LigfywI0QLBdwW81oDUrAbWLk9rCHXAU/6vqUEc0H2eml2D1ZC3tvB5OQQTu92axA+wyDl7GBZb2SIdAGW0NoNhVMBTEhVmUuyfPPVZjSoiO/wvk6+z0DMWurbEUfK8dna/bfpbEIq/VZ/avIlCW02/uyxK7aEH0e8L+uis9XVqc3w0ReXaodt+vGFtx+3M1n+kAqkrvETFvpn6gqEkKnNnqkpERJtyQR+28a76gXwBzT7EDlJr30cCEFv7vMtsGQzQStwVPk3ZvwY3w+zLKpmZY7S2WyhSGpdvjDeuQ38GCJzzcMhyb57KKKmqHUB7gGV+U0AOQ3KVH02QU5XfPRufVB5HJ2NRPNrCulPndLUyIkmqQt7ZZ9OzpFLHCLWWoADSTuHFOG+Ob7oct0TC+jAWBu9eoUhTgcimSrM2KG9UdeiUpht5ZD2RqQaLGm2v25Y7fiHRUDxPNz033avcuvwOuFaJmec6Vpzxq3FNrsg1OzfXzrzkBnbWm2c75UalbI5tRWIqxKXYppwPVza6Htv83GwndrMB4m6e7BWvPhVut76H18+cCMTpuZQIsFIDEbArikCcWwTWpcbdw4qPjFrN7u8iUSZeXY5s+WkrXk7JHnNitum3KhqGXFJEu12eDF08cOnUQVC98b882SPnB/mRq3+cntaxJg0x9ltuhbHavUvMYAeDXtbG96LTqfN7zuUzzg2YM0Xk85fhIOY83vTw0+YwvI5bCOuH5AayQxv16Jtq/SB70pIYtURHLdWTnrR0kkA78Q+pQn0xVYipKuhnqoJIueSKtk8Y4Gtn7lknRH9UMkTjJcOifSQbdqPEsmcVgl5KT/qL6Yncl54ofZ/oM+mJkvdlez49hRueLyModl+CYtcRFLueoPBUUL8X9O72ZVnk/8e978a9o2MvLOsrxL145s5yTOETNAKCRLfPoMEpN7P7khaSRpf7m4rQkEFBrsfgzDXkXZ8BQZL9QwC/4f+bOAXGYUXYvS8cVoRRXOAUmLnt/HKSEp+pKAxUS6Q4QVhqKtv7h7B/aAZKkIghypVk7FfDCqSWIComtSJIgIrpsBfFQNZEcy0YJpxfLqqYuU+9bz2F16i3EqdiTWELUlWoqDUS46tzSpYQm3KOGYJgQ5NflZOADQhLySVCmrJxJ3oJOyFjimuFKVJnUhNkuze4dfHuLTn9/h8=</diagram></mxfile>" style="background-color: rgb(26, 26, 26);">
|
||||||
|
<defs/>
|
||||||
|
<g>
|
||||||
|
<path d="M 192 148 L 242 148 L 262 188 L 242 228 L 192 228 L 172 188 Z" fill="#ffe6cc" stroke="#d79b00" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 188px; margin-left: 173px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||||
|
@capire/
|
||||||
|
<br/>
|
||||||
|
<b>
|
||||||
|
bookshop
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="217" y="192" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
@capire/...
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<path d="M 192 48 L 242 48 L 262 88 L 242 128 L 192 128 L 172 88 Z" fill="#f8cecc" stroke="#b85450" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 88px; margin-left: 173px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||||
|
@capire/
|
||||||
|
<br/>
|
||||||
|
<b>
|
||||||
|
fiori
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="217" y="92" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
@capire/...
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<path d="M 276 98 L 326 98 L 346 138 L 326 178 L 276 178 L 256 138 Z" fill="#d5e8d4" stroke="#82b366" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 138px; margin-left: 257px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||||
|
@capire/
|
||||||
|
<br/>
|
||||||
|
<b>
|
||||||
|
reviews
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="301" y="142" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
@capire/...
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<path d="M 109 198 L 159 198 L 179 238 L 159 278 L 109 278 L 89 238 Z" fill="#f5f5f5" stroke="#666666" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 238px; margin-left: 90px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #333333; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||||
|
@capire/
|
||||||
|
<br/>
|
||||||
|
<b>
|
||||||
|
common
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="134" y="242" fill="#333333" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
@capire/...
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<path d="M 108 98 L 158 98 L 178 138 L 158 178 L 108 178 L 88 138 Z" fill="#dae8fc" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 138px; margin-left: 89px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||||
|
@capire/
|
||||||
|
<br/>
|
||||||
|
<b>
|
||||||
|
orders
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="133" y="142" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
@capire/...
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<path d="M 168.58 217.17 L 174.72 213.47" fill="none" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||||
|
<path d="M 180.5 209.99 L 175.11 218.49 L 174.72 213.47 L 170.47 210.78 Z" fill="#ffffff" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<path d="M 167.68 117.36 L 174.6 113.24" fill="none" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||||
|
<path d="M 180.4 109.79 L 174.97 118.26 L 174.6 113.24 L 170.36 110.52 Z" fill="#ffffff" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<path d="M 217 148 L 217 136.99" fill="none" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||||
|
<path d="M 217 130.24 L 221.5 139.24 L 217 136.99 L 212.5 139.24 Z" fill="#ffffff" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<path d="M 266.32 117.36 L 259.4 113.24" fill="none" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||||
|
<path d="M 253.6 109.79 L 263.64 110.52 L 259.4 113.24 L 259.03 118.26 Z" fill="#ffffff" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<path d="M 107 1 L 157 1 L 177 41 L 157 81 L 107 81 L 87 41 Z" fill="#dae8fc" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 41px; margin-left: 88px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||||
|
@capire/
|
||||||
|
<br/>
|
||||||
|
<b>
|
||||||
|
suppliers
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="132" y="45" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
@capire/...
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<path d="M 21 53 L 71 53 L 91 93 L 71 133 L 21 133 L 1 93 Z" fill="#e1d5e7" stroke="#9673a6" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 93px; margin-left: 2px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||||
|
<b>
|
||||||
|
S/4
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="46" y="97" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
S/4
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<path d="M 80.55 72.11 L 89.76 66.54" fill="none" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||||
|
<path d="M 95.53 63.05 L 90.16 71.56 L 89.76 66.54 L 85.5 63.86 Z" fill="#ffffff" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<path d="M 81.75 111.49 L 89.27 115.38" fill="none" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||||
|
<path d="M 95.26 118.48 L 85.2 118.34 L 89.27 115.38 L 89.33 110.35 Z" fill="#ffffff" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<path d="M 167.25 60.49 L 173.88 64.16" fill="none" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||||
|
<path d="M 179.79 67.42 L 169.74 67.01 L 173.88 64.16 L 174.09 59.13 Z" fill="#ffffff" stroke="#ffffff" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
</g>
|
||||||
|
<switch>
|
||||||
|
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
|
||||||
|
<a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
|
||||||
|
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
|
||||||
|
Viewer does not support full SVG 1.1
|
||||||
|
</text>
|
||||||
|
</a>
|
||||||
|
</switch>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 14 KiB |
281
etc/incidents.drawio.svg
Normal file
281
etc/incidents.drawio.svg
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="569px" height="428px" viewBox="-0.5 -0.5 569 428" content="<mxfile><diagram id="QQJxv4aCTC7ZgE7HHOvM" name="Page-1">7Vpdd9o4EP01PJZjWf7SY8jHds9pd3tKt30WtjDayBZHFknor18ployNDCXEELbtyUPsQRpLvndmrgZG8Lp4+kPg5eIjzwgb+V72NII3I98HMPLUP21Z15YYBbUhFzQzgzaGKf1OjNHMy1c0I1VnoOScSbrsGlNeliSVHRsWgj92h8056z51iXPzRG9jmKaYEWfYN5rJRW1N/Hhjf09ovrBPBhGqPymwHWwcVwuc8ceWCd6O4LXgXNZXxdM1Yfrl2fdSz7vb8WmzMEFKecgEv57wgNnK7G2qBis0vPdXf12ZNcq13bjgqzIjeq43gpPHBZVkusSp/vRRQa1sC1kwdQfUZcoLmpqh1T2R6cLc/Evz/NljoG7mvJR3uKBMc+E9YQ9E0hTrKVLw++b9qjczwYzmpboW9audPBChx7IrY5dcr6BSC6Jlru7Dxss1Z1w8bwEmvv7TK+WCflcPx8yusZ742QAXm8VNzfaBuW/5Cm70n7Kbt6jWQ552IgEafFVgEF4QKdZqiJnwDtioMEEBLEceNxQDKKxtiza9LIrY0DpvnG+QVxcG/H4iQIcIk1VFS1JVI72OiOkXPhPqKtdXn7CQJRF7CALORJAWHLMkDEKvlzpzylhr5DxJSZoOBRxIxmEXuhC50Fk429CF3uuRCxzkGrgsWn+WqUqYaju+9zEvZAtR5bwZ5aJ8+yRJWVFeqhlXy+WFpgNG5odkA7Q3G5wwsg+IaxjELjlggF7PjtBhhyVDdVnBG6UJmc0PCd4Mk2Q+UPD6YdwFB/lu5IKepDtE5EY9kbsFCSmzKy1Y1F1GccHL7MuCKnZP1Ad3lFkw1J1RSSDqwnRSACqpyoBd3+3nApdrazWL8+y9WR5qYCOZ1Vi7QFMvgq9EakeZl6Oc5cSC0I9tGzsbfIIwLOlD95F94Bl3nzh9zpc2hoMOTQIQ2pRvfdRrNdPaimvLU5Owd7uqt+i4eqZTs82DGBY7DLteVVJ5EBcW/VmMZt5hpXtOoqFKN+pC0Rv8pyrbiQPNdJWmSm/d4VTyvfj8/7V3j9R25Phbam8/ilwmoGjcUwh8bwD1jRwyfOPifs519uuV3yqAtSq7pBC+GPUNPXi+MLYtiRZ0t8WS8TXpR+4LLUi1IET+Bq8fvKQn8k4GHniRApsxnt6rnWdYIWiB2ggxryvE/GOF2NG18KWSCrqKKj6fouoK7xDEx+qpHzgaTk1ZCdqiy0dVsHFOLkxNXcZZCnrADeVTnaWA28DaF8qXephqHZMOO1wdF/nhW0a+H8MuTyA8MvQB2u9owNB3u2xTIh7oDn2m1duldUcv4oh1Xm3mNr9+pfKO3CCvaXyWKAfbwg5GxxZ4tOUqCrdWM2Ccv6wnV/KS7CPJxdQPviTlYNXDJsO3acWByB97CYp0Uw6EHoJ+hxx+hI7jGYjgOAAejGMYJgHyoqTrF5yuuLhtOltAemrL1QOmDM8oo3L9u8K4FeasB0i3i/crVRjb/OiUmPMJSeDUmODIGgO8YL+jAYPd7fVNcHqvcFdGoynd46Rtt37AM8I+8YpK/a2sYhSXkhcaavtDEq/NL6/VuU0VkiqldDi1o4vr0EZ5X+qVFE+5/kXPuFA8Xi3HBRb6X7oSbD0Rz2lmT2vplelGcInNrr09bak5o8uvZnsDZJd3YOv7mtDryS8QuPnFHyC/+G538R/db1Cmr5Q8/lQ8abLWaXliM97APEm2G5nn5YnbyGz/duMGS/wzUaVRxyelSqOsB6aKH6A3ZIrbw3SYYaFJ14wqNSngj6XkrNadH2aNQdW0/FmN/r2Syg0x9qoWN/4YQRSiGIIwgJEPwTaAu085h5+UBsAKJF2sAIpdrJALFQhfDJW63fzusxYWm1/Pwtv/AA==</diagram></mxfile>">
|
||||||
|
<defs/>
|
||||||
|
<g>
|
||||||
|
<rect x="1" y="1" width="195" height="122" fill="#ffffff" stroke="#828282" stroke-width="2" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)rotate(-90 11 13)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe flex-end; width: 107px; height: 1px; padding-top: 13px; margin-left: -96px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: right; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #4D4D4D; line-height: 1.2; pointer-events: all; font-weight: bold; white-space: normal; word-wrap: normal; ">
|
||||||
|
S/4 HANA
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="11" y="25" fill="#4D4D4D" font-family="Helvetica" font-size="12px" text-anchor="end" font-weight="bold">
|
||||||
|
S/4 HANA
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<rect x="42.5" y="40" width="120" height="50" rx="7.5" ry="7.5" fill="#f8cecc" stroke="#b85450" stroke-width="2" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 65px; margin-left: 44px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||||
|
Business
|
||||||
|
<br/>
|
||||||
|
Partner
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="103" y="69" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
Business...
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<rect x="221" y="1" width="347" height="349" fill="#ffffff" stroke="#828282" stroke-width="2" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe flex-start; width: 331px; height: 1px; padding-top: 15px; margin-left: 230px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: left; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #4D4D4D; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||||
|
<b>
|
||||||
|
Incident Mgmt
|
||||||
|
</b>
|
||||||
|
<br/>
|
||||||
|
Extension App
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="230" y="27" fill="#4D4D4D" font-family="Helvetica" font-size="12px">
|
||||||
|
Incident Mgmt...
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<rect x="418" y="73" width="115" height="50" rx="7.5" ry="7.5" fill="#dae8fc" stroke="#6c8ebf" stroke-width="2" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 113px; height: 1px; padding-top: 98px; margin-left: 419px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||||
|
Incidents
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="476" y="102" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
Incidents
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<path d="M 475.5 182 L 475.5 142.97" fill="none" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||||
|
<path d="M 481.5 182 L 475.5 170 L 469.5 182" fill="none" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<path d="M 475.5 124.97 L 480.79 133.97 L 475.5 142.97 L 470.21 133.97 Z" fill="#6c8ebf" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<rect x="255" y="73" width="120" height="50" rx="7.5" ry="7.5" fill="#ffe6cc" stroke="#d79b00" stroke-width="2" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 98px; margin-left: 256px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||||
|
Customers
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="315" y="102" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
Customers
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<rect x="1" y="147" width="196.5" height="202" fill="#ffffff" stroke="#828282" stroke-width="2" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)rotate(-90 11 159)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe flex-end; width: 187px; height: 1px; padding-top: 159px; margin-left: -176px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: right; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #4D4D4D; line-height: 1.2; pointer-events: all; font-weight: bold; white-space: normal; word-wrap: normal; ">
|
||||||
|
SuccessFactors
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="11" y="171" fill="#4D4D4D" font-family="Helvetica" font-size="12px" text-anchor="end" font-weight="bold">
|
||||||
|
SuccessFactors
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<rect x="42.5" y="184" width="120" height="50" rx="7.5" ry="7.5" fill="#f8cecc" stroke="#b85450" stroke-width="2" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 209px; margin-left: 44px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||||
|
Workforce
|
||||||
|
<br/>
|
||||||
|
Person
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="103" y="213" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
Workforce...
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<rect x="42.5" y="267" width="120" height="50" rx="7.5" ry="7.5" fill="#f8cecc" stroke="#b85450" stroke-width="2" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 292px; margin-left: 44px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||||
|
Employee
|
||||||
|
<br/>
|
||||||
|
Timesheet
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="103" y="296" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
Employee...
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<path d="M 162.5 74.32 L 238.96 86.19" fill="none" stroke="#d79b00" stroke-width="2" stroke-miterlimit="10" stroke-dasharray="6 6" pointer-events="stroke"/>
|
||||||
|
<path d="M 252.79 88.34 L 237.88 93.11 L 240.03 79.27 Z" fill="none" stroke="#d79b00" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<rect x="418" y="182" width="115" height="50" rx="7.5" ry="7.5" fill="#dae8fc" stroke="#6c8ebf" stroke-width="2" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 113px; height: 1px; padding-top: 207px; margin-left: 419px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||||
|
Messages
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="476" y="211" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
Messages
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<path d="M 418 98 L 394.97 98" fill="none" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||||
|
<path d="M 418 92 L 406 98 L 418 104" fill="none" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<path d="M 376.97 98 L 385.97 92.71 L 394.97 98 L 385.97 103.29 Z" fill="#6c8ebf" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<rect x="255" y="184" width="120" height="50" rx="7.5" ry="7.5" fill="#ffe6cc" stroke="#d79b00" stroke-width="2" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 209px; margin-left: 256px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||||
|
Service
|
||||||
|
<br/>
|
||||||
|
Worker
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="315" y="213" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
Service...
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<path d="M 162.5 209 L 238.76 209" fill="none" stroke="#d79b00" stroke-width="2" stroke-miterlimit="10" stroke-dasharray="6 6" pointer-events="stroke"/>
|
||||||
|
<path d="M 252.76 209 L 238.76 216 L 238.76 202 Z" fill="none" stroke="#d79b00" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<path d="M 354.83 181.46 L 439.35 123" fill="none" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||||
|
<path d="M 358.91 171.95 L 352.99 182.73 L 365.16 180.99" fill="none" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<rect x="255" y="267" width="120" height="50" rx="7.5" ry="7.5" fill="#ffe6cc" stroke="#d79b00" stroke-width="2" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 118px; height: 1px; padding-top: 292px; margin-left: 256px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||||
|
Worker
|
||||||
|
<br/>
|
||||||
|
Availability
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="315" y="296" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
Worker...
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<path d="M 162.5 292 L 238.76 292" fill="none" stroke="#d79b00" stroke-width="2" stroke-miterlimit="10" stroke-dasharray="6 6" pointer-events="stroke"/>
|
||||||
|
<path d="M 252.76 292 L 238.76 299 L 238.76 285 Z" fill="none" stroke="#d79b00" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<path d="M 37 407 C 37 401.48 41.48 397 47 397 L 92.5 397 C 98.02 397 102.5 392.52 102.5 387 C 102.5 392.52 106.98 397 112.5 397 L 158 397 C 163.52 397 168 401.48 168 407" fill="none" stroke="#b85450" stroke-width="2" stroke-miterlimit="10" transform="translate(0,397)scale(1,-1)translate(0,-397)" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 414px; margin-left: 103px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
|
||||||
|
Backend Services
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="103" y="426" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
Backend Services
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<path d="M 249.5 407 C 249.5 401.48 253.98 397 259.5 397 L 305 397 C 310.52 397 315 392.52 315 387 C 315 392.52 319.48 397 325 397 L 370.5 397 C 376.02 397 380.5 401.48 380.5 407" fill="none" stroke="#d79b00" stroke-width="2" stroke-miterlimit="10" transform="translate(0,397)scale(1,-1)translate(0,-397)" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 414px; margin-left: 315px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
|
||||||
|
Usage Views
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="315" y="426" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
Usage Views
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<path d="M 410 407 C 410 401.48 414.48 397 420 397 L 465.5 397 C 471.02 397 475.5 392.52 475.5 387 C 475.5 392.52 479.98 397 485.5 397 L 531 397 C 536.52 397 541 401.48 541 407" fill="none" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" transform="translate(0,397)scale(1,-1)translate(0,-397)" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 414px; margin-left: 476px;">
|
||||||
|
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
|
||||||
|
Extension Data
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="476" y="426" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
Extension Data
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<path d="M 350 80.94 C 350 79.32 354.25 78 359.5 78 C 362.02 78 364.44 78.31 366.22 78.86 C 368 79.41 369 80.16 369 80.94 L 369 90.06 C 369 91.68 364.75 93 359.5 93 C 354.25 93 350 91.68 350 90.06 Z" fill="#dae8fc" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<path d="M 369 80.94 C 369 82.56 364.75 83.88 359.5 83.88 C 354.25 83.88 350 82.56 350 80.94" fill="none" stroke="#6c8ebf" stroke-width="2" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
</g>
|
||||||
|
<switch>
|
||||||
|
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
|
||||||
|
<a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
|
||||||
|
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
|
||||||
|
Viewer does not support full SVG 1.1
|
||||||
|
</text>
|
||||||
|
</a>
|
||||||
|
</switch>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 24 KiB |
4
etc/samples.drawio.svg
Normal file
4
etc/samples.drawio.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 40 KiB |
@@ -50,10 +50,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sap.ui5": {
|
"sap.ui5": {
|
||||||
"flexEnabled": true,
|
|
||||||
"config": {
|
|
||||||
"experimentalCAPScenario": true
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minUI5Version": "1.81.0",
|
"minUI5Version": "1.81.0",
|
||||||
"libs": {
|
"libs": {
|
||||||
|
|||||||
@@ -64,8 +64,9 @@ annotate AdminService.Books.texts with @(
|
|||||||
|
|
||||||
// Add Value Help for Locales
|
// Add Value Help for Locales
|
||||||
annotate AdminService.Books.texts {
|
annotate AdminService.Books.texts {
|
||||||
locale @ValueList:{entity:'Languages'};
|
locale @(
|
||||||
locale @Common.ValueListWithFixedValues:true; //show as drop down, not a dialog
|
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 as a target for ValueList
|
||||||
using { sap } from '@sap/cds/common';
|
using { sap } from '@sap/cds/common';
|
||||||
|
|||||||
@@ -22,10 +22,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sap.ui5": {
|
"sap.ui5": {
|
||||||
"flexEnabled": true,
|
|
||||||
"config": {
|
|
||||||
"experimentalCAPScenario": true
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"libs": {
|
"libs": {
|
||||||
"sap.fe.templates": {}
|
"sap.fe.templates": {}
|
||||||
|
|||||||
@@ -10,21 +10,7 @@
|
|||||||
<script>
|
<script>
|
||||||
window["sap-ushell-config"] = {
|
window["sap-ushell-config"] = {
|
||||||
defaultRenderer: "fiori2",
|
defaultRenderer: "fiori2",
|
||||||
applications: {},
|
applications: {}
|
||||||
bootstrapPlugins: {
|
|
||||||
RuntimeAuthoringPlugin: {
|
|
||||||
component: "sap.ushell.plugins.rta",
|
|
||||||
config: {
|
|
||||||
validateAppVersion: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
PersonalizePlugin: {
|
|
||||||
component: "sap.ushell.plugins.rta-personalize",
|
|
||||||
config: {
|
|
||||||
validateAppVersion: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -36,13 +22,8 @@
|
|||||||
data-sap-ui-frameOptions="allow"
|
data-sap-ui-frameOptions="allow"
|
||||||
></script>
|
></script>
|
||||||
<script>
|
<script>
|
||||||
sap.ui.getCore().attachInit(()=> sap.ushell.Container.createRenderer().placeAt("content"));
|
sap.ui.getCore().attachInit(()=> sap.ushell.Container.createRenderer().placeAt("content"))
|
||||||
sap.ui
|
</script>
|
||||||
.getCore()
|
|
||||||
.getConfiguration()
|
|
||||||
.setFlexibilityServices([{ connector: "SessionStorageConnector" }]);
|
|
||||||
sap.ui.getCore().getConfiguration().setLanguage("en");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body class="sapUiBody" id="content"></body>
|
<body class="sapUiBody" id="content"></body>
|
||||||
|
|||||||
@@ -13,9 +13,6 @@
|
|||||||
},
|
},
|
||||||
"cds": {
|
"cds": {
|
||||||
"requires": {
|
"requires": {
|
||||||
"extensibility": {
|
|
||||||
"kind": "uiflex"
|
|
||||||
},
|
|
||||||
"auth": {
|
"auth": {
|
||||||
"strategy": "dummy"
|
"strategy": "dummy"
|
||||||
},
|
},
|
||||||
|
|||||||
28
gdpr/.cdsrc.json
Normal file
28
gdpr/.cdsrc.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"target": "gen",
|
||||||
|
"tasks": [{
|
||||||
|
"for": "hana",
|
||||||
|
"src": "db",
|
||||||
|
"options": {
|
||||||
|
"model": [
|
||||||
|
"db",
|
||||||
|
"srv",
|
||||||
|
"app"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"for": "node-cf",
|
||||||
|
"src": "srv",
|
||||||
|
"options": {
|
||||||
|
"model": [
|
||||||
|
"db",
|
||||||
|
"srv",
|
||||||
|
"app"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
4
gdpr/.etc/deploy.sh
Normal file
4
gdpr/.etc/deploy.sh
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
npm run build
|
||||||
|
cf create-service-push
|
||||||
|
cf bind-service gdpr-srv gdpr-pdm -c .pdm/pdm-binding-config.json
|
||||||
|
cf restage gdpr-srv
|
||||||
7
gdpr/.etc/undeploy.sh
Normal file
7
gdpr/.etc/undeploy.sh
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
cf delete gdpr-srv -f
|
||||||
|
cf delete gdpr-db-deployer -f
|
||||||
|
cf delete-service gdpr-pdm -f
|
||||||
|
cf delete-service gdpr-auditlog -f
|
||||||
|
cf delete-service gdpr-uaa -f
|
||||||
|
cf delete-service gdpr-hdi -f
|
||||||
|
cf delete-service gdpr-logs -f
|
||||||
16
gdpr/.pdm/pdm-binding-config.json
Normal file
16
gdpr/.pdm/pdm-binding-config.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"fullyQualifiedApplicationName": "capire-gdpr",
|
||||||
|
"fullyQualifiedModuleName": "gdpr-srv",
|
||||||
|
"applicationTitle": "Capire GDPR Sample App",
|
||||||
|
"applicationTitleKey": "Capire GDPR Sample App",
|
||||||
|
"applicationURL": "https://capire-gdpr-srv.cfapps.eu10.hana.ondemand.com",
|
||||||
|
"endPoints": [{
|
||||||
|
"type": "odatav4",
|
||||||
|
"serviceName": "PDMService",
|
||||||
|
"serviceURI": "/pdm",
|
||||||
|
"serviceTitle": "Capire GDPR Sample App PDM Service",
|
||||||
|
"serviceTitleKey": "Capire GDPR Sample App PDM Service",
|
||||||
|
"hasGdprV4Annotations": true,
|
||||||
|
"cacheControl": "no-cache"
|
||||||
|
}]
|
||||||
|
}
|
||||||
8
gdpr/.pdm/pdm-instance-config.json
Normal file
8
gdpr/.pdm/pdm-instance-config.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"xs-security": {
|
||||||
|
"xsappname": "capire-gdpr",
|
||||||
|
"authorities": ["$ACCEPT_GRANTED_AUTHORITIES"]
|
||||||
|
},
|
||||||
|
"fullyQualifiedApplicationName": "capire-gdpr",
|
||||||
|
"appConsentServiceEnabled": true
|
||||||
|
}
|
||||||
317
gdpr/app/fiori.cds
Normal file
317
gdpr/app/fiori.cds
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Note: this is designed for the GDPRService being co-located with
|
||||||
|
// orders. It does not work if GDPRService is run as a separate
|
||||||
|
// process, and is not intended to do so.
|
||||||
|
//
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
using {GDPRService} from '../srv/gdpr-service';
|
||||||
|
|
||||||
|
annotate cds.UUID with @Core.Computed;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Orders
|
||||||
|
*/
|
||||||
|
@odata.draft.enabled
|
||||||
|
annotate GDPRService.Orders with @(UI : {
|
||||||
|
SelectionFields : [
|
||||||
|
createdAt,
|
||||||
|
createdBy
|
||||||
|
],
|
||||||
|
LineItem : [
|
||||||
|
{
|
||||||
|
Value : OrderNo,
|
||||||
|
Label : 'Order number'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : customer.firstName,
|
||||||
|
Label : 'First Name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : customer.lastName,
|
||||||
|
Label : 'Last Name'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
HeaderInfo : {
|
||||||
|
TypeName : 'Order',
|
||||||
|
TypeNamePlural : 'Orders',
|
||||||
|
Title : {
|
||||||
|
Value : OrderNo,
|
||||||
|
Label : 'Order number'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Identification : [
|
||||||
|
{
|
||||||
|
Value : createdBy,
|
||||||
|
Label : 'Created by'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : createdAt,
|
||||||
|
Label : 'Created at'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
HeaderFacets : [
|
||||||
|
{
|
||||||
|
$Type : 'UI.ReferenceFacet',
|
||||||
|
Label : '{i18n>Created}',
|
||||||
|
Target : '@UI.FieldGroup#Created'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$Type : 'UI.ReferenceFacet',
|
||||||
|
Label : '{i18n>Modified}',
|
||||||
|
Target : '@UI.FieldGroup#Modified'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Facets : [
|
||||||
|
{
|
||||||
|
$Type : 'UI.ReferenceFacet',
|
||||||
|
Label : '{i18n>Details}',
|
||||||
|
Target : '@UI.FieldGroup#Details'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$Type : 'UI.ReferenceFacet',
|
||||||
|
Label : '{i18n>OrderItems}',
|
||||||
|
Target : 'Items/@UI.LineItem'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
FieldGroup #Details : {Data : [
|
||||||
|
{
|
||||||
|
Value : customer_ID,
|
||||||
|
Label : 'Customer'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : customer.firstName,
|
||||||
|
Label : 'First Name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : customer.lastName,
|
||||||
|
Label : 'Last Name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : currency_code,
|
||||||
|
Label : 'Currency'
|
||||||
|
}
|
||||||
|
]},
|
||||||
|
FieldGroup #Created : {Data : [
|
||||||
|
{
|
||||||
|
Value : createdBy,
|
||||||
|
Label : 'Created by'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : createdAt,
|
||||||
|
Label : 'Created at'
|
||||||
|
}
|
||||||
|
]},
|
||||||
|
FieldGroup #Modified : {Data : [
|
||||||
|
{
|
||||||
|
Value : modifiedBy,
|
||||||
|
Label : 'Modified by'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : modifiedAt,
|
||||||
|
Label : 'Modified at'
|
||||||
|
}
|
||||||
|
]},
|
||||||
|
}, ) {
|
||||||
|
createdAt @UI.HiddenFilter : false;
|
||||||
|
createdBy @UI.HiddenFilter : false;
|
||||||
|
customer @ValueList.entity : 'Customers';
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: Order Items are not really maintainable in Fiori preview app
|
||||||
|
*/
|
||||||
|
annotate GDPRService.Orders.Items with @(UI : {
|
||||||
|
LineItem : [
|
||||||
|
{
|
||||||
|
Value : product_ID,
|
||||||
|
Label : 'Product ID'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : title,
|
||||||
|
Label : 'Product Name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : price,
|
||||||
|
Label : 'Price'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : quantity,
|
||||||
|
Label : 'Quantity'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Identification : [
|
||||||
|
{
|
||||||
|
Value : product_ID,
|
||||||
|
Label : 'Product ID'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : title,
|
||||||
|
Label : 'Product Name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : quantity,
|
||||||
|
Label : 'Quantity'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : price,
|
||||||
|
Label : 'Price'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Facets : [{
|
||||||
|
$Type : 'UI.ReferenceFacet',
|
||||||
|
Label : 'Order Items',
|
||||||
|
Target : '@UI.Identification'
|
||||||
|
}, ],
|
||||||
|
}, ) {
|
||||||
|
ID @Core.Computed @UI.Hidden : true;
|
||||||
|
title @Core.Computed;
|
||||||
|
price @Core.Computed;
|
||||||
|
quantity @(Common.FieldControl : #Mandatory);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Customers
|
||||||
|
*/
|
||||||
|
@odata.draft.enabled
|
||||||
|
annotate GDPRService.Customers with @(UI : {
|
||||||
|
SelectionFields : [
|
||||||
|
firstName,
|
||||||
|
lastName
|
||||||
|
],
|
||||||
|
LineItem : [
|
||||||
|
{
|
||||||
|
Value : firstName,
|
||||||
|
Label : 'First Name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : lastName,
|
||||||
|
Label : 'Last Name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : dateOfBirth,
|
||||||
|
Label : 'Date of Birth'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
HeaderInfo : {
|
||||||
|
TypeName : 'Customer',
|
||||||
|
TypeNamePlural : 'Customers',
|
||||||
|
Title : {
|
||||||
|
Value : lastName,
|
||||||
|
Label : 'Last Name'
|
||||||
|
},
|
||||||
|
Description : {
|
||||||
|
Value : firstName,
|
||||||
|
Label : 'First Name'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Identification : [
|
||||||
|
{
|
||||||
|
Value : createdBy,
|
||||||
|
Label : 'Created by'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : createdAt,
|
||||||
|
Label : 'Created at'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
HeaderFacets : [
|
||||||
|
{
|
||||||
|
$Type : 'UI.ReferenceFacet',
|
||||||
|
Label : '{i18n>Created}',
|
||||||
|
Target : '@UI.FieldGroup#Created'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$Type : 'UI.ReferenceFacet',
|
||||||
|
Label : '{i18n>Modified}',
|
||||||
|
Target : '@UI.FieldGroup#Modified'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Facets : [
|
||||||
|
{
|
||||||
|
$Type : 'UI.ReferenceFacet',
|
||||||
|
Label : '{i18n>Details}',
|
||||||
|
Target : '@UI.FieldGroup#Details'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$Type : 'UI.ReferenceFacet',
|
||||||
|
Label : '{i18n>Addresses}',
|
||||||
|
Target : 'addresses/@UI.LineItem'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
FieldGroup #Details : {Data : [
|
||||||
|
{
|
||||||
|
Value : dateOfBirth,
|
||||||
|
Label : 'Date of Birth'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : email,
|
||||||
|
Label : 'E-Mail'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : creditCardNo,
|
||||||
|
Label : 'Credit Card Number'
|
||||||
|
}
|
||||||
|
]},
|
||||||
|
FieldGroup #Created : {Data : [
|
||||||
|
{
|
||||||
|
Value : createdBy,
|
||||||
|
Label : 'Created by'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : createdAt,
|
||||||
|
Label : 'Created at'
|
||||||
|
}
|
||||||
|
]},
|
||||||
|
FieldGroup #Modified : {Data : [
|
||||||
|
{
|
||||||
|
Value : modifiedBy,
|
||||||
|
Label : 'Modified by'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : modifiedAt,
|
||||||
|
Label : 'Modified at'
|
||||||
|
}
|
||||||
|
]},
|
||||||
|
}, ) {
|
||||||
|
createdAt @UI.HiddenFilter : false;
|
||||||
|
createdBy @UI.HiddenFilter : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
annotate GDPRService.CustomerPostalAddresses with @(UI : {
|
||||||
|
LineItem : [
|
||||||
|
{
|
||||||
|
Value : town,
|
||||||
|
Label : 'Town'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : street,
|
||||||
|
Label : 'Street'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : country.name,
|
||||||
|
Label : 'Country'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
Identification : [
|
||||||
|
{
|
||||||
|
Value : town,
|
||||||
|
Label : 'Town'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : street,
|
||||||
|
Label : 'Street'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value : country_code,
|
||||||
|
Label : 'Country Code'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
Facets : [{
|
||||||
|
$Type : 'UI.ReferenceFacet',
|
||||||
|
Label : 'Customer Postal Address',
|
||||||
|
Target : '@UI.Identification'
|
||||||
|
}, ],
|
||||||
|
}, );
|
||||||
56
gdpr/db/data-privacy.cds
Normal file
56
gdpr/db/data-privacy.cds
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using {sap.capire.orders} from '@capire/orders';
|
||||||
|
using {sap.capire.gdpr} from './schema';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* annotations for Data Privacy (Personal Data Manager and Audit Logging)
|
||||||
|
*/
|
||||||
|
annotate gdpr.Customers with @PersonalData : {
|
||||||
|
DataSubjectRole : 'Customer',
|
||||||
|
EntitySemantics : 'DataSubject'
|
||||||
|
}{
|
||||||
|
ID @PersonalData.FieldSemantics : 'DataSubjectID';
|
||||||
|
email @PersonalData.IsPotentiallyPersonal;
|
||||||
|
firstName @PersonalData.IsPotentiallyPersonal;
|
||||||
|
lastName @PersonalData.IsPotentiallyPersonal;
|
||||||
|
creditCardNo @PersonalData.IsPotentiallySensitive;
|
||||||
|
dateOfBirth @PersonalData.IsPotentiallyPersonal;
|
||||||
|
}
|
||||||
|
|
||||||
|
annotate gdpr.CustomerPostalAddresses with @PersonalData : {
|
||||||
|
DataSubjectRole : 'Customer',
|
||||||
|
EntitySemantics : 'DataSubjectDetails'
|
||||||
|
}{
|
||||||
|
customer @PersonalData.FieldSemantics : 'DataSubjectID';
|
||||||
|
street @PersonalData.IsPotentiallyPersonal;
|
||||||
|
town @PersonalData.IsPotentiallyPersonal;
|
||||||
|
country @PersonalData.IsPotentiallyPersonal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: Personal Data Manager doesn't know EntitySemantics: 'Other' and FieldSemantics: 'ContractRelatedID'
|
||||||
|
* see: https://help.sap.com/viewer/620a3ea6aaf64610accdd05cca9e3de2/Cloud/en-US/5a55fae1eb7c496c92c56071186d76b3.html
|
||||||
|
*/
|
||||||
|
annotate orders.Orders with @PersonalData : {
|
||||||
|
DataSubjectRole : 'Customer',
|
||||||
|
EntitySemantics : 'LegalGround'
|
||||||
|
}{
|
||||||
|
ID @PersonalData.FieldSemantics : 'LegalGroundID';
|
||||||
|
customer @PersonalData.FieldSemantics : 'DataSubjectID';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* additional annotations for Audit Logging
|
||||||
|
*/
|
||||||
|
annotate gdpr.Customers with @AuditLog.Operation : {
|
||||||
|
Read : true,
|
||||||
|
Insert : true,
|
||||||
|
Update : true,
|
||||||
|
Delete : true
|
||||||
|
};
|
||||||
|
|
||||||
|
annotate gdpr.CustomerPostalAddresses with @AuditLog.Operation : {
|
||||||
|
Read : true,
|
||||||
|
Insert : true,
|
||||||
|
Update : true,
|
||||||
|
Delete : true
|
||||||
|
};
|
||||||
3
gdpr/db/data/sap.capire.gdpr-CustomerPostalAddresses.csv
Normal file
3
gdpr/db/data/sap.capire.gdpr-CustomerPostalAddresses.csv
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ID;modifiedAt;createdAt;createdBy;modifiedBy;customer_ID;street;town;country_code
|
||||||
|
1e2f2640-6866-4dcf-8f4d-3027aa831cad;2019-04-04;2019-01-31;admin@business.com;admin@business.com;8e2f2640-6866-4dcf-8f4d-3027aa831cad;Hauptstrasse 11;Berlin;DE
|
||||||
|
24e718c9-ff99-47f1-8ca3-950c850777d4;2019-04-04;2019-01-30;admin@business.com;admin@business.com;74e718c9-ff99-47f1-8ca3-950c850777d4;Main Street 22;London;GB
|
||||||
|
3
gdpr/db/data/sap.capire.gdpr-Customers.csv
Normal file
3
gdpr/db/data/sap.capire.gdpr-Customers.csv
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ID;modifiedAt;createdAt;createdBy;modifiedBy;email;firstName;lastName;creditCardNo;dateOfBirth
|
||||||
|
8e2f2640-6866-4dcf-8f4d-3027aa831cad;2019-04-04;2019-01-31;admin@business.com;admin@business.com;john.doe@test.com;John;Doe;9977-6655-4433-2211;1970-01-01
|
||||||
|
74e718c9-ff99-47f1-8ca3-950c850777d4;2019-04-04;2019-01-30;admin@business.com;admin@business.com;jane.doe@sap.com;Jane;Doe;2211-3344-5566-7788;1980-11-11
|
||||||
|
4
gdpr/db/data/sap.capire.orders-Orders.Items.csv
Normal file
4
gdpr/db/data/sap.capire.orders-Orders.Items.csv
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
ID;up__ID;quantity;product_ID;title;price
|
||||||
|
4bd2c9df-c19f-47b8-a921-3cde0d863b52;29f15ef6-4a13-47d4-aef4-329a403b49eb;1;201;Wuthering Heights;11.11
|
||||||
|
6c42a40d-5f7c-4c2f-816b-a73c7c28d722;29f15ef6-4a13-47d4-aef4-329a403b49eb;1;271;Catweazle;15
|
||||||
|
748555fc-2cb0-42b5-a361-dd19a50bd682;31c2bd15-5146-4418-b574-866a08911de7;2;252;Eleonora;28
|
||||||
|
3
gdpr/db/data/sap.capire.orders-Orders.csv
Normal file
3
gdpr/db/data/sap.capire.orders-Orders.csv
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ID;createdAt;createdBy;customer_ID;OrderNo;currency_code
|
||||||
|
29f15ef6-4a13-47d4-aef4-329a403b49eb;2019-01-31;john.doe@test.com;8e2f2640-6866-4dcf-8f4d-3027aa831cad;1;EUR
|
||||||
|
31c2bd15-5146-4418-b574-866a08911de7;2019-01-30;jane.doe@test.com;74e718c9-ff99-47f1-8ca3-950c850777d4;2;EUR
|
||||||
|
30
gdpr/db/schema.cds
Normal file
30
gdpr/db/schema.cds
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using {
|
||||||
|
Country,
|
||||||
|
managed,
|
||||||
|
cuid
|
||||||
|
} from '@sap/cds/common';
|
||||||
|
using {sap.capire.orders} from '@capire/orders';
|
||||||
|
|
||||||
|
namespace sap.capire.gdpr;
|
||||||
|
|
||||||
|
extend orders.Orders with {
|
||||||
|
customer : Association to Customers;
|
||||||
|
}
|
||||||
|
|
||||||
|
entity Customers : cuid, managed {
|
||||||
|
email : String;
|
||||||
|
firstName : String;
|
||||||
|
lastName : String;
|
||||||
|
creditCardNo : String;
|
||||||
|
dateOfBirth : Date;
|
||||||
|
addresses : Composition of many CustomerPostalAddresses
|
||||||
|
on addresses.customer = $self;
|
||||||
|
}
|
||||||
|
|
||||||
|
entity CustomerPostalAddresses : cuid, managed {
|
||||||
|
customer : Association to Customers;
|
||||||
|
street : String(128);
|
||||||
|
town : String(128);
|
||||||
|
@assert.integrity : false
|
||||||
|
country : Country;
|
||||||
|
};
|
||||||
136
gdpr/db/src/.hdiconfig
Normal file
136
gdpr/db/src/.hdiconfig
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
{
|
||||||
|
"file_suffixes": {
|
||||||
|
"csv": {
|
||||||
|
"plugin_name": "com.sap.hana.di.tabledata.source"
|
||||||
|
},
|
||||||
|
"hdbafllangprocedure": {
|
||||||
|
"plugin_name": "com.sap.hana.di.afllangprocedure"
|
||||||
|
},
|
||||||
|
"hdbanalyticprivilege": {
|
||||||
|
"plugin_name": "com.sap.hana.di.analyticprivilege"
|
||||||
|
},
|
||||||
|
"hdbcalculationview": {
|
||||||
|
"plugin_name": "com.sap.hana.di.calculationview"
|
||||||
|
},
|
||||||
|
"hdbcollection": {
|
||||||
|
"plugin_name": "com.sap.hana.di.collection"
|
||||||
|
},
|
||||||
|
"hdbconstraint": {
|
||||||
|
"plugin_name": "com.sap.hana.di.constraint"
|
||||||
|
},
|
||||||
|
"hdbdropcreatetable": {
|
||||||
|
"plugin_name": "com.sap.hana.di.dropcreatetable"
|
||||||
|
},
|
||||||
|
"hdbflowgraph": {
|
||||||
|
"plugin_name": "com.sap.hana.di.flowgraph"
|
||||||
|
},
|
||||||
|
"hdbfunction": {
|
||||||
|
"plugin_name": "com.sap.hana.di.function"
|
||||||
|
},
|
||||||
|
"hdbgraphworkspace": {
|
||||||
|
"plugin_name": "com.sap.hana.di.graphworkspace"
|
||||||
|
},
|
||||||
|
"hdbhadoopmrjob": {
|
||||||
|
"plugin_name": "com.sap.hana.di.virtualfunctionpackage.hadoop"
|
||||||
|
},
|
||||||
|
"hdbindex": {
|
||||||
|
"plugin_name": "com.sap.hana.di.index"
|
||||||
|
},
|
||||||
|
"hdblibrary": {
|
||||||
|
"plugin_name": "com.sap.hana.di.library"
|
||||||
|
},
|
||||||
|
"hdbmigrationtable": {
|
||||||
|
"plugin_name": "com.sap.hana.di.table.migration"
|
||||||
|
},
|
||||||
|
"hdbprocedure": {
|
||||||
|
"plugin_name": "com.sap.hana.di.procedure"
|
||||||
|
},
|
||||||
|
"hdbprojectionview": {
|
||||||
|
"plugin_name": "com.sap.hana.di.projectionview"
|
||||||
|
},
|
||||||
|
"hdbprojectionviewconfig": {
|
||||||
|
"plugin_name": "com.sap.hana.di.projectionview.config"
|
||||||
|
},
|
||||||
|
"hdbreptask": {
|
||||||
|
"plugin_name": "com.sap.hana.di.reptask"
|
||||||
|
},
|
||||||
|
"hdbresultcache": {
|
||||||
|
"plugin_name": "com.sap.hana.di.resultcache"
|
||||||
|
},
|
||||||
|
"hdbrole": {
|
||||||
|
"plugin_name": "com.sap.hana.di.role"
|
||||||
|
},
|
||||||
|
"hdbroleconfig": {
|
||||||
|
"plugin_name": "com.sap.hana.di.role.config"
|
||||||
|
},
|
||||||
|
"hdbsearchruleset": {
|
||||||
|
"plugin_name": "com.sap.hana.di.searchruleset"
|
||||||
|
},
|
||||||
|
"hdbsequence": {
|
||||||
|
"plugin_name": "com.sap.hana.di.sequence"
|
||||||
|
},
|
||||||
|
"hdbstatistics": {
|
||||||
|
"plugin_name": "com.sap.hana.di.statistics"
|
||||||
|
},
|
||||||
|
"hdbstructuredprivilege": {
|
||||||
|
"plugin_name": "com.sap.hana.di.structuredprivilege"
|
||||||
|
},
|
||||||
|
"hdbsynonym": {
|
||||||
|
"plugin_name": "com.sap.hana.di.synonym"
|
||||||
|
},
|
||||||
|
"hdbsynonymconfig": {
|
||||||
|
"plugin_name": "com.sap.hana.di.synonym.config"
|
||||||
|
},
|
||||||
|
"hdbsystemversioning": {
|
||||||
|
"plugin_name": "com.sap.hana.di.systemversioning"
|
||||||
|
},
|
||||||
|
"hdbtable": {
|
||||||
|
"plugin_name": "com.sap.hana.di.table"
|
||||||
|
},
|
||||||
|
"hdbtabledata": {
|
||||||
|
"plugin_name": "com.sap.hana.di.tabledata"
|
||||||
|
},
|
||||||
|
"hdbtabletype": {
|
||||||
|
"plugin_name": "com.sap.hana.di.tabletype"
|
||||||
|
},
|
||||||
|
"hdbtrigger": {
|
||||||
|
"plugin_name": "com.sap.hana.di.trigger"
|
||||||
|
},
|
||||||
|
"hdbview": {
|
||||||
|
"plugin_name": "com.sap.hana.di.view"
|
||||||
|
},
|
||||||
|
"hdbvirtualfunction": {
|
||||||
|
"plugin_name": "com.sap.hana.di.virtualfunction"
|
||||||
|
},
|
||||||
|
"hdbvirtualfunctionconfig": {
|
||||||
|
"plugin_name": "com.sap.hana.di.virtualfunction.config"
|
||||||
|
},
|
||||||
|
"hdbvirtualpackagehadoop": {
|
||||||
|
"plugin_name": "com.sap.hana.di.virtualpackage.hadoop"
|
||||||
|
},
|
||||||
|
"hdbvirtualpackagesparksql": {
|
||||||
|
"plugin_name": "com.sap.hana.di.virtualpackage.sparksql"
|
||||||
|
},
|
||||||
|
"hdbvirtualprocedure": {
|
||||||
|
"plugin_name": "com.sap.hana.di.virtualprocedure"
|
||||||
|
},
|
||||||
|
"hdbvirtualprocedureconfig": {
|
||||||
|
"plugin_name": "com.sap.hana.di.virtualprocedure.config"
|
||||||
|
},
|
||||||
|
"hdbvirtualtable": {
|
||||||
|
"plugin_name": "com.sap.hana.di.virtualtable"
|
||||||
|
},
|
||||||
|
"hdbvirtualtableconfig": {
|
||||||
|
"plugin_name": "com.sap.hana.di.virtualtable.config"
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"plugin_name": "com.sap.hana.di.tabledata.properties"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"plugin_name": "com.sap.hana.di.tabledata.properties"
|
||||||
|
},
|
||||||
|
"txt": {
|
||||||
|
"plugin_name": "com.sap.hana.di.copyonly"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
gdpr/manifest.yml
Normal file
31
gdpr/manifest.yml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
applications:
|
||||||
|
# -----------------------------------------------------------------------------------
|
||||||
|
# HANA Database Content Deployer App
|
||||||
|
# -----------------------------------------------------------------------------------
|
||||||
|
- name: gdpr-db-deployer
|
||||||
|
path: gen/db
|
||||||
|
no-route: true
|
||||||
|
health-check-type: process
|
||||||
|
memory: 256M
|
||||||
|
buildpack: nodejs_buildpack
|
||||||
|
services:
|
||||||
|
- gdpr-logs
|
||||||
|
- gdpr-hdi
|
||||||
|
# -----------------------------------------------------------------------------------
|
||||||
|
# Backend Service
|
||||||
|
# -----------------------------------------------------------------------------------
|
||||||
|
- name: gdpr-srv
|
||||||
|
path: gen/srv
|
||||||
|
memory: 256M
|
||||||
|
buildpack: nodejs_buildpack
|
||||||
|
routes:
|
||||||
|
- route: capire-gdpr-srv.cfapps.eu10.hana.ondemand.com
|
||||||
|
services:
|
||||||
|
- gdpr-logs
|
||||||
|
- gdpr-hdi
|
||||||
|
- gdpr-uaa
|
||||||
|
- gdpr-auditlog
|
||||||
|
# binding with parameters not yet supported -> binding done manually in .etc/deploy.sh
|
||||||
|
#- name: gdpr-pdm
|
||||||
|
# parameters: ./pdm-binding-config.json
|
||||||
49
gdpr/package.json
Normal file
49
gdpr/package.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "@capire/gdpr",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"dependencies": {
|
||||||
|
"@capire/orders": "../orders",
|
||||||
|
"@sap/audit-logging": "^5.1.0",
|
||||||
|
"@sap/cds": "^5.9",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"hdb": "^0.19.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "rm -rf gen && cds build --production",
|
||||||
|
"deploy": "sh .etc/deploy.sh",
|
||||||
|
"undeploy": "sh .etc/undeploy.sh",
|
||||||
|
"start": "cds run"
|
||||||
|
},
|
||||||
|
"cds": {
|
||||||
|
"requires": {
|
||||||
|
"auth": {
|
||||||
|
"__comment__": "workaround to avoid approuter et al. setup",
|
||||||
|
"impl": "srv/auth.js"
|
||||||
|
},
|
||||||
|
"audit-log": {
|
||||||
|
"[development]": {
|
||||||
|
"kind": "audit-log-to-console"
|
||||||
|
},
|
||||||
|
"[production]": {
|
||||||
|
"kind": "audit-log-service"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"db": {
|
||||||
|
"kind": "sql"
|
||||||
|
},
|
||||||
|
"uaa": {
|
||||||
|
"kind": "xsuaa"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"audit_personal_data": true,
|
||||||
|
"fiori_preview": true,
|
||||||
|
"[production]": {
|
||||||
|
"kibana_formatter": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hana": {
|
||||||
|
"deploy-format": "hdbtable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
gdpr/readme.md
Normal file
35
gdpr/readme.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# how-to
|
||||||
|
|
||||||
|
## required services and subscriptions
|
||||||
|
|
||||||
|
services:
|
||||||
|
- Audit Log Service
|
||||||
|
- SAP HANA Cloud
|
||||||
|
- SAP HANA Schemas & HDI Containers
|
||||||
|
- Application Logging Service
|
||||||
|
- Personal Data Manager
|
||||||
|
- Authorization and Trust Management Service
|
||||||
|
|
||||||
|
subscriptions:
|
||||||
|
- Audit Log Viewer Service
|
||||||
|
- Personal Data Manager
|
||||||
|
|
||||||
|
## deploy
|
||||||
|
|
||||||
|
after adding the necessary entitlements, do:
|
||||||
|
- `cf l` to log into the respective account
|
||||||
|
- `cd gdpr` (if still in root of `cloud-cap-samples`)
|
||||||
|
- `npm run deploy`, which executes build and deployment via `.etc/deploy.sh`
|
||||||
|
|
||||||
|
## authorization
|
||||||
|
|
||||||
|
create roles for Audit Log Viewer Service and Personal Data Manager, and assign the roles to the respective users
|
||||||
|
|
||||||
|
# open issues
|
||||||
|
|
||||||
|
- deploy via mta, which can bind with parameters, and get rid of scripts in `.etc`
|
||||||
|
- use approuter to remove hacky custom auth impl (`srv/auth.js`)
|
||||||
|
- clarify annotation `EntitySemantics`, which differs between audit logging (`Other`) and personal data manager (`LegalGround`)
|
||||||
|
- annotations for order items Fiori preview app
|
||||||
|
+ `Products` has `@cds.persistence.skip:'always'`
|
||||||
|
- how to reuse intial data from `common`?
|
||||||
20
gdpr/services-manifest.yml
Normal file
20
gdpr/services-manifest.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
create-services:
|
||||||
|
- name: gdpr-logs # > for kibana
|
||||||
|
broker: application-logs
|
||||||
|
plan: standard
|
||||||
|
- name: gdpr-hdi # > hana
|
||||||
|
broker: hana
|
||||||
|
plan: hdi-shared
|
||||||
|
- name: gdpr-auditlog # > audit log sink
|
||||||
|
broker: auditlog
|
||||||
|
plan: standard
|
||||||
|
# gdpr-pdm needs to exist before creating gdpr-uaa for authorization grant
|
||||||
|
- name: gdpr-pdm # > personal data manager
|
||||||
|
broker: personal-data-manager-service
|
||||||
|
plan: standard
|
||||||
|
parameters: ./.pdm/pdm-instance-config.json
|
||||||
|
- name: gdpr-uaa # > uaa for authentication
|
||||||
|
broker: xsuaa
|
||||||
|
plan: application
|
||||||
|
parameters: xs-security.json
|
||||||
43
gdpr/srv/auth.js
Normal file
43
gdpr/srv/auth.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* workaround to avoid approuter et al. setup
|
||||||
|
*/
|
||||||
|
|
||||||
|
const jwt = require('jsonwebtoken')
|
||||||
|
const tenant = process.env.VCAP_SERVICES
|
||||||
|
? JSON.parse(process.env.VCAP_SERVICES).xsuaa[0].credentials.tenantid
|
||||||
|
: 'anonymous'
|
||||||
|
|
||||||
|
module.exports = (req, res, next) => {
|
||||||
|
/*
|
||||||
|
* decode JWT coming from Personal Data Manager
|
||||||
|
*
|
||||||
|
* DO NOT USE FOR PRODUCTION!
|
||||||
|
* - no token validation
|
||||||
|
* - no xsappname check
|
||||||
|
*/
|
||||||
|
const bearer = req.headers.authorization && req.headers.authorization.split('Bearer ')[1]
|
||||||
|
if (bearer) {
|
||||||
|
const { client_id: id, zid: tenant, scope: roles } = jwt.decode(bearer)
|
||||||
|
req.user = {
|
||||||
|
id,
|
||||||
|
tenant,
|
||||||
|
is: role => roles.some(r => r.endsWith(`.${role}`))
|
||||||
|
}
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// mock user that has every role EXCEPT PersonalDataManagerUser
|
||||||
|
const basic = req.headers.authorization && req.headers.authorization.split('Basic ')[1]
|
||||||
|
if (basic) {
|
||||||
|
const [id] = Buffer.from(basic, 'base64').toString('utf-8').split(':')
|
||||||
|
req.user = {
|
||||||
|
id,
|
||||||
|
tenant,
|
||||||
|
is: role => role !== 'PersonalDataManagerUser'
|
||||||
|
}
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// no bearer & no basic -> 401
|
||||||
|
res.set('WWW-Authenticate', 'Basic realm="Users"').status(401).end()
|
||||||
|
}
|
||||||
10
gdpr/srv/gdpr-service.cds
Normal file
10
gdpr/srv/gdpr-service.cds
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using {
|
||||||
|
sap.capire.orders,
|
||||||
|
sap.capire.gdpr
|
||||||
|
} from '../db/schema';
|
||||||
|
|
||||||
|
@requires : 'admin' // > authorization check
|
||||||
|
service GDPRService {
|
||||||
|
entity Customers as projection on gdpr.Customers;
|
||||||
|
entity Orders as projection on orders.Orders;
|
||||||
|
}
|
||||||
24
gdpr/srv/pdm-service.cds
Normal file
24
gdpr/srv/pdm-service.cds
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using {
|
||||||
|
sap.capire.gdpr as gdpr,
|
||||||
|
sap.capire.orders as orders
|
||||||
|
} from '../db/data-privacy';
|
||||||
|
|
||||||
|
@requires : 'PersonalDataManagerUser' // > authorization check
|
||||||
|
service PDMService {
|
||||||
|
|
||||||
|
entity Customers as projection on gdpr.Customers;
|
||||||
|
entity CustomerPostalAddresses as projection on gdpr.CustomerPostalAddresses;
|
||||||
|
entity Orders as projection on orders.Orders;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* additional annotations for Personal Data Manager's Search Fields
|
||||||
|
*/
|
||||||
|
annotate Customers with @(Communication.Contact : {
|
||||||
|
n : {
|
||||||
|
surname : lastName,
|
||||||
|
given : firstName
|
||||||
|
},
|
||||||
|
bday : dateOfBirth
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
26
gdpr/srv/server.js
Normal file
26
gdpr/srv/server.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const cds = require('@sap/cds')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* in development, write audit logs to custom sink (i.e., to console in this example)
|
||||||
|
*/
|
||||||
|
cds.on('served', async () => {
|
||||||
|
if (process.env.NODE_ENV === 'production') return
|
||||||
|
|
||||||
|
const auditLogService = await cds.connect.to('audit-log')
|
||||||
|
// use prepend to get called before the generic implementation
|
||||||
|
auditLogService.prepend(function() {
|
||||||
|
const LOG = cds.log('my custom audit logging impl')
|
||||||
|
// triggered when reading sensitive personal data
|
||||||
|
this.on('dataAccessLog', function(req) {
|
||||||
|
const { accesses } = req.data
|
||||||
|
for (const access of accesses) LOG.info(access)
|
||||||
|
})
|
||||||
|
// triggered when modifying personal data
|
||||||
|
this.on('dataModificationLog', function(req) {
|
||||||
|
const { modifications } = req.data
|
||||||
|
for (const modification of modifications) LOG.info(modification)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = cds.server
|
||||||
14
gdpr/xs-security.json
Normal file
14
gdpr/xs-security.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"xsappname": "capire-gdpr",
|
||||||
|
"tenant-mode": "shared",
|
||||||
|
"scopes": [{
|
||||||
|
"name": "$XSAPPNAME.PersonalDataManagerUser",
|
||||||
|
"description": "Authority for Personal Data Manager",
|
||||||
|
"grant-as-authority-to-apps": [
|
||||||
|
"$XSSERVICENAME(gdpr-pdm)"
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
"name": "$XSAPPNAME.admin",
|
||||||
|
"description": "Administrator"
|
||||||
|
}]
|
||||||
|
}
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
"start": "cds serve srv/world.cds",
|
"start": "cds serve srv/world.cds",
|
||||||
"start:ts": "cds-ts serve srv/world.cds"
|
"start:ts": "cds-ts serve srv/world.cds"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sap/cds": "^5.0.4"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^27.0.2",
|
"@types/jest": "^27.0.2",
|
||||||
"@types/node": "^16.11.6",
|
"@types/node": "^16.11.6",
|
||||||
|
|||||||
5637
package-lock.json
generated
5637
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,9 @@
|
|||||||
"@capire/bookstore": "./bookstore",
|
"@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",
|
||||||
@@ -19,13 +21,14 @@
|
|||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"chai-subset": "^1.6.0",
|
"chai-subset": "^1.6.0",
|
||||||
"sqlite3": "^5"
|
"sqlite3": "npm:@mendix/sqlite3@^5"
|
||||||
},
|
},
|
||||||
"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",
|
"hello": "cds watch hello",
|
||||||
"media": "cds watch media",
|
"media": "cds watch media",
|
||||||
"mocha": "npx mocha || echo",
|
"mocha": "npx mocha || echo",
|
||||||
@@ -42,6 +45,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"mocha": {
|
"mocha": {
|
||||||
|
"recursive": true,
|
||||||
"parallel": true
|
"parallel": true
|
||||||
},
|
},
|
||||||
"license": "SAP SAMPLE CODE LICENSE",
|
"license": "SAP SAMPLE CODE LICENSE",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
subject;rating;reviewer;title;text
|
subject;rating;reviewer;title;text
|
||||||
201;5;adam;Intriguing;Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
|
201;5;bob;Intriguing;Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
|
||||||
201;4;bob;Fascinating;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Id diam maecenas ultricies mi eget mauris pharetra et. Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque. Pulvinar mattis nunc sed blandit libero. Facilisis magna etiam tempor orci eu. Nec sagittis aliquam malesuada bibendum arcu. Eu consequat ac felis donec. Ultricies tristique nulla aliquet enim tortor at auctor urna nunc. Tortor posuere ac ut consequat semper viverra nam libero. Amet nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Scelerisque purus semper eget duis at tellus. Elementum tempus egestas sed sed risus pretium. Arcu dictum varius duis at. Amet luctus venenatis lectus magna fringilla urna. Eget velit aliquet sagittis id consectetur purus ut faucibus. Vitae auctor eu augue ut lectus. Fermentum iaculis eu non diam phasellus vestibulum.
|
201;4;bob;Fascinating;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Id diam maecenas ultricies mi eget mauris pharetra et. Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque. Pulvinar mattis nunc sed blandit libero. Facilisis magna etiam tempor orci eu. Nec sagittis aliquam malesuada bibendum arcu. Eu consequat ac felis donec. Ultricies tristique nulla aliquet enim tortor at auctor urna nunc. Tortor posuere ac ut consequat semper viverra nam libero. Amet nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Scelerisque purus semper eget duis at tellus. Elementum tempus egestas sed sed risus pretium. Arcu dictum varius duis at. Amet luctus venenatis lectus magna fringilla urna. Eget velit aliquet sagittis id consectetur purus ut faucibus. Vitae auctor eu augue ut lectus. Fermentum iaculis eu non diam phasellus vestibulum.
|
||||||
207;2;bob;What is this?;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero justo laoreet sit amet cursus sit amet dictum. Nunc faucibus a pellentesque sit. Dis parturient montes nascetur ridiculus mus mauris vitae ultricies. Enim nunc faucibus a pellentesque. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien. Cras ornare arcu dui vivamus. Facilisi etiam dignissim diam quis enim lobortis. Et molestie ac feugiat sed. Urna neque viverra justo nec ultrices dui. Ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis. Dignissim sodales ut eu sem. Feugiat in fermentum posuere urna nec. At augue eget arcu dictum varius.
|
207;2;bob;What is this?;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero justo laoreet sit amet cursus sit amet dictum. Nunc faucibus a pellentesque sit. Dis parturient montes nascetur ridiculus mus mauris vitae ultricies. Enim nunc faucibus a pellentesque. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien. Cras ornare arcu dui vivamus. Facilisi etiam dignissim diam quis enim lobortis. Et molestie ac feugiat sed. Urna neque viverra justo nec ultrices dui. Ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis. Dignissim sodales ut eu sem. Feugiat in fermentum posuere urna nec. At augue eget arcu dictum varius.
|
||||||
251;3;bob;It's dark...;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Suscipit tellus mauris a diam. Velit aliquet sagittis id consectetur purus ut. Viverra adipiscing at in tellus integer. Vitae elementum curabitur vitae nunc. Mattis ullamcorper velit sed ullamcorper morbi. Diam quis enim lobortis scelerisque. Auctor neque vitae tempus quam pellentesque nec nam aliquam. Semper auctor neque vitae tempus. Quis eleifend quam adipiscing vitae proin. Neque convallis a cras semper auctor neque vitae. Imperdiet massa tincidunt nunc pulvinar sapien et ligula. Sit amet consectetur adipiscing elit ut aliquam purus. Pretium quam vulputate dignissim suspendisse.
|
251;3;bob;It's dark...;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Suscipit tellus mauris a diam. Velit aliquet sagittis id consectetur purus ut. Viverra adipiscing at in tellus integer. Vitae elementum curabitur vitae nunc. Mattis ullamcorper velit sed ullamcorper morbi. Diam quis enim lobortis scelerisque. Auctor neque vitae tempus quam pellentesque nec nam aliquam. Semper auctor neque vitae tempus. Quis eleifend quam adipiscing vitae proin. Neque convallis a cras semper auctor neque vitae. Imperdiet massa tincidunt nunc pulvinar sapien et ligula. Sit amet consectetur adipiscing elit ut aliquam purus. Pretium quam vulputate dignissim suspendisse.
|
||||||
|
@@ -32,7 +32,6 @@ entity Likes {
|
|||||||
|
|
||||||
// Auto-fill reviewers and review dates
|
// Auto-fill reviewers and review dates
|
||||||
annotate Reviews with {
|
annotate Reviews with {
|
||||||
reviewer @cds.on.insert:$user;
|
reviewer @cds.on:{insert:$user};
|
||||||
date @cds.on.insert:$now;
|
date @cds.on:{insert:$now,update:$now};
|
||||||
date @cds.on.update:$now;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,9 +58,11 @@ Each sub directory essentially is an individual npm package arranged in an [all-
|
|||||||
- [@capire/reviews](reviews)
|
- [@capire/reviews](reviews)
|
||||||
- [@capire/orders](orders)
|
- [@capire/orders](orders)
|
||||||
- [@capire/common](common)
|
- [@capire/common](common)
|
||||||
- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well
|
- [@capire/data-viewer](data-viewer)
|
||||||
- [The Vue.js app](reviews/app/vue) imported from reviews is served as well
|
- [The Vue.js app](bookshop/app/vue) imported from `bookshop` is served as well
|
||||||
- [The Fiori app](orders/app) imported from orders is served as well
|
- [The Vue.js app](reviews/app/vue) imported from `reviews` is served as well
|
||||||
|
- [The Vue.js app](data-viewer/app/data) imported from `data-viewer` is served as well
|
||||||
|
- [The Fiori app](orders/app) imported from `orders` is served as well
|
||||||
- [OpenAPI export + Swagger UI](https://cap.cloud.sap/docs/advanced/openapi)
|
- [OpenAPI export + Swagger UI](https://cap.cloud.sap/docs/advanced/openapi)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ expect.one = (cqn) => !cqn.SELECT.distinct ? expect(cqn) : skip
|
|||||||
describe('cds.ql → cqn', () => {
|
describe('cds.ql → cqn', () => {
|
||||||
//
|
//
|
||||||
|
|
||||||
for (let each of ['SELECT', 'SELECT one', 'SELECT distinct']) {
|
describe.each(['SELECT', 'SELECT one', 'SELECT distinct'])(`%s...`, (each) => {
|
||||||
|
|
||||||
let SELECT; beforeEach(()=> SELECT = (
|
let SELECT; beforeEach(()=> SELECT = (
|
||||||
each === 'SELECT distinct' ? cds.ql.SELECT.distinct :
|
each === 'SELECT distinct' ? cds.ql.SELECT.distinct :
|
||||||
each === 'SELECT one' ? cds.ql.SELECT.one :
|
each === 'SELECT one' ? cds.ql.SELECT.one :
|
||||||
cds.ql.SELECT
|
cds.ql.SELECT
|
||||||
))
|
))
|
||||||
describe(`${each}...`, () => {
|
|
||||||
|
|
||||||
test(`from Foo`, () => {
|
test(`from Foo`, () => {
|
||||||
expect(cqn = SELECT `from Foo`)
|
expect(cqn = SELECT `from Foo`)
|
||||||
@@ -333,7 +333,7 @@ describe('cds.ql → cqn', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
})}
|
})
|
||||||
|
|
||||||
describe ('SELECT where...', ()=>{
|
describe ('SELECT where...', ()=>{
|
||||||
|
|
||||||
@@ -630,6 +630,34 @@ describe('cds.ql → cqn', () => {
|
|||||||
//
|
//
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe(`SELECT for update`, () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
delete cds.env.sql.lock_acquire_timeout
|
||||||
|
})
|
||||||
|
|
||||||
|
it('no wait', () => {
|
||||||
|
const q = SELECT.from('Foo').forUpdate()
|
||||||
|
expect(q.SELECT.forUpdate).eqls({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('specific wait', () => {
|
||||||
|
const q = SELECT.from('Foo').forUpdate({ wait: 1 })
|
||||||
|
expect(q.SELECT.forUpdate).eqls({ wait: 1 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('default wait', () => {
|
||||||
|
cds.env.sql.lock_acquire_timeout = 2
|
||||||
|
const q = SELECT.from('Foo').forUpdate()
|
||||||
|
expect(q.SELECT.forUpdate).eqls({ wait: 2 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('override default', () => {
|
||||||
|
cds.env.sql.lock_acquire_timeout = 1
|
||||||
|
const q = SELECT.from('Foo').forUpdate({ wait:-1 })
|
||||||
|
expect(q.SELECT.forUpdate).eqls({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe(`INSERT...`, () => {
|
describe(`INSERT...`, () => {
|
||||||
test('entries ({a,b}, ...)', () => {
|
test('entries ({a,b}, ...)', () => {
|
||||||
const entries = [{ foo: 1 }, { boo: 2 }]
|
const entries = [{ foo: 1 }, { boo: 2 }]
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
const cds = require('@sap/cds/lib')
|
const cds = require('@sap/cds/lib')
|
||||||
const { expect } = cds.test (
|
const { expect } = cds.test ('@capire/bookshop')
|
||||||
'serve', 'AdminService', '--from', '@capire/bookshop,@capire/common', '--in-memory'
|
|
||||||
)
|
|
||||||
|
|
||||||
describe('Consuming Services locally', () => {
|
describe('Consuming Services locally', () => {
|
||||||
//
|
//
|
||||||
it('bootstrapped the database successfully', ()=>{
|
it('bootstrapped the database successfully', ()=>{
|
||||||
const { AdminService } = cds.services
|
const { AdminService } = cds.services
|
||||||
const { Authors } = AdminService.entities
|
const { Authors } = AdminService.entities
|
||||||
expect(AdminService).not.to.be.undefined
|
expect(AdminService).to.exist
|
||||||
expect(Authors).not.to.be.undefined
|
expect(Authors).to.exist
|
||||||
})
|
})
|
||||||
|
|
||||||
it('supports targets as strings or reflected defs', async () => {
|
it('supports targets as strings or reflected defs', async () => {
|
||||||
@@ -39,6 +37,7 @@ describe('Consuming Services locally', () => {
|
|||||||
name: 'Emily Brontë',
|
name: 'Emily Brontë',
|
||||||
books: [
|
books: [
|
||||||
{
|
{
|
||||||
|
ID: 201,
|
||||||
title: 'Wuthering Heights',
|
title: 'Wuthering Heights',
|
||||||
currency: { name: 'British Pound', symbol: '£' },
|
currency: { name: 'British Pound', symbol: '£' },
|
||||||
},
|
},
|
||||||
@@ -47,8 +46,8 @@ describe('Consuming Services locally', () => {
|
|||||||
{
|
{
|
||||||
name: 'Edgar Allen Poe',
|
name: 'Edgar Allen Poe',
|
||||||
books: [
|
books: [
|
||||||
{ title: 'The Raven', currency: { name: 'US Dollar', symbol: '$' } },
|
{ ID: 251, title: 'The Raven', currency: { name: 'US Dollar', symbol: '$' } },
|
||||||
{ title: 'Eleonora', currency: { name: 'US Dollar', symbol: '$' } },
|
{ ID: 252, title: 'Eleonora', currency: { name: 'US Dollar', symbol: '$' } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ describe('Hello world!', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should say hello with another impl', async () => {
|
it('should say hello with another impl', async () => {
|
||||||
const cds = require ('@sap/cds')
|
await cds.serve('say').from(cds.model)
|
||||||
cds.serve('say').from(cds.model)
|
|
||||||
.at('/say-again').in(cds.app)
|
.at('/say-again').in(cds.app)
|
||||||
.with(srv => {
|
.with(srv => {
|
||||||
srv.on('hello', (req) => `Hello again ${req.data.to}!`)
|
srv.on('hello', (req) => `Hello again ${req.data.to}!`)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
const cds = require('@sap/cds/lib')
|
const { GET, expect, cds } = require('@sap/cds/lib').test (__dirname)
|
||||||
const { GET, expect } = cds.test.run ('serve', __dirname+'/localized-data.cds', '--in-memory')
|
|
||||||
if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch
|
if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch
|
||||||
else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases
|
else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases
|
||||||
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
|
|
||||||
|
const cds = require('@sap/cds/lib')
|
||||||
|
const { expect } = cds.test
|
||||||
const { fork } = require('child_process')
|
const { fork } = require('child_process')
|
||||||
const { resolve } = require('path')
|
const { resolve } = require('path')
|
||||||
const Axios = require('axios')
|
|
||||||
const verbose = process.env.CDS_TEST_VERBOSE
|
const verbose = process.env.CDS_TEST_VERBOSE
|
||||||
// ||true
|
// ||true
|
||||||
|
|
||||||
@@ -10,28 +11,28 @@ describe('Local NPM registry', () => {
|
|||||||
let axios
|
let axios
|
||||||
const cwd = resolve(__dirname, '..')
|
const cwd = resolve(__dirname, '..')
|
||||||
|
|
||||||
beforeAll(async ()=> {
|
before(async ()=> {
|
||||||
const env = Object.assign(process.env, {PORT:'0'})
|
const env = Object.assign(process.env, {PORT:'0'})
|
||||||
const res = await exec (resolve(cwd, '.registry/server.js'), {cwd, stdio: 'pipe', env})
|
const res = await exec (resolve(cwd, '.registry/server.js'), {cwd, stdio: 'pipe', env})
|
||||||
registry = res.cp
|
registry = res.cp
|
||||||
axios = Axios.default.create ({ baseURL: res.url, validateStatus: (status)=>status<500 })
|
axios = require('axios').default.create ({ baseURL: res.url, validateStatus: (status)=>status<500 })
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => { registry.kill() })
|
after(() => { registry.kill() })
|
||||||
|
|
||||||
for (const mod of ['bookshop','fiori','orders','reviews']) {
|
for (const mod of ['bookshop','fiori','orders','reviews']) {
|
||||||
it(`should serve ${mod}`, async () => {
|
it(`should serve ${mod}`, async () => {
|
||||||
const resp = await axios.get(`/@capire/${mod}`)
|
const resp = await axios.get(`/@capire/${mod}`)
|
||||||
expect(resp.data).toMatchObject({name: `@capire/${mod}`, versions:{}})
|
expect(resp.data).to.containSubset({name: `@capire/${mod}`, versions:{}})
|
||||||
const versions = Object.values(resp.data.versions)
|
const versions = Object.values(resp.data.versions)
|
||||||
await axios.get(versions[0].dist.tarball)
|
await axios.get(versions[0].dist.tarball)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
it(`should return 404 for unknown packages`, async () => {
|
it(`should return 404 for unknown packages`, async () => {
|
||||||
let resp = await axios.get(`/@capire/foo`)
|
let resp = await axios.get(`/@capire/foo`)
|
||||||
expect(resp.status).toEqual(404)
|
expect(resp.status).to.equal(404)
|
||||||
resp = await axios.get(`/foo`)
|
resp = await axios.get(`/foo`)
|
||||||
expect(resp.status).toEqual(404)
|
expect(resp.status).to.equal(404)
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user