Compare commits

..

51 Commits

Author SHA1 Message Date
nkaputnik
2f1af693c9 Finalize ToDo's 2022-07-28 15:29:53 +02:00
nkaputnik
d2ab511d6d Package.json 2022-07-28 14:58:32 +02:00
nkaputnik
6c27228d62 Sandbox API with Application Service 2022-07-28 14:55:37 +02:00
nkaputnik
44880c7745 Better Sandbox API 2022-07-28 14:54:08 +02:00
nkaputnik
9c2a7598f2 First push 2022-07-26 09:47:56 +02:00
nkaputnik
9617e576f0 First push 2022-07-26 09:47:30 +02:00
nkaputnik
c3c9dae80d First push 2022-07-26 09:45:34 +02:00
nkaputnik
2b6d4c625e First push 2022-05-06 11:09:04 +02:00
nkaputnik
7d46db42ec First push 2022-05-06 11:05:33 +02:00
dependabot[bot]
df57a9d8d0 Bump sqlite3 from 5.0.5 to 5.0.6
Bumps [sqlite3](https://github.com/TryGhost/node-sqlite3) from 5.0.5 to 5.0.6.
- [Release notes](https://github.com/TryGhost/node-sqlite3/releases)
- [Changelog](https://github.com/TryGhost/node-sqlite3/blob/master/CHANGELOG.md)
- [Commits](https://github.com/TryGhost/node-sqlite3/compare/v5.0.5...v5.0.6)

---
updated-dependencies:
- dependency-name: sqlite3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-28 14:19:35 +02:00
dependabot[bot]
2d92b851f1 Bump @sap/cds from 5.9.2 to 5.9.3
Bumps [@sap/cds](https://cap.cloud.sap/) from 5.9.2 to 5.9.3.

---
updated-dependencies:
- dependency-name: "@sap/cds"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-27 15:42:39 +02:00
dependabot[bot]
e157fc8a92 Bump sqlite3 from 5.0.4 to 5.0.5
Bumps [sqlite3](https://github.com/TryGhost/node-sqlite3) from 5.0.4 to 5.0.5.
- [Release notes](https://github.com/TryGhost/node-sqlite3/releases)
- [Changelog](https://github.com/TryGhost/node-sqlite3/blob/master/CHANGELOG.md)
- [Commits](https://github.com/TryGhost/node-sqlite3/compare/v5.0.4...v5.0.5)

---
updated-dependencies:
- dependency-name: sqlite3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-25 14:37:35 +02:00
Christian Georgi
3b69a80975 Use original sqlite3 again 2022-04-20 09:00:05 +02:00
dependabot[bot]
b80ba7445d Bump semver from 7.3.6 to 7.3.7
Bumps [semver](https://github.com/npm/node-semver) from 7.3.6 to 7.3.7.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.3.6...v7.3.7)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-13 14:24:24 +02:00
Daniel
b733643f2a fixed accidential push 2022-04-09 12:38:24 +02:00
Daniel
bf317bc2c9 fixed 2022-04-09 11:54:12 +02:00
Daniel
bbebff4066 . 2022-04-08 19:52:26 +02:00
dependabot[bot]
eb75394044 Bump @sap/cds from 5.9.1 to 5.9.2
Bumps [@sap/cds](https://cap.cloud.sap/) from 5.9.1 to 5.9.2.

---
updated-dependencies:
- dependency-name: "@sap/cds"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-08 12:35:56 +02:00
dependabot[bot]
b8f65b687f Bump semver from 7.3.5 to 7.3.6
Bumps [semver](https://github.com/npm/node-semver) from 7.3.5 to 7.3.6.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.3.5...v7.3.6)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-08 12:33:12 +02:00
Daniel
c49c9691bd . 2022-04-06 15:29:46 +02:00
Daniel
7e4bc0985e Revert change to npm workspaces
This PARTIALLY reverts commit 912271338d.
2022-04-06 13:17:51 +02:00
Daniel Hutzel
912271338d Prepare for cds6 (#341) 2022-04-05 16:30:04 +02:00
Daniel Hutzel
bbf1194a09 Cosmetics to test outlines (#340) 2022-04-03 12:36:11 +02:00
dependabot[bot]
529c431518 Bump @sap/cds from 5.9.0 to 5.9.1
Bumps [@sap/cds](https://cap.cloud.sap/) from 5.9.0 to 5.9.1.

---
updated-dependencies:
- dependency-name: "@sap/cds"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-01 14:51:22 +02:00
Christian Georgi
3f85676edd Registry to handle packages with dashes
Like `@capire/data-viewer`, resulting in requests like
`GET /capire-data-viewer-0.1.0.tgz`
2022-04-01 11:49:28 +02:00
Daniel
959c07cee3 minor cosmetics for user service 2022-03-31 09:49:47 +02:00
Daniel
8cc09fde38 Fixed SELECT...columns { foo, bar } 2022-03-28 14:44:06 +02:00
Christian Georgi
bb55c432c9 Bookshop showing user info
Show current user name, locale and tenant (if available).
Useful in deployments w/ real auth and tenant contexts.
2022-03-28 14:36:32 +02:00
dependabot[bot]
5fc86d45ad Bump minimist from 1.2.5 to 1.2.6
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-28 14:31:42 +02:00
Steffen Waldmann
e86d0dc3a2 Use cds.requires.auth.kind instead of cds.requires.auth.strategy
As recently discussed, this is the preferred version as opposed to the legacy `cds.requires.auth.strategy`.
2022-03-28 14:29:58 +02:00
dependabot[bot]
a661fb2d45 Bump @sap/cds from 5.8.4 to 5.9.0
Bumps [@sap/cds](https://cap.cloud.sap/) from 5.8.4 to 5.9.0.

---
updated-dependencies:
- dependency-name: "@sap/cds"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-28 14:26:16 +02:00
Christian Georgi
bfe0c831d6 Relative Fiori dataSource URLs
Since this is what 'real' Fiori apps use as well, to allow serving from
FLP.  CDS's fiori helper takes care that they work w/o FLP as well.
2022-03-28 13:31:58 +02:00
dependabot[bot]
cad615a662 Bump @sap/cds from 5.8.3 to 5.8.4
Bumps [@sap/cds](https://cap.cloud.sap/) from 5.8.3 to 5.8.4.

---
updated-dependencies:
- dependency-name: "@sap/cds"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-17 15:07:20 +01:00
Christian Georgi
7101c58920 Use new horizon theme of Fiori 2022-03-02 15:48:58 +01:00
dependabot[bot]
07dc1e88b3 Bump @sap/cds from 5.8.2 to 5.8.3
Bumps [@sap/cds](https://cap.cloud.sap/) from 5.8.2 to 5.8.3.

---
updated-dependencies:
- dependency-name: "@sap/cds"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-02 11:40:49 +01:00
dependabot[bot]
6f8d74dc9a Bump @sap/cds from 5.8.1 to 5.8.2
Bumps [@sap/cds](https://cap.cloud.sap/) from 5.8.1 to 5.8.2.

---
updated-dependencies:
- dependency-name: "@sap/cds"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-01 14:57:33 +01:00
Christian Georgi
ec57c5ea48 Enable dependency updates 2022-03-01 14:53:44 +01:00
Christian Georgi
c040a47279 Merge pull request #326 from MikhailGoncharov/followup-324
Follow-up #324: better version handling
2022-02-21 09:45:44 +01:00
Christian Georgi
30bfd70c49 Update package-lock 2022-02-21 09:42:12 +01:00
Mikhail Goncharov
6fb9581cf1 Update package.json 2022-02-17 10:39:18 +01:00
Mikhail Goncharov
e87d6cdfc5 Update devDependencies 2022-02-17 10:35:35 +01:00
Mikhail Goncharov
29ea2bc2da Better version handling 2022-02-17 10:09:43 +01:00
Heiko Witteborg
12574271ac Merge pull request #324 from MikhailGoncharov/service-api-read-no-keys-if-not-asked
Fixed eagerly read IDs (CAP !== OData)
2022-02-17 08:53:01 +01:00
Mikhail Goncharov
17b5cc1ad2 Merge branch 'main' into service-api-read-no-keys-if-not-asked 2022-02-15 08:56:08 +01:00
Daniel Hutzel
26ca7f54ad Specified files in package.json (#325) 2022-02-15 01:33:02 +01:00
dependabot[bot]
573e78253d Merge pull request #323 from SAP-samples/dependabot/npm_and_yarn/follow-redirects-1.14.8 2022-02-14 12:02:04 +00:00
Mikhail Goncharov
a7511ed677 Sync with cds runtime for next release 2022-02-14 12:06:22 +01:00
dependabot[bot]
f1289b436b Bump follow-redirects from 1.14.7 to 1.14.8
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-14 10:32:09 +00:00
Dr. David A. Kunz
984ea2133b Merge pull request #322 from SAP-samples/fix-change-to-vue3
Changes in Vue 3 and stable version
2022-02-09 10:23:27 +01:00
D065023
179d301acb Use stable URI 2022-02-09 10:12:41 +01:00
D065023
bbf20b1ca3 Change to Vue 3 2022-02-09 10:10:19 +01:00
71 changed files with 1451 additions and 11843 deletions

8
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: npm
directory: /
versioning-strategy: increase-if-necessary
schedule:
interval: daily

View File

@@ -16,7 +16,8 @@ app.use('/-/:tarball', (req,res,next) => {
console.debug ('GET', req.params) console.debug ('GET', req.params)
try { try {
const { tarball } = req.params const { tarball } = req.params
const [, pkg ] = /^\w+-(\w+)/.exec(tarball) const pkgFull = tarball.substring(0, tarball.lastIndexOf('-'))
const [, pkg ] = /^\w+-(.+)/.exec(pkgFull)
fs.lstat(tarball,(err => { fs.lstat(tarball,(err => {
if (err) console.debug (`npm pack ../${pkg}`) if (err) console.debug (`npm pack ../${pkg}`)
if (err) exec(`npm pack ../${pkg}`,{cwd},next) if (err) exec(`npm pack ../${pkg}`,{cwd},next)
@@ -31,7 +32,7 @@ app.use('/-/:tarball', (req,res,next) => {
app.use('/-', express.static(__dirname)) app.use('/-', express.static(__dirname))
app.get('/*', (req,res)=>{ app.get('/*', (req,res)=>{
const urlRegex = /^\/(@\w+)\/(\w+)/ const urlRegex = /^\/(@[\w-]+)\/(.+)/
const url = decodeURIComponent(req.url) const url = decodeURIComponent(req.url)
console.debug ('GET',url) console.debug ('GET',url)
try { try {

View File

@@ -3,14 +3,15 @@ const $ = sel => document.querySelector(sel)
const GET = (url) => axios.get('/browse'+url) const GET = (url) => axios.get('/browse'+url)
const POST = (cmd,data) => axios.post('/browse'+cmd,data) const POST = (cmd,data) => axios.post('/browse'+cmd,data)
const books = new Vue ({ const books = Vue.createApp ({
el:'#app', data() {
return {
data: {
list: [], list: [],
book: undefined, book: undefined,
order: { quantity:1, succeeded:'', failed:'' } order: { quantity:1, succeeded:'', failed:'' },
user: {}
}
}, },
methods: { methods: {
@@ -37,12 +38,24 @@ const books = new Vue ({
book.stock = res.data.stock book.stock = res.data.stock
books.order = { quantity, succeeded: `Successfully ordered ${quantity} item(s).` } books.order = { quantity, succeeded: `Successfully ordered ${quantity} item(s).` }
} catch (e) { } catch (e) {
books.order = { quantity, failed: e.response.data.error.message } books.order = { quantity, failed: e.response.data.error ? e.response.data.error.message : e.response.data }
}
} }
},
async fetchUserInfo() {
try {
const { data } = await axios.get('/user/me')
books.user = data
} catch (err) { books.user = { id: err.message } }
} }
}) }
}).mount("#app")
// initially fill list of books // initially fill list of books
books.fetch() books.fetch()
books.fetchUserInfo()
document.addEventListener('keydown', (event) => {
// hide user info on request
if (event.key === 'u') books.user = undefined
})

View File

@@ -5,19 +5,26 @@
<title> Capire Books </title> <title> Capire Books </title>
<link rel="stylesheet" href="https://unpkg.com/primitive-ui/dist/css/main.css"> <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/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
<style> <style>
.hovering tr:hover td { color:cyan; background: #123; cursor: pointer; } .hovering tr:hover td { color:cyan; background: #123; cursor: pointer; }
.rating-stars { color:teal } .rating-stars { color:teal }
.succeeded { color:teal } .succeeded { color:teal }
.failed { color:red } .failed { color:red }
.user {text-align: end; color: grey;}
</style> </style>
</head> </head>
<body class="small-container", style="margin-top: 70px;"> <body class="small-container", style="margin-top: 70px;">
<div id='app'> <div id='app'>
<h1> {{ document.title }} </h1> <div v-if="user" class="user">
<div>User: {{ user.id || 'anonymous' }}</div>
<div>Locale: {{ user.locale }}</div>
<div v-if="user.tenant">Tenant: {{ user.tenant }}</div>
</div>
<h1> Capire Books </h1>
<input type="text" placeholder="Search..." @input="search"> <input type="text" placeholder="Search..." @input="search">

View File

@@ -0,0 +1,2 @@
using from '..\..\schema';

View File

@@ -1,7 +1,14 @@
using { Currency, managed, sap } from '@sap/cds/common'; using {
Currency,
managed,
sap,
extensible
} from '@sap/cds/common';
namespace sap.capire.bookshop; namespace sap.capire.bookshop;
entity Books : managed { @Extensibility.Any.Enabled : true
entity Books : managed, extensible {
key ID : Integer; key ID : Integer;
title : localized String(111); title : localized String(111);
descr : localized String(1111); descr : localized String(1111);
@@ -11,21 +18,39 @@ entity Books : managed {
price : Decimal; price : Decimal;
currency : Currency; currency : Currency;
image : LargeBinary @Core.MediaType : 'image/png'; image : LargeBinary @Core.MediaType : 'image/png';
authorName : String;
} }
entity Authors : managed {
entity Authors : managed, extensible {
key ID : Integer; key ID : Integer;
name : String(111); name : String(111);
dateOfBirth : Date; dateOfBirth : Date;
dateOfDeath : Date; dateOfDeath : Date;
placeOfBirth : String; placeOfBirth : String;
placeOfDeath : String; placeOfDeath : String;
books : Association to many Books on books.author = $self;
books : Association to many Books
on books.author = $self;
} }
/** Hierarchically organized Code List for Genres */ extend Authors with {
virtual age : Integer;
virtual exampleBook: String;
}
/**
* Hierarchically organized Code List for Genres
*/
entity Genres : sap.common.CodeList { entity Genres : sap.common.CodeList {
key ID : Integer; key ID : Integer;
parent : Association to Genres; parent : Association to Genres;
children : Composition of many Genres on children.parent = $self; children : Composition of many Genres
on children.parent = $self;
}
entity Publishers: managed {
key ID: Integer;
name: String(111);
} }

View File

@@ -0,0 +1,17 @@
async function run() {
//debugger
//while (true) {}
//process.exit()
//1.substring()
// let res = await specialselect
let res = await SELECT.one`title`.from(`Books`).where(`ID=201`)
let { title } = res
let Author = req.data
//await srv.read('Books')
Author.modifiedBy = "Custom Event handler changed this!"
Author.placeOfDeath = " --- Somewhere over " + title + " --- create in Sandbox"
//await this.emit("createdAuthor", { Author })
return Author
}
run()

View File

@@ -0,0 +1,41 @@
function getYear(v) {
return parseInt(v.substr(0, 4))
}
function getMonth(v) {
return parseInt(v.substr(5, 2))
}
function getDay(v) {
return parseInt(v.substr(8, 2))
}
function getAge(from, to) {
if (from === undefined || from == null) return 0
if (to === undefined || to == null) to = new Date().toISOString()
let year = getYear(to) - getYear(from) - 1
if (
getMonth(to) > getMonth(from) ||
(getMonth(to) === getMonth(from) && getDay(to) >= getDay(from))
) {
year++
}
return year
}
async function run() {
const result_ = Array.isArray(result) ? result : [result]
for (const row of result_) {
row.age = getAge(row.dateOfBirth, row.dateOfDeath)
let res = await SELECT.one`title`.from(`Books`).where({ author_ID: row.ID })
if (!res) {
res = {}
}
let { title } = res
if (!title) {
title = "no Books yet"
}
row.exampleBook = title
//let pub = await SELECT.one`name`.from(`sap_capire_bookshop_Publishers`)
}
}
run()

View File

@@ -0,0 +1,9 @@
async function run() {
const {stock, price, author_ID} = req.data
if (stock<0) return req.reject('409', 'Stock must not be negative')
if (price<0) return req.reject('409', 'Price must not be negative')
let {name} = await SELECT.one`name`.from(`Authors`).where({ID: author_ID})
req.data.authorName=name
}
output = run()

View File

@@ -0,0 +1,6 @@
const result_ = Array.isArray(result) ? result : [result];
for (const row of result_) {
if (row.stock > 50) {
row.title += " ---Order now for a 10% discount!";
}
}

View File

@@ -0,0 +1,9 @@
async function run() {
const {author, newName} = req.data
let a = await SELECT `name`.from(`Authors`).where({ID: author})
if(!a) return req.error (404, `Can't rename a non-existing author`)
await UPDATE (`Authors`,author).with ({ name: newName })
//await this.emit ('renamedAuthor', { author, newName })
output.msg = 'Success'
}
run()

View File

@@ -0,0 +1,5 @@
async function run() {
let {Author} = req.data
Author.placeOfBirth += ' --- modified in custom event'
}
run()

112
bookshop/notebook.md Normal file
View File

@@ -0,0 +1,112 @@
# Base assumption
Event handlers will always use **publicly available application API's**(services)
- already done in Sandbox API by overwriting **SELECT**, **UPDATE**, **READ** and **CREATE**
## Inbound data for validations
- req.target plus expand on related data
- lazy loading on expand
- event facade could have an explicit publishing of specific services or documents (e.g. remote services)
- CQN Protocol adapter for subsequent reads --> req.data plus application service calls
- what is the CDS subset to put in?
- req.data + target-rec (proxy, unloaded)
- ORM type lazy loading (dereferenced)
- application developer could actually provide custom proxies for specific functions
- performance impact of multiple accesses to object graph and multiple DB roundtrips
- can static code checking or developer annotations influence what is loaded into a graph?
- alternative: Stripped-down SELECT limited to req.target and ID
- application service only
- access rights of user respected
- What about to-many relationships? For compositions essential, for associations to be questioned
- Application Service Reads
- outbound data for changes
- call remote services
- register new remote services dynamically
- CAP provides an API on remote services - connect doesn't need to be done by extension developer
- alternative: declarative remote services plumbing with CDS service facade
- model looks like static internal services, remote calls done transparently behind the scenes
-Emit Events
- choreography of extension points
- deep inserts vs. fine grained operations
- input validation may be suited for fine grained operations
- today not in scope for performance reasons
- two different use case: Insert new page to book vs. update order-header with items-constraints in place
- reject request, return errors and warnings - suitable for UI, too
*/
/*
Annotations available:
Entity level
@expression.constraint : [{if: 'expression evaluates to bool'}, on: ['INSERT, UPDATE, DELETE'], error: 'Transaction Rollback and error message', warning: 'Transaction proceeds and warning message']
@expression.computed : [{expression: 'ability to access request payload and modify it', on: ['INSERT, UPDATE']}]
@event : [{if: 'expression evaluates to bool', on: ['INSERT, UPDATE, DELETE, READ'], when: 'before or after, default before', emit: 'Event Name', to: 'Messaging target, optional'}]
@expresion.code :[{file: 'file name', on:['insert', 'update'], when: 'before or after, default before'},
{source: 'each => { if (each.stock > 111) {each.title += `-- 11% discount!`; each.price= each.price*0.9}', on:['insert', 'update'], when: 'before or after'}]
Atribute Level
@assert.constraint : {if: 'stock>=0 OR stock <1000', error: 'i18n/error102'};
@event : {if: 'expression evaluates to bool', on: ['INSERT, UPDATE, DELETE, READ'], when: 'before or after', emit: 'Event Name', to: 'Messaging target, optional' }
Functions available:
EXISTS(association target)
COUNT,AVG,MIN,MAX,SUM: Composition items, arrays etc
OLD: before image
EACH: loop over composition items
Events covered:
CRUD --> Longhand and Shorthand supported?
Upsert as one event?
Before and after:
Before can change change request payload and stop transaction
After should trigger only asynchronous messages
Specific Events for status changes? I think expression based event emitter suffices
*/
//Entity level annotations
@expression.constraint : [{if: 'stock>100 AND price>15)', on: ['INSERT', 'UPDATE'], error: 'No Book over price 15 should have more than 100 stock' }, // error, rollback transactions
{if: 'stock>90 AND price>15)', on: ['I', 'U'], warning: 'No Book over price 15 should have more than 100 stock' }] //warning, proceed with transaction but report warning back to UI
@expression.computed : {expression: 'if(stock>100) then price=price*0.9', on: ['INSERT']} //ability to modify the payload of the request, but nothing beyond it
@expresion.code :[{file: 'sap.capire.bookshop-Books-beforeInsert', on:['insert', 'update'], when: 'before'}, //naming can be arbitrary?
{source: 'each => { if (each.stock > 111) {each.title += `-- 11% discount!`; each.price= each.price*0.9}', on:['insert', 'update'], when: 'before'}] //alternative
@event : { if:'price>200', emit: 'Expensive Book', to: 'RulesEngine'}
entity Books : managed {
key ID : Integer;
title : localized String(111); @event : {if: 'old.title="Hello"', emit: 'Hello changed' } //old refers to before Image. No "to" clause means message is emitted to any subscriber interested
descr : localized String(1111);
author : Association to Authors @assert.constraint: 'exists(author)'; //function calls need to evaluate to bool
genre : Association to Genres;
stock : Integer @assert.constraint : {if: 'stock>=0 OR stock <1000', error: 'Stock not within permitted parameters'}; //when operand is used, no auto-insert
price : Decimal(9,2) @assert.constraint : '>0'; //insert operand on left side by default
currency : Currency;
image : LargeBinary @Core.MediaType : 'image/png';
stockWorth: Decimal(9,2) @expression.computed : 'stock*price'; //persisted on write. Overhead in runtime, but performance benefit on read. Payload ignored?
// stockWorth2 = stock*price; -- long term goal from compiler team, not persisted on write, but calculated on read
stockWorth3 : Decimal @expression.computed: 'if (stock*price>1000) then stockWorth3=stock.price else stockworth3=1000'; //which altenative?
stockWorth4 : Decimal @expression.computed: {if: '(stock*price>1000)', then: 'stockWorth3=stock.price', else: 'stockworth3=1000'};
}
//@assert.expression: 'dateOfBirth<dateOfDeath'
entity Authors : managed {
key ID : Integer;
name : String(111);
dateOfBirth : Date ;
dateOfDeath : Date @expression.constraint: '>dateOfBirth';
placeOfBirth : String;
placeOfDeath : String;
books : Association to many Books on books.author = $self;
}
/** Hierarchically organized Code List for Genres */
entity Genres : sap.common.CodeList {
key ID : Integer;
parent : Association to Genres;
children : Composition of many Genres on children.parent = $self;
}
```
```

View File

@@ -2,10 +2,18 @@
"name": "@capire/bookshop", "name": "@capire/bookshop",
"version": "1.0.0", "version": "1.0.0",
"description": "A simple self-contained bookshop service.", "description": "A simple self-contained bookshop service.",
"files": [
"app",
"srv",
"db",
"index.cds",
"index.js"
],
"dependencies": { "dependencies": {
"@sap/cds": "^5.0.4", "@sap/cds": ">=5.9",
"express": "^4.17.1", "express": "^4.17.1",
"passport": "0.4.1" "passport": ">=0.4.1",
"vm2": ">=3.9.9"
}, },
"scripts": { "scripts": {
"genres": "cds serve test/genres.cds", "genres": "cds serve test/genres.cds",
@@ -14,8 +22,12 @@
}, },
"cds": { "cds": {
"requires": { "requires": {
"code-extensibility" : true,
"db": { "db": {
"kind": "sql" "kind": "sqlite",
"credentials": {
"database": "sqlite.db"
}
} }
} }
} }

BIN
bookshop/sqlite.db Normal file

Binary file not shown.

View File

@@ -1,5 +1,30 @@
using { sap.capire.bookshop as my } from '../db/schema'; using {sap.capire.bookshop as my} from '../db/schema';
service AdminService @(requires:'admin') {
entity Books as projection on my.Books; service AdminService // @(requires : 'admin')
entity Authors as projection on my.Authors; {
entity Books as projection on my.Books actions {
action increaseStock(count : Integer);
function stock() returns Integer;
};
@Extensibility : {
Fields.Enabled : true,
Fields.Quota: 100,
Relations.Enabled : false,
Annotations.Enabled : true,
Logic.Enabled : true,
Logic.constraints: true,
Logic.calculations: true,
Logic.Handler : [create, update, delete, read]
}
entity Authors as projection on my.Authors;
action renameAuthor(author : Authors:ID, newName : String) returns {
msg : String
};
function getStock(book: Books:ID) returns Integer;
event newBook : {
book : Books:ID;
name : Books:title
};
} }

View File

@@ -1,12 +1,135 @@
const cds = require('@sap/cds') const cds = require("@sap/cds")
//const cds_sandbox = require("sap/cds/sandbox")
const { VM, VMScript } = require("vm2")
const fs = require("fs")
const path = require("path")
const { nextTick } = require("process")
module.exports = cds.service.impl (function(){ class AdminService extends cds.ApplicationService {
this.before ('NEW','Authors', genid) init() {
this.before ('NEW','Books', genid) this.after("READ", async (result, req) => {
}) if (!(result === undefined || result == null)) {
const code = getCode(req.target.name, "READ")
if (code) {
await executeCode.call(this, code, req, result)
}
}
})
/** Generate primary keys for target entity in request */ this.before("CREATE", async (req) => {
async function genid (req) { const code = getCode(req.target.name, "CREATE")
const {ID} = await cds.tx(req).run (SELECT.one.from(req.target).columns('max(ID) as ID')) if (code) {
req.data.ID = ID - ID % 100 + 100 + 1 await executeCode.call(this, code, req)
}
})
this.before("UPDATE", async (req) => {
const code = getCode(req.target.name, "CREATE")
if (code) {
await executeCode.call(this, code, req)
}
})
this.on("*", async (req, next) => {
if (!(req.target === undefined || req.target == null)) return next()
//ToDo: check whether action or event is part of an extension
// DO NOT OVERWRITE EXISTING Action Implementations!
// evaluate: Can we augment action implementation with super.next?
if (req.constructor.name === "EventMessage") {
const code = getCode(req.event, "ON")
if (code) {
await executeCode.call(this, code, req)
}
} else if (req.constructor.name === "ODataRequest") {
var output = {}
const code = getCode(this.name + "." + req.event, "ON")
if (code) {
await executeCode.call(this, code, req, {}, output)
return output
}
}
})
//ToDo: Prefix for Service not in event emitter
this.before("CREATE", "Authors", async (req) => {
let Author = req.data
await this.emit("createdAuthor", { Author })
})
return super.init()
}
} }
var counter = 1;
function newLabel() {return "VM2 - req: " + counter++}
//should only work in local exection (cds watch)
// alternative: Upon Bootstrapping, merge files into CSN
function getCodeFromFile(name, operation) {
const filename = name + "." + operation + ".js"
const file = path.join(__dirname, "..", "handlers", filename)
try {
const code = fs.readFileSync(file, "utf8")
return code
} catch (error) {
return ""
}
}
//after push this should be the only thing that works
function getCodeFromAnnotation(name, operation) {
return ""
}
function getCode(name, operation) {
let code=getCodeFromAnnotation(name, operation)
if (code==="") {code=getCodeFromFile(name, operation)}
return code
}
function scanCode(code) {
//ESLINT
}
async function executeCode(code, req, result, output) {
const srv = this
const label=newLabel()
console.time(label)
const vm = new VM({
console: "inherit",
timeout: 500,
allowAsync: true,
sandbox: { req, //todo: isolate req.data, req.reject, req.error, req.message
result, //important for READ
output, //used for Action Implementation
SELECT : (class extends require('@sap/cds/lib/ql/SELECT') {then(r,e) {return srv.run(this).then(r,e)}})._api(),
INSERT : (class extends require('@sap/cds/lib/ql/INSERT') {then(r,e) {return srv.run(this).then(r,e)}})._api(),
UPDATE : (class extends require('@sap/cds/lib/ql/UPDATE') {then(r,e) {return srv.run(this).then(r,e)}})._api(),
CREATE : (class extends require('@sap/cds/lib/ql/CREATE') {then(r,e) {return srv.run(this).then(r,e)}})._api(),
//srv: this,
JSON },
})
try {
await vm.run(code)
return output
} catch (error) {
console.log(error)
req.reject("409", "Error in VM")
}
finally {
console.timeEnd(label)
}
// console.log(req.data)
}
/** Generate primary keys for target entity in request */
async function genid(req) {
const { ID } = await cds
.tx(req)
.run(SELECT.one.from(req.target).columns("max(ID) as ID"))
req.data.ID = ID - (ID % 100) + 100 + 1
}
module.exports = { AdminService }

View File

@@ -10,6 +10,8 @@ service CatalogService @(path:'/browse') {
author.name as author author.name as author
} excluding { createdBy, modifiedBy }; } excluding { createdBy, modifiedBy };
@readonly entity Publishers as projection on my.Publishers;
@requires: 'authenticated-user' @requires: 'authenticated-user'
action submitOrder ( book: Books:ID, quantity: Integer ) returns { stock: Integer }; action submitOrder ( book: Books:ID, quantity: Integer ) returns { stock: Integer };
event OrderedBook : { book: Books:ID; quantity: Integer; buyer: String }; event OrderedBook : { book: Books:ID; quantity: Integer; buyer: String };

View File

@@ -0,0 +1,11 @@
//this file is machine-created during cds.build
namespace sap.capire.bookshop; //> important for reflection
using from '../db/schema';
using from '../srv/cat-service';
using from '../srv/admin-service';
annotate AdminService.Authors with @extension.logic: [{when: 'CREATE', code: 'async function run() {\r\n \/\/debugger\r\n \/\/while (true) {}\r\n \/\/process.exit()\r\n \/\/1.substring()\r\n \/\/ let res = await specialselect\r\n let res = await SELECT.one`title`.from(`Books`).where(`ID=201`)\r\n let { title } = res\r\n let Author = req.data\r\n Author.modifiedBy = \"Custom Event handler changed this!\"\r\n Author.placeOfDeath = \" --- Somewhere over \" + title + \" --- create in Sandbox\"\r\n \/\/await this.emit(\"createdAuthor\", { Author })\r\n return Author\r\n}\r\nrun()\r\n'},
{when: 'READ', code: 'function getYear(v) {\r\n return parseInt(v.substr(0, 4))\r\n}\r\nfunction getMonth(v) {\r\n return parseInt(v.substr(5, 2))\r\n}\r\nfunction getDay(v) {\r\n return parseInt(v.substr(8, 2))\r\n}\r\n\r\nfunction getAge(from, to) {\r\n if (from === undefined || from == null) return 0\r\n if (to === undefined || to == null) to = new Date().toISOString()\r\n let year = getYear(to) - getYear(from) - 1\r\n if (\r\n getMonth(to) > getMonth(from) ||\r\n (getMonth(to) === getMonth(from) && getDay(to) >= getDay(from))\r\n ) {\r\n year++\r\n }\r\n return year\r\n}\r\n\r\nconst result_ = Array.isArray(result) ? result : [result]\r\nfor (const row of result_) {\r\n row.modifiedBy += \" --- read in sandbox\"\r\n row.age = getAge(row.dateOfBirth, row.dateOfDeath)\r\n}'}
];
annotate AdminService.Books with @extension.logic;
annotate CatalogService.ListOfBooks with @extension.logic;

View File

@@ -0,0 +1,16 @@
/**
* Exposes user information
*/
@requires: 'authenticated-user'
service UserService {
/**
* The current user
*/
@odata.singleton entity me {
id : String; // user id
locale : String;
tenant : String;
}
}

View File

@@ -0,0 +1,4 @@
const cds = require('@sap/cds')
module.exports = cds.service.impl((srv) => {
srv.on('READ', 'me', ({ tenant, user, locale }) => ({ id: user.id, locale, tenant }))
})

View File

@@ -1,10 +1,46 @@
@server = http://localhost:4004 @server = http://localhost:4004
@me = Authorization: Basic {{$processEnv USER}}: @me = Authorization: Basic {{$processEnv USER}}:
@id = 2000
### ------------------------------------------------------------------------
# Fetch Authors
GET {{server}}/admin/Authors
### ------------------------------------------------------------------------
# Fetch one Author
GET {{server}}/admin/Authors({{id}})
### ------------------------------------------------------------------------
# Create Author
POST {{server}}/admin/Authors
Content-Type: application/json;IEEE754Compatible=true
{
"ID": {{id}},
"name": "Nick",
"placeOfBirth": "Somewhere",
"placeOfDeath": "over the Rainbox",
"dateOfBirth" : "1975-05-27"
}
### ------------------------------------------------------------------------
# rename author via unbound action
POST {{server}}/admin/renameAuthor
Content-Type: application/json
{{me}}
{ "author":{{id}}, "newName":"Super Nick" }
### ------------------------------------------------------------------------ ### ------------------------------------------------------------------------
# Get service info # Get service info
GET {{server}}/browse GET {{server}}/admin
{{me}}
### ------------------------------------------------------------------------
# Get $metadata document
GET {{server}}/admin/$metadata
{{me}} {{me}}
@@ -16,34 +52,19 @@ GET {{server}}/browse/$metadata
### ------------------------------------------------------------------------ ### ------------------------------------------------------------------------
# Browse Books as any user # Browse Books as any user
GET {{server}}/browse/Books? GET {{server}}/browse/ListOfBooks?
# &$select=title,stock # &$select=title,stock
# &$expand=currency &$expand=genre
# &sap-language=de # &sap-language=de
{{me}} {{me}}
### ------------------------------------------------------------------------
# Fetch Authors as admin
GET {{server}}/admin/Authors?
# &$select=name,dateOfBirth,placeOfBirth
# &$expand=books($select=title;$expand=currency)
# &$filter=ID eq 101
# &sap-language=de
Authorization: Basic alice:
### ------------------------------------------------------------------------ ### ------------------------------------------------------------------------
# Create Author # Fetch Books as admin
POST {{server}}/admin/Authors GET {{server}}/admin/Books
Content-Type: application/json;IEEE754Compatible=true
Authorization: Basic alice:
{
"ID": 112,
"name": "Shakespeeeeere",
"age": 22
}
### ------------------------------------------------------------------------ ### ------------------------------------------------------------------------
# Create book # Create book
@@ -52,12 +73,12 @@ Content-Type: application/json;IEEE754Compatible=true
Authorization: Basic alice: Authorization: Basic alice:
{ {
"ID": 2, "ID": 16,
"title": "Poems : Pocket Poets", "title": "Deh4",
"descr": "The Everyman's Library Pocket Poets hardcover series is popular for its compact size and reasonable price which does not compromise content. Poems: Bronte contains poems that demonstrate a sensibility elemental in its force with an imaginative discipline and flexibility of the highest order. Also included are an Editor's Note and an index of first lines.", "descr": "The Everyman's Library Pocket Poets hardcover series is popular for its compact size and reasonable price which does not compromise content. Poems: Bronte contains poems that demonstrate a sensibility elemental in its force with an imaginative discipline and flexibility of the highest order. Also included are an Editor's Note and an index of first lines.",
"author": { "ID": 101 }, "author": { "ID": 101 },
"genre": { "ID": 12 }, "genre": { "ID": 12 },
"stock": 5, "stock": -100,
"price": "12.05", "price": "12.05",
"currency": { "code": "USD" } "currency": { "code": "USD" }
} }

View File

@@ -7,7 +7,7 @@
"@capire/orders": "*", "@capire/orders": "*",
"@capire/common": "*", "@capire/common": "*",
"@capire/data-viewer": "*", "@capire/data-viewer": "*",
"@sap/cds": "^5", "@sap/cds": ">=5",
"express": "^4.17.1" "express": "^4.17.1"
}, },
"cds": { "cds": {

View File

@@ -3,7 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"description": "A generic browser for data", "description": "A generic browser for data",
"dependencies": { "dependencies": {
"@sap/cds": "^5.0.4" "@sap/cds": ">=5.0.4"
}, },
"files": [ "files": [
"app", "app",

View File

@@ -39,7 +39,7 @@ annotate AdminService.Authors with @(UI : {
// Workaround to avoid errors for unknown db-specific calculated fields above // Workaround to avoid errors for unknown db-specific calculated fields above
extend sap.capire.bookshop.Authors with { extend sap.capire.bookshop.Authors with {
virtual age : Integer; //virtual age : Integer;
virtual lifetime : String; virtual lifetime : String;
} }

View File

@@ -11,7 +11,7 @@
}, },
"dataSources": { "dataSources": {
"AdminService": { "AdminService": {
"uri": "/admin/", "uri": "admin/",
"type": "OData", "type": "OData",
"settings": { "settings": {
"odataVersion": "4.0" "odataVersion": "4.0"
@@ -50,6 +50,10 @@
} }
}, },
"sap.ui5": { "sap.ui5": {
"flexEnabled": true,
"config": {
"experimentalCAPScenario": true
},
"dependencies": { "dependencies": {
"minUI5Version": "1.81.0", "minUI5Version": "1.81.0",
"libs": { "libs": {

View File

@@ -8,7 +8,7 @@
"i18n": "i18n/i18n.properties", "i18n": "i18n/i18n.properties",
"dataSources": { "dataSources": {
"AdminService": { "AdminService": {
"uri": "/admin/", "uri": "admin/",
"type": "OData", "type": "OData",
"settings": { "settings": {
"odataVersion": "4.0" "odataVersion": "4.0"
@@ -22,6 +22,10 @@
} }
}, },
"sap.ui5": { "sap.ui5": {
"flexEnabled": true,
"config": {
"experimentalCAPScenario": true
},
"dependencies": { "dependencies": {
"libs": { "libs": {
"sap.fe.templates": {} "sap.fe.templates": {}

View File

@@ -11,7 +11,7 @@
}, },
"dataSources": { "dataSources": {
"CatalogService": { "CatalogService": {
"uri": "/browse/", "uri": "browse/",
"type": "OData", "type": "OData",
"settings": { "settings": {
"odataVersion": "4.0" "odataVersion": "4.0"
@@ -53,6 +53,10 @@
} }
}, },
"sap.ui5": { "sap.ui5": {
"flexEnabled": true,
"config": {
"experimentalCAPScenario": true
},
"dependencies": { "dependencies": {
"minUI5Version": "1.81.0", "minUI5Version": "1.81.0",
"libs": { "libs": {

View File

@@ -10,7 +10,21 @@
<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>
@@ -18,11 +32,15 @@
<script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js" <script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout" data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout"
data-sap-ui-compatVersion="edge" data-sap-ui-compatVersion="edge"
data-sap-ui-theme="sap_fiori_3" data-sap-ui-theme="sap_horizon"
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
.getCore()
.getConfiguration()
.setFlexibilityServices([{ connector: "SessionStorageConnector" }]);
</script> </script>
</head> </head>

View File

@@ -3,9 +3,9 @@
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@capire/bookstore": "*", "@capire/bookstore": "*",
"@sap/cds": "^5", "@sap/cds": ">=5",
"express": "^4.17.1", "express": "^4.17.1",
"passport": "^0.4.1" "passport": ">=0.4.1"
}, },
"scripts": { "scripts": {
"start": "cds run --in-memory?", "start": "cds run --in-memory?",
@@ -14,7 +14,7 @@
"cds": { "cds": {
"requires": { "requires": {
"auth": { "auth": {
"strategy": "dummy" "kind": "dummy-auth"
}, },
"ReviewsService": { "ReviewsService": {
"kind": "odata", "kind": "odata",
@@ -36,7 +36,10 @@
} }
}, },
"db": { "db": {
"kind": "sql" "kind": "sqlite",
"credentials": {
"database": "sqlite.db"
}
}, },
"db-ext": { "db-ext": {
"[development]": { "[development]": {

View File

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

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
cf delete gdpr-srv -f
cf delete gdpr-db-deployer -f
cf delete-service gdpr-pdm -f
cf delete-service gdpr-auditlog -f
cf delete-service gdpr-uaa -f
cf delete-service gdpr-hdi -f
cf delete-service gdpr-logs -f

View File

@@ -1,16 +0,0 @@
{
"fullyQualifiedApplicationName": "capire-gdpr",
"fullyQualifiedModuleName": "gdpr-srv",
"applicationTitle": "Capire GDPR Sample App",
"applicationTitleKey": "Capire GDPR Sample App",
"applicationURL": "https://capire-gdpr-srv.cfapps.eu10.hana.ondemand.com",
"endPoints": [{
"type": "odatav4",
"serviceName": "PDMService",
"serviceURI": "/pdm",
"serviceTitle": "Capire GDPR Sample App PDM Service",
"serviceTitleKey": "Capire GDPR Sample App PDM Service",
"hasGdprV4Annotations": true,
"cacheControl": "no-cache"
}]
}

View File

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

View File

@@ -1,317 +0,0 @@
////////////////////////////////////////////////////////////////////////////
//
// Note: this is designed for the GDPRService being co-located with
// orders. It does not work if GDPRService is run as a separate
// process, and is not intended to do so.
//
////////////////////////////////////////////////////////////////////////////
using {GDPRService} from '../srv/gdpr-service';
annotate cds.UUID with @Core.Computed;
/*
* Orders
*/
@odata.draft.enabled
annotate GDPRService.Orders with @(UI : {
SelectionFields : [
createdAt,
createdBy
],
LineItem : [
{
Value : OrderNo,
Label : 'Order number'
},
{
Value : customer.firstName,
Label : 'First Name'
},
{
Value : customer.lastName,
Label : 'Last Name'
}
],
HeaderInfo : {
TypeName : 'Order',
TypeNamePlural : 'Orders',
Title : {
Value : OrderNo,
Label : 'Order number'
}
},
Identification : [
{
Value : createdBy,
Label : 'Created by'
},
{
Value : createdAt,
Label : 'Created at'
}
],
HeaderFacets : [
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Created}',
Target : '@UI.FieldGroup#Created'
},
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Modified}',
Target : '@UI.FieldGroup#Modified'
},
],
Facets : [
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Details}',
Target : '@UI.FieldGroup#Details'
},
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>OrderItems}',
Target : 'Items/@UI.LineItem'
},
],
FieldGroup #Details : {Data : [
{
Value : customer_ID,
Label : 'Customer'
},
{
Value : customer.firstName,
Label : 'First Name'
},
{
Value : customer.lastName,
Label : 'Last Name'
},
{
Value : currency_code,
Label : 'Currency'
}
]},
FieldGroup #Created : {Data : [
{
Value : createdBy,
Label : 'Created by'
},
{
Value : createdAt,
Label : 'Created at'
}
]},
FieldGroup #Modified : {Data : [
{
Value : modifiedBy,
Label : 'Modified by'
},
{
Value : modifiedAt,
Label : 'Modified at'
}
]},
}, ) {
createdAt @UI.HiddenFilter : false;
createdBy @UI.HiddenFilter : false;
customer @ValueList.entity : 'Customers';
};
/*
* TODO: Order Items are not really maintainable in Fiori preview app
*/
annotate GDPRService.Orders.Items with @(UI : {
LineItem : [
{
Value : product_ID,
Label : 'Product ID'
},
{
Value : title,
Label : 'Product Name'
},
{
Value : price,
Label : 'Price'
},
{
Value : quantity,
Label : 'Quantity'
},
],
Identification : [
{
Value : product_ID,
Label : 'Product ID'
},
{
Value : title,
Label : 'Product Name'
},
{
Value : quantity,
Label : 'Quantity'
},
{
Value : price,
Label : 'Price'
},
],
Facets : [{
$Type : 'UI.ReferenceFacet',
Label : 'Order Items',
Target : '@UI.Identification'
}, ],
}, ) {
ID @Core.Computed @UI.Hidden : true;
title @Core.Computed;
price @Core.Computed;
quantity @(Common.FieldControl : #Mandatory);
};
/*
* Customers
*/
@odata.draft.enabled
annotate GDPRService.Customers with @(UI : {
SelectionFields : [
firstName,
lastName
],
LineItem : [
{
Value : firstName,
Label : 'First Name'
},
{
Value : lastName,
Label : 'Last Name'
},
{
Value : dateOfBirth,
Label : 'Date of Birth'
}
],
HeaderInfo : {
TypeName : 'Customer',
TypeNamePlural : 'Customers',
Title : {
Value : lastName,
Label : 'Last Name'
},
Description : {
Value : firstName,
Label : 'First Name'
}
},
Identification : [
{
Value : createdBy,
Label : 'Created by'
},
{
Value : createdAt,
Label : 'Created at'
}
],
HeaderFacets : [
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Created}',
Target : '@UI.FieldGroup#Created'
},
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Modified}',
Target : '@UI.FieldGroup#Modified'
},
],
Facets : [
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Details}',
Target : '@UI.FieldGroup#Details'
},
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Addresses}',
Target : 'addresses/@UI.LineItem'
},
],
FieldGroup #Details : {Data : [
{
Value : dateOfBirth,
Label : 'Date of Birth'
},
{
Value : email,
Label : 'E-Mail'
},
{
Value : creditCardNo,
Label : 'Credit Card Number'
}
]},
FieldGroup #Created : {Data : [
{
Value : createdBy,
Label : 'Created by'
},
{
Value : createdAt,
Label : 'Created at'
}
]},
FieldGroup #Modified : {Data : [
{
Value : modifiedBy,
Label : 'Modified by'
},
{
Value : modifiedAt,
Label : 'Modified at'
}
]},
}, ) {
createdAt @UI.HiddenFilter : false;
createdBy @UI.HiddenFilter : false;
};
annotate GDPRService.CustomerPostalAddresses with @(UI : {
LineItem : [
{
Value : town,
Label : 'Town'
},
{
Value : street,
Label : 'Street'
},
{
Value : country.name,
Label : 'Country'
}
],
Identification : [
{
Value : town,
Label : 'Town'
},
{
Value : street,
Label : 'Street'
},
{
Value : country_code,
Label : 'Country Code'
}
],
Facets : [{
$Type : 'UI.ReferenceFacet',
Label : 'Customer Postal Address',
Target : '@UI.Identification'
}, ],
}, );

View File

@@ -1,56 +0,0 @@
using {sap.capire.orders} from '@capire/orders';
using {sap.capire.gdpr} from './schema';
/*
* annotations for Data Privacy (Personal Data Manager and Audit Logging)
*/
annotate gdpr.Customers with @PersonalData : {
DataSubjectRole : 'Customer',
EntitySemantics : 'DataSubject'
}{
ID @PersonalData.FieldSemantics : 'DataSubjectID';
email @PersonalData.IsPotentiallyPersonal;
firstName @PersonalData.IsPotentiallyPersonal;
lastName @PersonalData.IsPotentiallyPersonal;
creditCardNo @PersonalData.IsPotentiallySensitive;
dateOfBirth @PersonalData.IsPotentiallyPersonal;
}
annotate gdpr.CustomerPostalAddresses with @PersonalData : {
DataSubjectRole : 'Customer',
EntitySemantics : 'DataSubjectDetails'
}{
customer @PersonalData.FieldSemantics : 'DataSubjectID';
street @PersonalData.IsPotentiallyPersonal;
town @PersonalData.IsPotentiallyPersonal;
country @PersonalData.IsPotentiallyPersonal;
}
/*
* TODO: Personal Data Manager doesn't know EntitySemantics: 'Other' and FieldSemantics: 'ContractRelatedID'
* see: https://help.sap.com/viewer/620a3ea6aaf64610accdd05cca9e3de2/Cloud/en-US/5a55fae1eb7c496c92c56071186d76b3.html
*/
annotate orders.Orders with @PersonalData : {
DataSubjectRole : 'Customer',
EntitySemantics : 'LegalGround'
}{
ID @PersonalData.FieldSemantics : 'LegalGroundID';
customer @PersonalData.FieldSemantics : 'DataSubjectID';
}
/*
* additional annotations for Audit Logging
*/
annotate gdpr.Customers with @AuditLog.Operation : {
Read : true,
Insert : true,
Update : true,
Delete : true
};
annotate gdpr.CustomerPostalAddresses with @AuditLog.Operation : {
Read : true,
Insert : true,
Update : true,
Delete : true
};

View File

@@ -1,3 +0,0 @@
ID;modifiedAt;createdAt;createdBy;modifiedBy;customer_ID;street;town;country_code
1e2f2640-6866-4dcf-8f4d-3027aa831cad;2019-04-04;2019-01-31;admin@business.com;admin@business.com;8e2f2640-6866-4dcf-8f4d-3027aa831cad;Hauptstrasse 11;Berlin;DE
24e718c9-ff99-47f1-8ca3-950c850777d4;2019-04-04;2019-01-30;admin@business.com;admin@business.com;74e718c9-ff99-47f1-8ca3-950c850777d4;Main Street 22;London;GB
1 ID modifiedAt createdAt createdBy modifiedBy customer_ID street town country_code
2 1e2f2640-6866-4dcf-8f4d-3027aa831cad 2019-04-04 2019-01-31 admin@business.com admin@business.com 8e2f2640-6866-4dcf-8f4d-3027aa831cad Hauptstrasse 11 Berlin DE
3 24e718c9-ff99-47f1-8ca3-950c850777d4 2019-04-04 2019-01-30 admin@business.com admin@business.com 74e718c9-ff99-47f1-8ca3-950c850777d4 Main Street 22 London GB

View File

@@ -1,3 +0,0 @@
ID;modifiedAt;createdAt;createdBy;modifiedBy;email;firstName;lastName;creditCardNo;dateOfBirth
8e2f2640-6866-4dcf-8f4d-3027aa831cad;2019-04-04;2019-01-31;admin@business.com;admin@business.com;john.doe@test.com;John;Doe;9977-6655-4433-2211;1970-01-01
74e718c9-ff99-47f1-8ca3-950c850777d4;2019-04-04;2019-01-30;admin@business.com;admin@business.com;jane.doe@sap.com;Jane;Doe;2211-3344-5566-7788;1980-11-11
1 ID modifiedAt createdAt createdBy modifiedBy email firstName lastName creditCardNo dateOfBirth
2 8e2f2640-6866-4dcf-8f4d-3027aa831cad 2019-04-04 2019-01-31 admin@business.com admin@business.com john.doe@test.com John Doe 9977-6655-4433-2211 1970-01-01
3 74e718c9-ff99-47f1-8ca3-950c850777d4 2019-04-04 2019-01-30 admin@business.com admin@business.com jane.doe@sap.com Jane Doe 2211-3344-5566-7788 1980-11-11

View File

@@ -1,4 +0,0 @@
ID;up__ID;quantity;product_ID;title;price
4bd2c9df-c19f-47b8-a921-3cde0d863b52;29f15ef6-4a13-47d4-aef4-329a403b49eb;1;201;Wuthering Heights;11.11
6c42a40d-5f7c-4c2f-816b-a73c7c28d722;29f15ef6-4a13-47d4-aef4-329a403b49eb;1;271;Catweazle;15
748555fc-2cb0-42b5-a361-dd19a50bd682;31c2bd15-5146-4418-b574-866a08911de7;2;252;Eleonora;28
1 ID up__ID quantity product_ID title price
2 4bd2c9df-c19f-47b8-a921-3cde0d863b52 29f15ef6-4a13-47d4-aef4-329a403b49eb 1 201 Wuthering Heights 11.11
3 6c42a40d-5f7c-4c2f-816b-a73c7c28d722 29f15ef6-4a13-47d4-aef4-329a403b49eb 1 271 Catweazle 15
4 748555fc-2cb0-42b5-a361-dd19a50bd682 31c2bd15-5146-4418-b574-866a08911de7 2 252 Eleonora 28

View File

@@ -1,3 +0,0 @@
ID;createdAt;createdBy;customer_ID;OrderNo;currency_code
29f15ef6-4a13-47d4-aef4-329a403b49eb;2019-01-31;john.doe@test.com;8e2f2640-6866-4dcf-8f4d-3027aa831cad;1;EUR
31c2bd15-5146-4418-b574-866a08911de7;2019-01-30;jane.doe@test.com;74e718c9-ff99-47f1-8ca3-950c850777d4;2;EUR
1 ID createdAt createdBy customer_ID OrderNo currency_code
2 29f15ef6-4a13-47d4-aef4-329a403b49eb 2019-01-31 john.doe@test.com 8e2f2640-6866-4dcf-8f4d-3027aa831cad 1 EUR
3 31c2bd15-5146-4418-b574-866a08911de7 2019-01-30 jane.doe@test.com 74e718c9-ff99-47f1-8ca3-950c850777d4 2 EUR

View File

@@ -1,30 +0,0 @@
using {
Country,
managed,
cuid
} from '@sap/cds/common';
using {sap.capire.orders} from '@capire/orders';
namespace sap.capire.gdpr;
extend orders.Orders with {
customer : Association to Customers;
}
entity Customers : cuid, managed {
email : String;
firstName : String;
lastName : String;
creditCardNo : String;
dateOfBirth : Date;
addresses : Composition of many CustomerPostalAddresses
on addresses.customer = $self;
}
entity CustomerPostalAddresses : cuid, managed {
customer : Association to Customers;
street : String(128);
town : String(128);
@assert.integrity : false
country : Country;
};

View File

@@ -1,136 +0,0 @@
{
"file_suffixes": {
"csv": {
"plugin_name": "com.sap.hana.di.tabledata.source"
},
"hdbafllangprocedure": {
"plugin_name": "com.sap.hana.di.afllangprocedure"
},
"hdbanalyticprivilege": {
"plugin_name": "com.sap.hana.di.analyticprivilege"
},
"hdbcalculationview": {
"plugin_name": "com.sap.hana.di.calculationview"
},
"hdbcollection": {
"plugin_name": "com.sap.hana.di.collection"
},
"hdbconstraint": {
"plugin_name": "com.sap.hana.di.constraint"
},
"hdbdropcreatetable": {
"plugin_name": "com.sap.hana.di.dropcreatetable"
},
"hdbflowgraph": {
"plugin_name": "com.sap.hana.di.flowgraph"
},
"hdbfunction": {
"plugin_name": "com.sap.hana.di.function"
},
"hdbgraphworkspace": {
"plugin_name": "com.sap.hana.di.graphworkspace"
},
"hdbhadoopmrjob": {
"plugin_name": "com.sap.hana.di.virtualfunctionpackage.hadoop"
},
"hdbindex": {
"plugin_name": "com.sap.hana.di.index"
},
"hdblibrary": {
"plugin_name": "com.sap.hana.di.library"
},
"hdbmigrationtable": {
"plugin_name": "com.sap.hana.di.table.migration"
},
"hdbprocedure": {
"plugin_name": "com.sap.hana.di.procedure"
},
"hdbprojectionview": {
"plugin_name": "com.sap.hana.di.projectionview"
},
"hdbprojectionviewconfig": {
"plugin_name": "com.sap.hana.di.projectionview.config"
},
"hdbreptask": {
"plugin_name": "com.sap.hana.di.reptask"
},
"hdbresultcache": {
"plugin_name": "com.sap.hana.di.resultcache"
},
"hdbrole": {
"plugin_name": "com.sap.hana.di.role"
},
"hdbroleconfig": {
"plugin_name": "com.sap.hana.di.role.config"
},
"hdbsearchruleset": {
"plugin_name": "com.sap.hana.di.searchruleset"
},
"hdbsequence": {
"plugin_name": "com.sap.hana.di.sequence"
},
"hdbstatistics": {
"plugin_name": "com.sap.hana.di.statistics"
},
"hdbstructuredprivilege": {
"plugin_name": "com.sap.hana.di.structuredprivilege"
},
"hdbsynonym": {
"plugin_name": "com.sap.hana.di.synonym"
},
"hdbsynonymconfig": {
"plugin_name": "com.sap.hana.di.synonym.config"
},
"hdbsystemversioning": {
"plugin_name": "com.sap.hana.di.systemversioning"
},
"hdbtable": {
"plugin_name": "com.sap.hana.di.table"
},
"hdbtabledata": {
"plugin_name": "com.sap.hana.di.tabledata"
},
"hdbtabletype": {
"plugin_name": "com.sap.hana.di.tabletype"
},
"hdbtrigger": {
"plugin_name": "com.sap.hana.di.trigger"
},
"hdbview": {
"plugin_name": "com.sap.hana.di.view"
},
"hdbvirtualfunction": {
"plugin_name": "com.sap.hana.di.virtualfunction"
},
"hdbvirtualfunctionconfig": {
"plugin_name": "com.sap.hana.di.virtualfunction.config"
},
"hdbvirtualpackagehadoop": {
"plugin_name": "com.sap.hana.di.virtualpackage.hadoop"
},
"hdbvirtualpackagesparksql": {
"plugin_name": "com.sap.hana.di.virtualpackage.sparksql"
},
"hdbvirtualprocedure": {
"plugin_name": "com.sap.hana.di.virtualprocedure"
},
"hdbvirtualprocedureconfig": {
"plugin_name": "com.sap.hana.di.virtualprocedure.config"
},
"hdbvirtualtable": {
"plugin_name": "com.sap.hana.di.virtualtable"
},
"hdbvirtualtableconfig": {
"plugin_name": "com.sap.hana.di.virtualtable.config"
},
"properties": {
"plugin_name": "com.sap.hana.di.tabledata.properties"
},
"tags": {
"plugin_name": "com.sap.hana.di.tabledata.properties"
},
"txt": {
"plugin_name": "com.sap.hana.di.copyonly"
}
}
}

View File

@@ -1,31 +0,0 @@
---
applications:
# -----------------------------------------------------------------------------------
# HANA Database Content Deployer App
# -----------------------------------------------------------------------------------
- name: gdpr-db-deployer
path: gen/db
no-route: true
health-check-type: process
memory: 256M
buildpack: nodejs_buildpack
services:
- gdpr-logs
- gdpr-hdi
# -----------------------------------------------------------------------------------
# Backend Service
# -----------------------------------------------------------------------------------
- name: gdpr-srv
path: gen/srv
memory: 256M
buildpack: nodejs_buildpack
routes:
- route: capire-gdpr-srv.cfapps.eu10.hana.ondemand.com
services:
- gdpr-logs
- gdpr-hdi
- gdpr-uaa
- gdpr-auditlog
# binding with parameters not yet supported -> binding done manually in .etc/deploy.sh
#- name: gdpr-pdm
# parameters: ./pdm-binding-config.json

View File

@@ -1,49 +0,0 @@
{
"name": "@capire/gdpr",
"version": "0.0.1",
"dependencies": {
"@capire/orders": "../orders",
"@sap/audit-logging": "^5.1.0",
"@sap/cds": "^5.9",
"express": "^4.17.1",
"hdb": "^0.19.0"
},
"scripts": {
"build": "rm -rf gen && cds build --production",
"deploy": "sh .etc/deploy.sh",
"undeploy": "sh .etc/undeploy.sh",
"start": "cds run"
},
"cds": {
"requires": {
"auth": {
"__comment__": "workaround to avoid approuter et al. setup",
"impl": "srv/auth.js"
},
"audit-log": {
"[development]": {
"kind": "audit-log-to-console"
},
"[production]": {
"kind": "audit-log-service"
}
},
"db": {
"kind": "sql"
},
"uaa": {
"kind": "xsuaa"
}
},
"features": {
"audit_personal_data": true,
"fiori_preview": true,
"[production]": {
"kibana_formatter": true
}
},
"hana": {
"deploy-format": "hdbtable"
}
}
}

View File

@@ -1,35 +0,0 @@
# how-to
## required services and subscriptions
services:
- Audit Log Service
- SAP HANA Cloud
- SAP HANA Schemas & HDI Containers
- Application Logging Service
- Personal Data Manager
- Authorization and Trust Management Service
subscriptions:
- Audit Log Viewer Service
- Personal Data Manager
## deploy
after adding the necessary entitlements, do:
- `cf l` to log into the respective account
- `cd gdpr` (if still in root of `cloud-cap-samples`)
- `npm run deploy`, which executes build and deployment via `.etc/deploy.sh`
## authorization
create roles for Audit Log Viewer Service and Personal Data Manager, and assign the roles to the respective users
# open issues
- deploy via mta, which can bind with parameters, and get rid of scripts in `.etc`
- use approuter to remove hacky custom auth impl (`srv/auth.js`)
- clarify annotation `EntitySemantics`, which differs between audit logging (`Other`) and personal data manager (`LegalGround`)
- annotations for order items Fiori preview app
+ `Products` has `@cds.persistence.skip:'always'`
- how to reuse intial data from `common`?

View File

@@ -1,20 +0,0 @@
---
create-services:
- name: gdpr-logs # > for kibana
broker: application-logs
plan: standard
- name: gdpr-hdi # > hana
broker: hana
plan: hdi-shared
- name: gdpr-auditlog # > audit log sink
broker: auditlog
plan: standard
# gdpr-pdm needs to exist before creating gdpr-uaa for authorization grant
- name: gdpr-pdm # > personal data manager
broker: personal-data-manager-service
plan: standard
parameters: ./.pdm/pdm-instance-config.json
- name: gdpr-uaa # > uaa for authentication
broker: xsuaa
plan: application
parameters: xs-security.json

View File

@@ -1,43 +0,0 @@
/*
* workaround to avoid approuter et al. setup
*/
const jwt = require('jsonwebtoken')
const tenant = process.env.VCAP_SERVICES
? JSON.parse(process.env.VCAP_SERVICES).xsuaa[0].credentials.tenantid
: 'anonymous'
module.exports = (req, res, next) => {
/*
* decode JWT coming from Personal Data Manager
*
* DO NOT USE FOR PRODUCTION!
* - no token validation
* - no xsappname check
*/
const bearer = req.headers.authorization && req.headers.authorization.split('Bearer ')[1]
if (bearer) {
const { client_id: id, zid: tenant, scope: roles } = jwt.decode(bearer)
req.user = {
id,
tenant,
is: role => roles.some(r => r.endsWith(`.${role}`))
}
return next()
}
// mock user that has every role EXCEPT PersonalDataManagerUser
const basic = req.headers.authorization && req.headers.authorization.split('Basic ')[1]
if (basic) {
const [id] = Buffer.from(basic, 'base64').toString('utf-8').split(':')
req.user = {
id,
tenant,
is: role => role !== 'PersonalDataManagerUser'
}
return next()
}
// no bearer & no basic -> 401
res.set('WWW-Authenticate', 'Basic realm="Users"').status(401).end()
}

View File

@@ -1,10 +0,0 @@
using {
sap.capire.orders,
sap.capire.gdpr
} from '../db/schema';
@requires : 'admin' // > authorization check
service GDPRService {
entity Customers as projection on gdpr.Customers;
entity Orders as projection on orders.Orders;
}

View File

@@ -1,24 +0,0 @@
using {
sap.capire.gdpr as gdpr,
sap.capire.orders as orders
} from '../db/data-privacy';
@requires : 'PersonalDataManagerUser' // > authorization check
service PDMService {
entity Customers as projection on gdpr.Customers;
entity CustomerPostalAddresses as projection on gdpr.CustomerPostalAddresses;
entity Orders as projection on orders.Orders;
/*
* additional annotations for Personal Data Manager's Search Fields
*/
annotate Customers with @(Communication.Contact : {
n : {
surname : lastName,
given : firstName
},
bday : dateOfBirth
});
};

View File

@@ -1,26 +0,0 @@
const cds = require('@sap/cds')
/*
* in development, write audit logs to custom sink (i.e., to console in this example)
*/
cds.on('served', async () => {
if (process.env.NODE_ENV === 'production') return
const auditLogService = await cds.connect.to('audit-log')
// use prepend to get called before the generic implementation
auditLogService.prepend(function() {
const LOG = cds.log('my custom audit logging impl')
// triggered when reading sensitive personal data
this.on('dataAccessLog', function(req) {
const { accesses } = req.data
for (const access of accesses) LOG.info(access)
})
// triggered when modifying personal data
this.on('dataModificationLog', function(req) {
const { modifications } = req.data
for (const modification of modifications) LOG.info(modification)
})
})
})
module.exports = cds.server

View File

@@ -1,14 +0,0 @@
{
"xsappname": "capire-gdpr",
"tenant-mode": "shared",
"scopes": [{
"name": "$XSAPPNAME.PersonalDataManagerUser",
"description": "Authority for Personal Data Manager",
"grant-as-authority-to-apps": [
"$XSSERVICENAME(gdpr-pdm)"
]
}, {
"name": "$XSAPPNAME.admin",
"description": "Administrator"
}]
}

View File

@@ -7,11 +7,11 @@
"start:ts": "cds-ts serve srv/world.cds" "start:ts": "cds-ts serve srv/world.cds"
}, },
"dependencies": { "dependencies": {
"@sap/cds": "^5.0.4" "@sap/cds": ">=5.0.4"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^27.0.2", "@types/jest": "*",
"@types/node": "^16.11.6", "@types/node": "*",
"ts-jest": "^27.0.2", "ts-jest": "^27.0.2",
"typescript": "^4.3.5" "typescript": "^4.3.5"
}, },

View File

@@ -27,7 +27,7 @@
<script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js" <script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout" data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout"
data-sap-ui-compatVersion="edge" data-sap-ui-compatVersion="edge"
data-sap-ui-theme="sap_fiori_3" data-sap-ui-theme="sap_horizon"
data-sap-ui-frameOptions="allow" data-sap-ui-frameOptions="allow"
></script> ></script>
<script> <script>

View File

@@ -3,6 +3,6 @@
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@capire/common": "*", "@capire/common": "*",
"@sap/cds": "^5" "@sap/cds": ">=5"
} }
} }

11533
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,25 +10,24 @@
"@capire/common": "./common", "@capire/common": "./common",
"@capire/data-viewer": "./data-viewer", "@capire/data-viewer": "./data-viewer",
"@capire/fiori": "./fiori", "@capire/fiori": "./fiori",
"@capire/gdpr": "./gdpr",
"@capire/hello": "./hello", "@capire/hello": "./hello",
"@capire/media": "./media", "@capire/media": "./media",
"@capire/orders": "./orders", "@capire/orders": "./orders",
"@capire/reviews": "./reviews", "@capire/reviews": "./reviews",
"@sap/cds": "^5.5.3" "@sap/cds": ">=5.5.3"
}, },
"devDependencies": { "devDependencies": {
"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": "npm:@mendix/sqlite3@^5" "semver": "^7",
"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",

View File

@@ -4,11 +4,10 @@ const GET = (url) => axios.get('/reviews'+url)
const PUT = (cmd,data) => axios.patch('/reviews'+cmd,data) const PUT = (cmd,data) => axios.patch('/reviews'+cmd,data)
const POST = (cmd,data) => axios.post('/reviews'+cmd,data) const POST = (cmd,data) => axios.post('/reviews'+cmd,data)
const reviews = new Vue ({ const reviews = Vue.createApp ({
el:'#app', data() {
return {
data: {
list: [], list: [],
review: undefined, review: undefined,
message: {}, message: {},
@@ -19,6 +18,7 @@ const reviews = new Vue ({
2 : '★★', 2 : '★★',
1 : '★', 1 : '★',
}).reverse() }).reverse()
}
}, },
methods: { methods: {
@@ -66,7 +66,7 @@ const reviews = new Vue ({
datetime: (d) => d && new Date(d).toLocaleString(), datetime: (d) => d && new Date(d).toLocaleString(),
}, },
}) }).mount("#app")
// initially fill list of my reviews // initially fill list of my reviews
reviews.fetch() reviews.fetch()

View File

@@ -5,7 +5,7 @@
<title> Capire Reviews </title> <title> Capire Reviews </title>
<link rel="stylesheet" href="https://unpkg.com/primitive-ui/dist/css/main.css"> <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/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue"></script> <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
<style> <style>
.hovering tr:hover td { color:cyan; background: #123; cursor: pointer; } .hovering tr:hover td { color:cyan; background: #123; cursor: pointer; }
.rating-stars { color:teal } .rating-stars { color:teal }
@@ -18,7 +18,7 @@
<body class="small-container", style="margin-top: 70px;"> <body class="small-container", style="margin-top: 70px;">
<div id='app'> <div id='app'>
<h1> {{ document.title }} </h1> <h1> Capire Reviews </h1>
<input type="text" placeholder="Search..." @input="search"> <input type="text" placeholder="Search..." @input="search">

View File

@@ -7,7 +7,7 @@
"index.cds" "index.cds"
], ],
"dependencies": { "dependencies": {
"@sap/cds": "^5", "@sap/cds": ">=5",
"express": "^4.17.1" "express": "^4.17.1"
}, },
"cds": { "cds": {

View File

@@ -81,6 +81,8 @@ describe('cds.ql → cqn', () => {
.to.eql(SELECT('Foo','Boo').from('Bar')) .to.eql(SELECT('Foo','Boo').from('Bar'))
.to.eql(SELECT(['Foo','Boo']).from('Bar')) .to.eql(SELECT(['Foo','Boo']).from('Bar'))
.to.eql(SELECT `Bar` .columns `Foo, Boo`) .to.eql(SELECT `Bar` .columns `Foo, Boo`)
.to.eql(SELECT `Bar` .columns `{ Foo, Boo }`)
.to.eql(SELECT `Bar` .columns ('{ Foo, Boo }'))
.to.eql(SELECT `Bar` .columns ('Foo','Boo')) .to.eql(SELECT `Bar` .columns ('Foo','Boo'))
.to.eql(SELECT `Bar` .columns (['Foo','Boo'])) .to.eql(SELECT `Bar` .columns (['Foo','Boo']))
.to.eql(SELECT.from `Bar` .columns ('Foo','Boo')) .to.eql(SELECT.from `Bar` .columns ('Foo','Boo'))

View File

@@ -1,7 +1,7 @@
const cds = require('@sap/cds/lib') const cds = require('@sap/cds/lib')
const { expect } = cds.test ('@capire/bookshop') const { expect } = cds.test ('@capire/bookshop')
describe('Consuming Services locally', () => { describe('cap/samples - Consuming Services locally', () => {
// //
it('bootstrapped the database successfully', ()=>{ it('bootstrapped the database successfully', ()=>{
const { AdminService } = cds.services const { AdminService } = cds.services
@@ -32,6 +32,27 @@ describe('Consuming Services locally', () => {
}) })
}) })
}).where(`name like`, 'E%') }).where(`name like`, 'E%')
if (require('semver').gte(cds.version, '5.9.0')) {
expect(authors).to.containSubset([
{
name: 'Emily Brontë',
books: [
{
title: 'Wuthering Heights',
currency: { name: 'British Pound', symbol: '£' },
},
],
},
{
name: 'Edgar Allen Poe',
books: [
{ title: 'The Raven', currency: { name: 'US Dollar', symbol: '$' } },
{ title: 'Eleonora', currency: { name: 'US Dollar', symbol: '$' } },
],
},
])
return
}
expect(authors).to.containSubset([ expect(authors).to.containSubset([
{ {
name: 'Emily Brontë', name: 'Emily Brontë',

View File

@@ -3,7 +3,7 @@ const { GET, POST, expect } = cds.test(__dirname+'/../bookshop')
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
describe('Custom Handlers', () => { describe('cap/samples - Custom Handlers', () => {
it('should reject out-of-stock orders', async () => { it('should reject out-of-stock orders', async () => {
await POST `/browse/submitOrder ${{ book: 201, quantity: 5 }}` await POST `/browse/submitOrder ${{ book: 201, quantity: 5 }}`

View File

@@ -1,7 +1,7 @@
const cds = require('@sap/cds/lib') const cds = require('@sap/cds/lib')
const { GET, expect } = cds.test (__dirname+'/../hello') const { GET, expect } = cds.test (__dirname+'/../hello')
describe('Hello world!', () => { describe('cap/samples - Hello world!', () => {
it('should say hello with class impl', async () => { it('should say hello with class impl', async () => {
const {data} = await GET `/say/hello(to='world')` const {data} = await GET `/say/hello(to='world')`

View File

@@ -13,7 +13,7 @@ const model = cds.compile.to.csn (`
const {Categories:Cats} = model.definitions const {Categories:Cats} = model.definitions
describe('Hierarchical Data', ()=>{ describe('cap/samples - Hierarchical Data', ()=>{
before ('bootstrap sqlite in-memory db...', async()=>{ before ('bootstrap sqlite in-memory db...', async()=>{
await cds.deploy (model) .to ('sqlite::memory:') await cds.deploy (model) .to ('sqlite::memory:')
@@ -35,6 +35,21 @@ describe('Hierarchical Data', ()=>{
)) ))
it ('supports nested reads', async()=>{ it ('supports nested reads', async()=>{
if (require('semver').gte(cds.version, '5.9.0')) {
expect (await
SELECT.one.from (Cats, c=>{
c.ID, c.name.as('parent'), c.children (c=>{
c.name.as('child')
})
}) .where ({name:'Cat'})
) .to.eql (
{ ID:101, parent:'Cat', children:[
{ child:'Kitty' },
{ child:'Catwoman' },
]}
)
return
}
expect (await expect (await
SELECT.one.from (Cats, c=>{ SELECT.one.from (Cats, c=>{
c.ID, c.name.as('parent'), c.children (c=>{ c.ID, c.name.as('parent'), c.children (c=>{
@@ -50,6 +65,25 @@ describe('Hierarchical Data', ()=>{
}) })
it ('supports deeply nested reads', async()=>{ it ('supports deeply nested reads', async()=>{
if (require('semver').gte(cds.version, '5.9.0')) {
expect (await SELECT.one.from (Cats, c=>{
c.ID, c.name, c.children (
c => { c.name },
{levels:3}
)
}) .where ({name:'Cat'})
) .to.eql (
{ ID:101, name:'Cat', children:[
{ name:'Kitty', children:[
{ name:'Kitty Cat', children:[
{ name:'Aristocat' }, ]}, // level 3
{ name:'Kitty Bat', children:[] }, ]},
{ name:'Catwoman', children:[
{ name:'Catalina', children:[] } ]},
]}
)
return
}
expect (await SELECT.one.from (Cats, c=>{ expect (await SELECT.one.from (Cats, c=>{
c.ID, c.name, c.children ( c.ID, c.name, c.children (
c => { c.name }, c => { c.name },

View File

@@ -2,7 +2,7 @@ const { GET, expect, cds } = require('@sap/cds/lib').test (__dirname)
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
describe('Localized Data', () => { describe('cap/samples - Localized Data', () => {
it('serves localized $metadata documents', async () => { it('serves localized $metadata documents', async () => {
const { data } = await GET`/browse/$metadata?sap-language=de` const { data } = await GET`/browse/$metadata?sap-language=de`

View File

@@ -4,7 +4,7 @@ const _model = '@capire/reviews'
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
describe('Messaging', ()=>{ describe('cap/samples - Messaging', ()=>{
it ('should bootstrap sqlite in-memory db', async()=>{ it ('should bootstrap sqlite in-memory db', async()=>{
const db = await cds.deploy (_model) .to ('sqlite::memory:') const db = await cds.deploy (_model) .to ('sqlite::memory:')

View File

@@ -1,9 +1,48 @@
const cds = require('@sap/cds/lib') const cds = require('@sap/cds/lib')
const { GET, expect } = cds.test ('@capire/bookshop') const { GET, expect, axios } = cds.test ('@capire/bookshop')
if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch axios.defaults.auth = { username: 'alice', password: 'admin' }
else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases
describe('OData Protocol', () => { describe('cap/samples - Bookshop APIs', () => {
// Genres
const Drama = {
"name": "Drama",
"descr": null,
"ID": 11,
"parent_ID": 10
}
const Mystery = {
"name": "Mystery",
"descr": null,
"ID": 16,
"parent_ID": 10
}
const Fantasy = {
"name": "Fantasy",
"descr": null,
"ID": 13,
"parent_ID": 10
}
// Currencies
const GBP = {
"name": "British Pound",
"descr": null,
"code": "GBP",
"symbol": "£"
}
const USD = {
"name": "US Dollar",
"descr": null,
"code": "USD",
"symbol": "$"
}
const JPY = {
"name": "Yen",
"descr": null,
"code": "JPY",
"symbol": "¥"
}
it('serves $metadata documents in v4', async () => { it('serves $metadata documents in v4', async () => {
@@ -17,6 +56,16 @@ describe('OData Protocol', () => {
expect(data).to.contain('<Annotation Term="Common.Label" String="Currency"/>') expect(data).to.contain('<Annotation Term="Common.Label" String="Currency"/>')
}) })
it('serves ListOfBooks?$expand=genre,currency', async () => {
const { data } = await GET `/browse/ListOfBooks ${{
params: { $search: 'Po', $select: `title,author`, $expand:`genre,currency` },
}}`
expect(data.value).to.eql([
{ ID: 251, title: 'The Raven', author: 'Edgar Allen Poe', genre:Mystery, currency:USD },
{ ID: 252, title: 'Eleonora', author: 'Edgar Allen Poe', genre:Mystery, currency:USD },
])
})
it('supports $search in multiple fields', async () => { it('supports $search in multiple fields', async () => {
const { data } = await GET `/browse/Books ${{ const { data } = await GET `/browse/Books ${{
params: { $search: 'Po', $select: `title,author` }, params: { $search: 'Po', $select: `title,author` },
@@ -75,4 +124,16 @@ describe('OData Protocol', () => {
{ ID: 271, title: 'Catweazle' }, { ID: 271, title: 'Catweazle' },
]) ])
}) })
it('serves user info', async () => {
{
const { data } = await GET (`/user/me`)
expect(data).to.containSubset({ id: 'alice', locale:'en', tenant: null })
}
{
const { data } = await GET (`/user/me`, {auth: { username: 'joe' }})
expect(data).to.containSubset({ id: 'joe', locale:'en', tenant: null })
}
})
}) })

View File

@@ -6,7 +6,7 @@ const { resolve } = require('path')
const verbose = process.env.CDS_TEST_VERBOSE const verbose = process.env.CDS_TEST_VERBOSE
// ||true // ||true
describe('Local NPM registry', () => { describe('cap/samples - Local NPM registry', () => {
let registry let registry
let axios let axios
const cwd = resolve(__dirname, '..') const cwd = resolve(__dirname, '..')
@@ -20,7 +20,7 @@ describe('Local NPM registry', () => {
after(() => { registry.kill() }) after(() => { registry.kill() })
for (const mod of ['bookshop','fiori','orders','reviews']) { for (const mod of ['bookshop', 'data-viewer', '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).to.containSubset({name: `@capire/${mod}`, versions:{}}) expect(resp.data).to.containSubset({name: `@capire/${mod}`, versions:{}})