Compare commits
240 Commits
temporal
...
sandbox_vm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f1af693c9 | ||
|
|
d2ab511d6d | ||
|
|
6c27228d62 | ||
|
|
44880c7745 | ||
|
|
9c2a7598f2 | ||
|
|
9617e576f0 | ||
|
|
c3c9dae80d | ||
|
|
2b6d4c625e | ||
|
|
7d46db42ec | ||
|
|
df57a9d8d0 | ||
|
|
2d92b851f1 | ||
|
|
e157fc8a92 | ||
|
|
3b69a80975 | ||
|
|
b80ba7445d | ||
|
|
b733643f2a | ||
|
|
bf317bc2c9 | ||
|
|
bbebff4066 | ||
|
|
eb75394044 | ||
|
|
b8f65b687f | ||
|
|
c49c9691bd | ||
|
|
7e4bc0985e | ||
|
|
912271338d | ||
|
|
bbf1194a09 | ||
|
|
529c431518 | ||
|
|
3f85676edd | ||
|
|
959c07cee3 | ||
|
|
8cc09fde38 | ||
|
|
bb55c432c9 | ||
|
|
5fc86d45ad | ||
|
|
e86d0dc3a2 | ||
|
|
a661fb2d45 | ||
|
|
bfe0c831d6 | ||
|
|
cad615a662 | ||
|
|
7101c58920 | ||
|
|
07dc1e88b3 | ||
|
|
6f8d74dc9a | ||
|
|
ec57c5ea48 | ||
|
|
c040a47279 | ||
|
|
30bfd70c49 | ||
|
|
6fb9581cf1 | ||
|
|
e87d6cdfc5 | ||
|
|
29ea2bc2da | ||
|
|
12574271ac | ||
|
|
17b5cc1ad2 | ||
|
|
26ca7f54ad | ||
|
|
573e78253d | ||
|
|
a7511ed677 | ||
|
|
f1289b436b | ||
|
|
984ea2133b | ||
|
|
179d301acb | ||
|
|
bbf20b1ca3 | ||
|
|
f4f02f6fe5 | ||
|
|
68c2504fc4 | ||
|
|
51a8762738 | ||
|
|
ff533754b4 | ||
|
|
f6b4d534fc | ||
|
|
3c5107ba42 | ||
|
|
dbc561d2e3 | ||
|
|
f21488fd71 | ||
|
|
0bce8d9ce0 | ||
|
|
0f03ffd3eb | ||
|
|
bc03cae550 | ||
|
|
8197559065 | ||
|
|
7accf1ae23 | ||
|
|
2ffa1721b3 | ||
|
|
37811381e7 | ||
|
|
74f9d5cc1e | ||
|
|
7c0769e059 | ||
|
|
10dcd6aadd | ||
|
|
f8c23f4c54 | ||
|
|
ae6be30aa0 | ||
|
|
e6baa95f74 | ||
|
|
aa28804a51 | ||
|
|
ea0773071e | ||
|
|
1a71a6d28a | ||
|
|
50791bed80 | ||
|
|
d58a71af7f | ||
|
|
c7c979236e | ||
|
|
0675793c87 | ||
|
|
91bca922c4 | ||
|
|
30403d239a | ||
|
|
589e282288 | ||
|
|
a8345122ea | ||
|
|
788a101f41 | ||
|
|
379ddc9fb0 | ||
|
|
1fde0c928b | ||
|
|
fa854a2411 | ||
|
|
40b6acee6a | ||
|
|
a37f1b8e0c | ||
|
|
042ddec41c | ||
|
|
8d8aa6c2c9 | ||
|
|
756ed48ce3 | ||
|
|
19b680ab3d | ||
|
|
33380c0792 | ||
|
|
0d5b65d93f | ||
|
|
dcf5ec6068 | ||
|
|
fb41dd100b | ||
|
|
769aa2b4f5 | ||
|
|
f6c5e620c0 | ||
|
|
7013cb6296 | ||
|
|
6411ce4ea3 | ||
|
|
cd914a8d87 | ||
|
|
b35e92b5bc | ||
|
|
8350aff111 | ||
|
|
9aa604c5a0 | ||
|
|
f5cb53adf8 | ||
|
|
9fe16edc55 | ||
|
|
0093883f25 | ||
|
|
14024a4837 | ||
|
|
2e13b37096 | ||
|
|
7407b0aae3 | ||
|
|
152d088ff4 | ||
|
|
19c886d6dc | ||
|
|
575cffe70a | ||
|
|
03869e5896 | ||
|
|
597afeb3d8 | ||
|
|
d376e6e063 | ||
|
|
2a44cfa9dc | ||
|
|
7964c9aefd | ||
|
|
ce752097d0 | ||
|
|
e838f380c5 | ||
|
|
e89e012eba | ||
|
|
bf0f57efe5 | ||
|
|
40ea057f7f | ||
|
|
31d2f4c932 | ||
|
|
8cc2db7118 | ||
|
|
482b71e60b | ||
|
|
7536451e29 | ||
|
|
2703e8b999 | ||
|
|
ac2e4dd6f9 | ||
|
|
c718fa400d | ||
|
|
730da6e435 | ||
|
|
7d9b4f7a5d | ||
|
|
35d3708592 | ||
|
|
eaaf0d29a5 | ||
|
|
64fe700d1e | ||
|
|
5c3cec973e | ||
|
|
680a6ae68f | ||
|
|
366b0f8f9a | ||
|
|
b95df77b9a | ||
|
|
7f6b87171a | ||
|
|
21e74bbbfb | ||
|
|
102b15c3cd | ||
|
|
73db2e96bc | ||
|
|
7bb58ee2d5 | ||
|
|
27e82d16e0 | ||
|
|
ff9bbe6d8d | ||
|
|
5f1b7b8cbf | ||
|
|
0253300557 | ||
|
|
139d957495 | ||
|
|
404427237b | ||
|
|
5f89334403 | ||
|
|
e612fa97ea | ||
|
|
da2ea39466 | ||
|
|
117000df71 | ||
|
|
f3ffb69d3a | ||
|
|
f908484973 | ||
|
|
c4529f3cd7 | ||
|
|
0220400484 | ||
|
|
c1911b6e96 | ||
|
|
19083d156e | ||
|
|
48d547e6cd | ||
|
|
bae491a832 | ||
|
|
ca41a2141c | ||
|
|
08f409af73 | ||
|
|
efa60550fb | ||
|
|
f599206bf4 | ||
|
|
2f5d159428 | ||
|
|
2be3d50389 | ||
|
|
46b58f1b5c | ||
|
|
e87527cbcd | ||
|
|
091844219b | ||
|
|
de796e5a89 | ||
|
|
f988088412 | ||
|
|
c4a51ab719 | ||
|
|
f58376607a | ||
|
|
839048d87c | ||
|
|
3b3463f889 | ||
|
|
d3396304ec | ||
|
|
ae09caf7ad | ||
|
|
e1052c209b | ||
|
|
cd3aad59e1 | ||
|
|
bcbf65f95e | ||
|
|
1189374860 | ||
|
|
9f2eb8fa83 | ||
|
|
7b18e15e5b | ||
|
|
00bfc1507e | ||
|
|
47a58e7393 | ||
|
|
c04b972d27 | ||
|
|
85b9b9d3a3 | ||
|
|
3655eef971 | ||
|
|
87dd356204 | ||
|
|
a89ec34f7c | ||
|
|
444dcacb14 | ||
|
|
2011a1ca7a | ||
|
|
7e0c36ede5 | ||
|
|
e8d08d039e | ||
|
|
5e3258913e | ||
|
|
44e14926e4 | ||
|
|
592e945c28 | ||
|
|
5d5c787365 | ||
|
|
bb79def1d9 | ||
|
|
bd4a38bf54 | ||
|
|
abf18d74db | ||
|
|
f144354229 | ||
|
|
97670bbc84 | ||
|
|
82d6f54337 | ||
|
|
a4baee6e3b | ||
|
|
95b01e7d7a | ||
|
|
c2437fc419 | ||
|
|
9381459458 | ||
|
|
28a7c16895 | ||
|
|
c1141e6c87 | ||
|
|
6e686c6c8b | ||
|
|
1ccddb2155 | ||
|
|
7c8e52d5e9 | ||
|
|
0dc1dc198d | ||
|
|
20bfb5c5f7 | ||
|
|
be87f5617a | ||
|
|
3117df1282 | ||
|
|
be685437aa | ||
|
|
ff3801be71 | ||
|
|
702c3245bd | ||
|
|
14002300c5 | ||
|
|
ad417ec061 | ||
|
|
a6ec296129 | ||
|
|
1b10bbbfe4 | ||
|
|
aaa1b2d6c7 | ||
|
|
1cb169e886 | ||
|
|
9b9bb4c114 | ||
|
|
2536f36596 | ||
|
|
5e829ae7bb | ||
|
|
d265a385f8 | ||
|
|
80f469b5b6 | ||
|
|
8e8ae949df | ||
|
|
3d24dc491d | ||
|
|
f593265687 | ||
|
|
6d488b042c | ||
|
|
d5ef630743 | ||
|
|
b47c9d75df |
@@ -8,7 +8,7 @@
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"globals": {
|
||||
"SELECT": true,
|
||||
@@ -22,6 +22,7 @@
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"require-atomic-updates": "off",
|
||||
"require-await":"warn"
|
||||
"require-await":"warn",
|
||||
"no-unused-vars": ["warn", { "argsIgnorePattern": "_" }]
|
||||
}
|
||||
}
|
||||
|
||||
8
.github/dependabot.yml
vendored
Normal file
8
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
versioning-strategy: increase-if-necessary
|
||||
schedule:
|
||||
interval: daily
|
||||
8
.github/workflows/node.js.yml
vendored
8
.github/workflows/node.js.yml
vendored
@@ -5,9 +5,9 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x, 14.x]
|
||||
node-version: [16.x, 14.x, 12.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -24,5 +24,5 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,3 +15,5 @@ default-env.json
|
||||
packages/messageBox
|
||||
reviews/msg-box
|
||||
reviews/db/test.db
|
||||
|
||||
*.openapi3.json
|
||||
|
||||
3
.npmrc
Normal file
3
.npmrc
Normal file
@@ -0,0 +1,3 @@
|
||||
# Ensure we always use public packages, i.e. avoid using local registries from ~/.npmrc
|
||||
@sap:registry=https://registry.npmjs.org/
|
||||
registry=https://registry.npmjs.org/
|
||||
@@ -1,19 +1,26 @@
|
||||
const { exec } = require ('child_process')
|
||||
const isWin = process.platform === 'win32'
|
||||
const express = require ('express')
|
||||
const fs = require ('fs')
|
||||
const app = express()
|
||||
|
||||
const { PORT=4444 } = process.env
|
||||
const [,,port=PORT] = process.argv
|
||||
process.chdir(__dirname)
|
||||
const [,,port=PORT,scope='@capire'] = process.argv
|
||||
const cwd = __dirname
|
||||
|
||||
// clean up on start (exit handler might not complete on Windows)
|
||||
exec(isWin ? 'del *.tgz' : 'rm *.tgz', {cwd})
|
||||
|
||||
|
||||
app.use('/-/:tarball', (req,res,next) => {
|
||||
console.debug ('GET', req.params)
|
||||
try {
|
||||
const { tarball } = req.params
|
||||
const [, pkg ] = /^capire-(\w+)/.exec(tarball)
|
||||
const pkgFull = tarball.substring(0, tarball.lastIndexOf('-'))
|
||||
const [, pkg ] = /^\w+-(.+)/.exec(pkgFull)
|
||||
fs.lstat(tarball,(err => {
|
||||
if (err) exec(`npm pack ../${pkg}`,next)
|
||||
if (err) console.debug (`npm pack ../${pkg}`)
|
||||
if (err) exec(`npm pack ../${pkg}`,{cwd},next)
|
||||
else next()
|
||||
}))
|
||||
} catch (e) {
|
||||
@@ -25,12 +32,14 @@ app.use('/-/:tarball', (req,res,next) => {
|
||||
app.use('/-', express.static(__dirname))
|
||||
|
||||
app.get('/*', (req,res)=>{
|
||||
const urlRegex = /^\/(@[\w-]+)\/(.+)/
|
||||
const url = decodeURIComponent(req.url)
|
||||
console.debug ('GET',url)
|
||||
try {
|
||||
const [, capire, pkg ] = /^\/(@capire)\/(\w+)/.exec(url)
|
||||
const package = require (`${capire}/${pkg}/package.json`)
|
||||
const tarball = `capire-${pkg}-${package.version}.tgz`
|
||||
if (!urlRegex.test(url)) return res.sendStatus(404)
|
||||
const [, scpe, pkg ] = urlRegex.exec(url)
|
||||
const package = require (`${scpe}/${pkg}/package.json`)
|
||||
const tarball = `${scpe.slice(1)}-${pkg}-${package.version}.tgz`
|
||||
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
|
||||
res.json({
|
||||
"name": package.name,
|
||||
@@ -42,29 +51,30 @@ app.get('/*', (req,res)=>{
|
||||
"name": package.name,
|
||||
"version": package.version,
|
||||
"dist": {
|
||||
"tarball": `http://localhost:${port}/-/${tarball}`
|
||||
"tarball": `${server.url}/-/${tarball}`
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
res.sendStatus(404)
|
||||
if (e.code === 'MODULE_NOT_FOUND') return res.sendStatus(404)
|
||||
console.error(e); throw e
|
||||
}
|
||||
})
|
||||
|
||||
app.listen(port, ()=>{
|
||||
console.log (`npm set @capire:registry=http://localhost:${port}`)
|
||||
console.log (`@capire registry listening on http://localhost:${port}`)
|
||||
exec(`npm set @capire:registry=http://localhost:${port}`)
|
||||
const server = app.listen(port, ()=>{
|
||||
const url = server.url = `http://localhost:${server.address().port}`
|
||||
console.log (`npm set ${scope}:registry=${url}`)
|
||||
exec(`npm set ${scope}:registry=${url}`)
|
||||
console.log (`${scope} registry listening on ${url}`)
|
||||
})
|
||||
|
||||
|
||||
const _exit = ()=>{
|
||||
console.log ('\nnpm conf rm @capire:registry')
|
||||
exec('npm conf rm @capire:registry')
|
||||
exec('rm *.tgz')
|
||||
process.exit()
|
||||
server.close()
|
||||
exec(`npm conf rm "${scope}:registry"`, ()=> { process.exit() })
|
||||
}
|
||||
|
||||
process.on ('SIGTERM',_exit)
|
||||
process.on ('SIGHUP',_exit)
|
||||
process.on ('SIGINT',_exit)
|
||||
|
||||
@@ -68,26 +68,35 @@
|
||||
},
|
||||
{
|
||||
"file": "fiori/package.json",
|
||||
"description": "#### Configuration\n\nThe `cds` section in `package.json` is a place to configure which of the `db/sqlite` and `db/hana` folders are used for which database.\nWe use [Node.js profiles](https://cap.cloud.sap/docs/node.js/cds-env#profiles) to separate the configuration.\nIn the `development` profile, you can see that `db/sqlite` is set as the model, while the `db/hana` folder is configured in the `production` profile.",
|
||||
"line": 17,
|
||||
"description": "#### Configuration\n\nThe `cds.requires` section in `package.json` is a place to configure which of the `db/sqlite` and `db/hana` folders are used for which database.\n\nWe use [Node.js profiles](https://cap.cloud.sap/docs/node.js/cds-env#profiles) to separate the configuration.\nIn the `development` profile, you can see that `db/sqlite` is set as the model, while the `db/hana` folder is configured in the `production` profile. `db-ext` is a pseudo datasource, its name doesn't matter.\n\nSee [`cds.resolve`](https://cap.cloud.sap/docs/node.js/cds-compile#cds-resolve) to learn more about how models are found.",
|
||||
"selection": {
|
||||
"start": {
|
||||
"line": 41,
|
||||
"character": 1
|
||||
},
|
||||
"end": {
|
||||
"line": 48,
|
||||
"character": 1
|
||||
}
|
||||
},
|
||||
"title": "Configuration"
|
||||
},
|
||||
{
|
||||
"file": "fiori/package.json",
|
||||
"description": "#### Run with SQLite\n\nTo run with `development` and an in-memory SQLite database, you don't need to do anything special, because it's activated by default. Just run:\n\n>> cds watch fiori\n\nThen open [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) to see the two new fields.\n",
|
||||
"line": 28,
|
||||
"line": 43,
|
||||
"title": "Run with SQLite"
|
||||
},
|
||||
{
|
||||
"file": "fiori/package.json",
|
||||
"description": "#### Deploy the CDS Model to SAP HANA\n\nTo 'activate' SAP HANA through the `production` profile, you can use the global `--production` flag:\n\n>> cd fiori; cds deploy --to hana --production\n\n[Learn more about SAP HANA deployment](https://cap.cloud.sap/docs/guides/databases#get-hana)\n\n#### Run the Application\n\n>> cd fiori; cds watch --production\n\nThe service on [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) is the same as before, but this time the `Authors` entity is backed by a database view with an SAP HANA function.\n\n#### More\n\nIf you don't see data, you can add some in the next step.",
|
||||
"line": 31,
|
||||
"line": 46,
|
||||
"title": "Run with SAP HANA"
|
||||
},
|
||||
{
|
||||
"file": "fiori/test/requests.http",
|
||||
"description": "### Add More Data\n\nOptionally you can add some `Authors` data by clicking on the _Send Request_ link (provided by the [REST client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension).",
|
||||
"line": 68,
|
||||
"line": 72,
|
||||
"selection": {
|
||||
"start": {
|
||||
"line": 67,
|
||||
@@ -104,6 +113,5 @@
|
||||
"title": "Wrap-up",
|
||||
"description": "### Summary\n\nThat's it! You have seen: \n- How to integrate database-specific functions in a CDS model.\n- How to switch between the two implementations for SQLite and SAP HANA."
|
||||
}
|
||||
],
|
||||
"ref": "master"
|
||||
]
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "hello/world.cds",
|
||||
"file": "hello/srv/world.cds",
|
||||
"description": "### Hello World!\n\nThis is a simplistic [Hello World](https://cap.cloud.sap/docs/get-started/hello-world) service using [CDS](https://cap.cloud.sap/docs/cds/) and [cds.services](https://cap.cloud.sap/docs/node.js/api#services-api).",
|
||||
"line": 2,
|
||||
"selection": {
|
||||
@@ -68,7 +68,7 @@
|
||||
},
|
||||
{
|
||||
"file": "orders/db/schema.cds",
|
||||
"description": "### Compositions and Serving Documents\n\nA standalone orders management service, demonstrating:\n- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with\n- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)\n",
|
||||
"description": "### Orders - Compositions and Serving Documents\n\nA standalone orders management service, demonstrating:\n- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with\n- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)\n",
|
||||
"line": 1,
|
||||
"selection": {
|
||||
"start": {
|
||||
@@ -84,7 +84,7 @@
|
||||
},
|
||||
{
|
||||
"file": "reviews/db/schema.cds",
|
||||
"description": "### More Modularity\n\nShows how to implement a modular service to manage product reviews, including:\n- Consuming other services synchronously and asynchronously\n- Serving requests synchronously\n- Emitting events asynchronously\n- Grow as you go, with:\n- Mocking app services\n- Running service meshes\n- Late-cut Micro Services\n- As well as managed data, input validations, and authorization\n",
|
||||
"description": "### Reviews - More Modularity\n\nShows how to implement a modular service to manage product reviews, including:\n- Consuming other services synchronously and asynchronously\n- Serving requests synchronously\n- Emitting events asynchronously\n- Grow as you go, with:\n- Mocking app services\n- Running service meshes\n- Late-cut Micro Services\n- As well as managed data, input validations, and authorization\n",
|
||||
"line": 1,
|
||||
"selection": {
|
||||
"start": {
|
||||
@@ -99,8 +99,12 @@
|
||||
"title": "Reviews"
|
||||
},
|
||||
{
|
||||
"file": "fiori/app/index.cds",
|
||||
"description": "### Annotations for SAP Fiori Elements\n\nA [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:\n - [@capire/bookshop](bookshop)\n - [@capire/reviews](reviews)\n - [@capire/orders](orders)\n - [@capire/common](common)\n\n[Adds a SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookshop, thereby introducing to:\n - [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files\n - Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)\n - Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)\n - Serving SAP Fiori apps locally\n\n[The Vue.js app](bookshop/app/vue) imported from bookshop is served as well.\n",
|
||||
"title": "Bookstore",
|
||||
"description": "### Bookstore - Reuse and UI\n\n- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/reuse-and-compose) these packages:\n - [@capire/bookshop](bookshop)\n - [@capire/reviews](reviews)\n - [@capire/orders](orders)\n - [@capire/common](common)\n- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well\n- [The Vue.js app](reviews/app/vue) imported from reviews is served as well\n- [The Fiori app](orders/app) imported from orders is served as well\n- [OpenAPI export + Swagger UI](https://cap.cloud.sap/docs/advanced/openapi)"
|
||||
},
|
||||
{
|
||||
"file": "fiori/app/services.cds",
|
||||
"description": "### Annotations for SAP Fiori Elements\n\n- [Adds an SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookstore, thereby introducing to:\n- [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files\n- Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)\n- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)\n- Serving SAP Fiori apps locally\n",
|
||||
"line": 1,
|
||||
"selection": {
|
||||
"start": {
|
||||
@@ -117,14 +121,13 @@
|
||||
{
|
||||
"file": "package.json",
|
||||
"description": "### All-in-one Monorepo\n\nEach sample sub directory essentially is a standard npm package, some with standard npm dependencies to other samples. The root folder's [package.json](package.json) has local links to the sub folders, such that an `npm install` populates a local `node_modules` folder acts like a local npm registry to the individual sample packages.\n",
|
||||
"line": 8,
|
||||
"selection": {
|
||||
"start": {
|
||||
"line": 8,
|
||||
"character": 1
|
||||
},
|
||||
"end": {
|
||||
"line": 15,
|
||||
"line": 16,
|
||||
"character": 1
|
||||
}
|
||||
},
|
||||
|
||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -14,4 +14,5 @@
|
||||
"**/odata-v4/okra/**"
|
||||
]
|
||||
},
|
||||
"mochaExplorer.parallel": true
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ Find here a collection of samples for the [SAP Cloud Application Programming Mod
|
||||
|
||||
### Download
|
||||
|
||||
If you've [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, otherwise [download as ZIP file](archive/master.zip).
|
||||
If you've [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, otherwise [download as ZIP file](archive/main.zip).
|
||||
|
||||
```sh
|
||||
git clone https://github.com/sap-samples/cloud-cap-samples samples
|
||||
@@ -83,4 +83,4 @@ In case you've a question, find a bug, or otherwise need support, use our [commu
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2021 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](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';
|
||||
@@ -3,14 +3,15 @@ const $ = sel => document.querySelector(sel)
|
||||
const GET = (url) => axios.get('/browse'+url)
|
||||
const POST = (cmd,data) => axios.post('/browse'+cmd,data)
|
||||
|
||||
const books = new Vue ({
|
||||
const books = Vue.createApp ({
|
||||
|
||||
el:'#app',
|
||||
|
||||
data: {
|
||||
data() {
|
||||
return {
|
||||
list: [],
|
||||
book: undefined,
|
||||
order: { amount:1, succeeded:'', failed:'' }
|
||||
order: { quantity:1, succeeded:'', failed:'' },
|
||||
user: {}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -26,23 +27,35 @@ const books = new Vue ({
|
||||
const book = books.book = books.list [eve.currentTarget.rowIndex-1]
|
||||
const res = await GET(`/Books/${book.ID}?$select=descr,stock,image`)
|
||||
Object.assign (book, res.data)
|
||||
books.order = { amount:1 }
|
||||
books.order = { quantity:1 }
|
||||
setTimeout (()=> $('form > input').focus(), 111)
|
||||
},
|
||||
|
||||
async submitOrder () {
|
||||
const {book,order} = books, amount = parseInt (order.amount) || 1 // REVISIT: Okra should be less strict
|
||||
const {book,order} = books, quantity = parseInt (order.quantity) || 1 // REVISIT: Okra should be less strict
|
||||
try {
|
||||
const res = await POST(`/submitOrder`, { amount, book: book.ID })
|
||||
const res = await POST(`/submitOrder`, { quantity, book: book.ID })
|
||||
book.stock = res.data.stock
|
||||
books.order = { amount, succeeded: `Successfully orderd ${amount} item(s).` }
|
||||
books.order = { quantity, succeeded: `Successfully ordered ${quantity} item(s).` }
|
||||
} catch (e) {
|
||||
books.order = { amount, failed: e.response.data.error.message }
|
||||
books.order = { quantity, failed: e.response.data.error ? e.response.data.error.message : e.response.data }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async fetchUserInfo() {
|
||||
try {
|
||||
const { data } = await axios.get('/user/me')
|
||||
books.user = data
|
||||
} catch (err) { books.user = { id: err.message } }
|
||||
}
|
||||
}
|
||||
})
|
||||
}).mount("#app")
|
||||
|
||||
// initially fill list of books
|
||||
books.fetch()
|
||||
|
||||
books.fetchUserInfo()
|
||||
document.addEventListener('keydown', (event) => {
|
||||
// hide user info on request
|
||||
if (event.key === 'u') books.user = undefined
|
||||
})
|
||||
|
||||
@@ -5,19 +5,26 @@
|
||||
<title> Capire Books </title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/primitive-ui/dist/css/main.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
|
||||
<style>
|
||||
.hovering tr:hover td { color:cyan; background: #123; cursor: pointer; }
|
||||
.rating-stars { color:teal }
|
||||
.succeeded { color:teal }
|
||||
.failed { color:red }
|
||||
.user {text-align: end; color: grey;}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="small-container", style="margin-top: 70px;">
|
||||
<div id='app'>
|
||||
|
||||
<h1> {{ document.title }} </h1>
|
||||
<div v-if="user" class="user">
|
||||
<div>User: {{ user.id || 'anonymous' }}</div>
|
||||
<div>Locale: {{ user.locale }}</div>
|
||||
<div v-if="user.tenant">Tenant: {{ user.tenant }}</div>
|
||||
</div>
|
||||
|
||||
<h1> Capire Books </h1>
|
||||
|
||||
<input type="text" placeholder="Search..." @input="search">
|
||||
|
||||
@@ -34,7 +41,7 @@
|
||||
<td>{{ book.author }}</td>
|
||||
<td>{{ book.genre.name }}</td>
|
||||
<td class="rating-stars">
|
||||
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}
|
||||
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }} ({{ book.numberOfReviews }})
|
||||
</td>
|
||||
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
|
||||
</tr>
|
||||
@@ -48,7 +55,7 @@
|
||||
{{ book.stock }} in stock
|
||||
</label>
|
||||
<form @submit.prevent="submitOrder" style="float:right; display:flex; flex-direction:row-reverse">
|
||||
<input type="number" v-model="order.amount" v-bind:class="{ failed: order.failed }" style="width:5em">
|
||||
<input type="number" v-model="order.quantity" v-bind:class="{ failed: order.failed }" style="width:5em">
|
||||
<input type="submit" value="Order:" class="muted-button">
|
||||
</form>
|
||||
<h4> {{ book.title }} </h4>
|
||||
|
||||
@@ -3,4 +3,4 @@ ID;title;descr;author_ID;stock;price;currency_code;genre_ID
|
||||
207;Jane Eyre;"Jane Eyre /ɛər/ (originally published as Jane Eyre: An Autobiography) is a novel by English writer Charlotte Brontë, published under the pen name ""Currer Bell"", on 16 October 1847, by Smith, Elder & Co. of London. The first American edition was published the following year by Harper & Brothers of New York. Primarily a bildungsroman, Jane Eyre follows the experiences of its eponymous heroine, including her growth to adulthood and her love for Mr. Rochester, the brooding master of Thornfield Hall. The novel revolutionised prose fiction in that the focus on Jane's moral and spiritual development is told through an intimate, first-person narrative, where actions and events are coloured by a psychological intensity. The book contains elements of social criticism, with a strong sense of Christian morality at its core and is considered by many to be ahead of its time because of Jane's individualistic character and how the novel approaches the topics of class, sexuality, religion and feminism.";107;11;12.34;GBP;11
|
||||
251;The Raven;"""The Raven"" is a narrative poem by American writer Edgar Allan Poe. First published in January 1845, the poem is often noted for its musicality, stylized language, and supernatural atmosphere. It tells of a talking raven's mysterious visit to a distraught lover, tracing the man's slow fall into madness. The lover, often identified as being a student, is lamenting the loss of his love, Lenore. Sitting on a bust of Pallas, the raven seems to further distress the protagonist with its constant repetition of the word ""Nevermore"". The poem makes use of folk, mythological, religious, and classical references.";150;333;13.13;USD;16
|
||||
252;Eleonora;"""Eleonora"" is a short story by Edgar Allan Poe, first published in 1842 in Philadelphia in the literary annual The Gift. It is often regarded as somewhat autobiographical and has a relatively ""happy"" ending.";150;555;14;USD;16
|
||||
271;Catweazle;Catweazle is a British fantasy television series, starring Geoffrey Bayldon in the title role, and created by Richard Carpenter for London Weekend Television. The first series, produced and directed by Quentin Lawrence, was screened in the UK on ITV in 1970. The second series, directed by David Reid and David Lane, was shown in 1971. Each series had thirteen episodes, most but not all written by Carpenter, who also published two books based on the scripts.;170;22;15;EUR;13
|
||||
271;Catweazle;Catweazle is a British fantasy television series, starring Geoffrey Bayldon in the title role, and created by Richard Carpenter for London Weekend Television. The first series, produced and directed by Quentin Lawrence, was screened in the UK on ITV in 1970. The second series, directed by David Reid and David Lane, was shown in 1971. Each series had thirteen episodes, most but not all written by Carpenter, who also published two books based on the scripts.;170;22;150;JPY;13
|
||||
|
2
bookshop/db/handlers/sandbox.cds
Normal file
2
bookshop/db/handlers/sandbox.cds
Normal file
@@ -0,0 +1,2 @@
|
||||
using from '..\..\schema';
|
||||
|
||||
24
bookshop/db/init.js
Normal file
24
bookshop/db/init.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* In order to keep basic bookshop sample as simple as possible, we don't add
|
||||
* reuse dependencies. This db/init.js ensures we still have a minimum set of
|
||||
* currencies, if not obtained through @capire/common.
|
||||
*/
|
||||
|
||||
module.exports = async (db)=>{
|
||||
|
||||
const has_common = db.model.definitions['sap.common.Currencies'].elements.numcode
|
||||
if (has_common) return
|
||||
|
||||
const already_filled = await db.exists('sap.common.Currencies',{code:'EUR'})
|
||||
if (already_filled) return
|
||||
|
||||
await INSERT.into ('sap.common.Currencies') .columns (
|
||||
'code','symbol','name'
|
||||
) .rows (
|
||||
[ 'EUR','€','Euro' ],
|
||||
[ 'USD','$','US Dollar' ],
|
||||
[ 'GBP','£','British Pound' ],
|
||||
[ 'ILS','₪','Shekel' ],
|
||||
[ 'JPY','¥','Yen' ],
|
||||
)
|
||||
}
|
||||
@@ -1,31 +1,56 @@
|
||||
using { Currency, managed, sap } from '@sap/cds/common';
|
||||
using {
|
||||
Currency,
|
||||
managed,
|
||||
sap,
|
||||
extensible
|
||||
} from '@sap/cds/common';
|
||||
|
||||
namespace sap.capire.bookshop;
|
||||
|
||||
entity Books : managed {
|
||||
key ID : Integer;
|
||||
title : localized String(111);
|
||||
descr : localized String(1111);
|
||||
author : Association to Authors;
|
||||
genre : Association to Genres;
|
||||
stock : Integer;
|
||||
price : Decimal;
|
||||
currency : Currency;
|
||||
image : LargeBinary @Core.MediaType : 'image/png';
|
||||
@Extensibility.Any.Enabled : true
|
||||
entity Books : managed, extensible {
|
||||
key ID : Integer;
|
||||
title : localized String(111);
|
||||
descr : localized String(1111);
|
||||
author : Association to Authors;
|
||||
genre : Association to Genres;
|
||||
stock : Integer;
|
||||
price : Decimal;
|
||||
currency : Currency;
|
||||
image : LargeBinary @Core.MediaType : 'image/png';
|
||||
authorName : String;
|
||||
}
|
||||
|
||||
entity Authors : managed {
|
||||
key ID : Integer;
|
||||
name : String(111);
|
||||
dateOfBirth : Date;
|
||||
dateOfDeath : Date;
|
||||
placeOfBirth : String;
|
||||
placeOfDeath : String;
|
||||
books : Association to many Books on books.author = $self;
|
||||
|
||||
entity Authors : managed, extensible {
|
||||
key ID : Integer;
|
||||
name : String(111);
|
||||
dateOfBirth : Date;
|
||||
dateOfDeath : Date;
|
||||
placeOfBirth : String;
|
||||
placeOfDeath : String;
|
||||
|
||||
books : Association to many Books
|
||||
on books.author = $self;
|
||||
}
|
||||
|
||||
/** Hierarchically organized Code List for Genres */
|
||||
extend Authors with {
|
||||
virtual age : Integer;
|
||||
virtual exampleBook: String;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hierarchically organized Code List for Genres
|
||||
*/
|
||||
entity Genres : sap.common.CodeList {
|
||||
key ID : Integer;
|
||||
parent : Association to Genres;
|
||||
children : Composition of many Genres on children.parent = $self;
|
||||
key ID : Integer;
|
||||
parent : Association to Genres;
|
||||
children : Composition of many Genres
|
||||
on children.parent = $self;
|
||||
}
|
||||
|
||||
entity Publishers: managed {
|
||||
key ID: Integer;
|
||||
name: String(111);
|
||||
|
||||
}
|
||||
17
bookshop/handlers/AdminService.Authors.CREATE.js
Normal file
17
bookshop/handlers/AdminService.Authors.CREATE.js
Normal file
@@ -0,0 +1,17 @@
|
||||
async function run() {
|
||||
//debugger
|
||||
//while (true) {}
|
||||
//process.exit()
|
||||
//1.substring()
|
||||
// let res = await specialselect
|
||||
let res = await SELECT.one`title`.from(`Books`).where(`ID=201`)
|
||||
let { title } = res
|
||||
let Author = req.data
|
||||
//await srv.read('Books')
|
||||
|
||||
Author.modifiedBy = "Custom Event handler changed this!"
|
||||
Author.placeOfDeath = " --- Somewhere over " + title + " --- create in Sandbox"
|
||||
//await this.emit("createdAuthor", { Author })
|
||||
return Author
|
||||
}
|
||||
run()
|
||||
41
bookshop/handlers/AdminService.Authors.READ.js
Normal file
41
bookshop/handlers/AdminService.Authors.READ.js
Normal file
@@ -0,0 +1,41 @@
|
||||
function getYear(v) {
|
||||
return parseInt(v.substr(0, 4))
|
||||
}
|
||||
function getMonth(v) {
|
||||
return parseInt(v.substr(5, 2))
|
||||
}
|
||||
function getDay(v) {
|
||||
return parseInt(v.substr(8, 2))
|
||||
}
|
||||
|
||||
function getAge(from, to) {
|
||||
if (from === undefined || from == null) return 0
|
||||
if (to === undefined || to == null) to = new Date().toISOString()
|
||||
let year = getYear(to) - getYear(from) - 1
|
||||
if (
|
||||
getMonth(to) > getMonth(from) ||
|
||||
(getMonth(to) === getMonth(from) && getDay(to) >= getDay(from))
|
||||
) {
|
||||
year++
|
||||
}
|
||||
return year
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const result_ = Array.isArray(result) ? result : [result]
|
||||
for (const row of result_) {
|
||||
row.age = getAge(row.dateOfBirth, row.dateOfDeath)
|
||||
let res = await SELECT.one`title`.from(`Books`).where({ author_ID: row.ID })
|
||||
if (!res) {
|
||||
res = {}
|
||||
}
|
||||
let { title } = res
|
||||
if (!title) {
|
||||
title = "no Books yet"
|
||||
}
|
||||
row.exampleBook = title
|
||||
//let pub = await SELECT.one`name`.from(`sap_capire_bookshop_Publishers`)
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
9
bookshop/handlers/AdminService.Books.CREATE.js
Normal file
9
bookshop/handlers/AdminService.Books.CREATE.js
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
async function run() {
|
||||
const {stock, price, author_ID} = req.data
|
||||
if (stock<0) return req.reject('409', 'Stock must not be negative')
|
||||
if (price<0) return req.reject('409', 'Price must not be negative')
|
||||
let {name} = await SELECT.one`name`.from(`Authors`).where({ID: author_ID})
|
||||
req.data.authorName=name
|
||||
}
|
||||
output = run()
|
||||
6
bookshop/handlers/AdminService.Books.READ.js
Normal file
6
bookshop/handlers/AdminService.Books.READ.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const result_ = Array.isArray(result) ? result : [result];
|
||||
for (const row of result_) {
|
||||
if (row.stock > 50) {
|
||||
row.title += " ---Order now for a 10% discount!";
|
||||
}
|
||||
}
|
||||
9
bookshop/handlers/AdminService.renameAuthor.ON.js
Normal file
9
bookshop/handlers/AdminService.renameAuthor.ON.js
Normal file
@@ -0,0 +1,9 @@
|
||||
async function run() {
|
||||
const {author, newName} = req.data
|
||||
let a = await SELECT `name`.from(`Authors`).where({ID: author})
|
||||
if(!a) return req.error (404, `Can't rename a non-existing author`)
|
||||
await UPDATE (`Authors`,author).with ({ name: newName })
|
||||
//await this.emit ('renamedAuthor', { author, newName })
|
||||
output.msg = 'Success'
|
||||
}
|
||||
run()
|
||||
5
bookshop/handlers/createdAuthor.ON.js
Normal file
5
bookshop/handlers/createdAuthor.ON.js
Normal file
@@ -0,0 +1,5 @@
|
||||
async function run() {
|
||||
let {Author} = req.data
|
||||
Author.placeOfBirth += ' --- modified in custom event'
|
||||
}
|
||||
run()
|
||||
@@ -1 +1,2 @@
|
||||
exports.CatalogService = require('./srv/cat-service')
|
||||
const { CatalogService } = require('./srv/cat-service')
|
||||
module.exports = { CatalogService }
|
||||
|
||||
112
bookshop/notebook.md
Normal file
112
bookshop/notebook.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Base assumption
|
||||
|
||||
Event handlers will always use **publicly available application API's**(services)
|
||||
|
||||
- already done in Sandbox API by overwriting **SELECT**, **UPDATE**, **READ** and **CREATE**
|
||||
|
||||
## Inbound data for validations
|
||||
|
||||
- req.target plus expand on related data
|
||||
- lazy loading on expand
|
||||
- event facade could have an explicit publishing of specific services or documents (e.g. remote services)
|
||||
- CQN Protocol adapter for subsequent reads --> req.data plus application service calls
|
||||
- what is the CDS subset to put in?
|
||||
- req.data + target-rec (proxy, unloaded)
|
||||
- ORM type lazy loading (dereferenced)
|
||||
- application developer could actually provide custom proxies for specific functions
|
||||
- performance impact of multiple accesses to object graph and multiple DB roundtrips
|
||||
- can static code checking or developer annotations influence what is loaded into a graph?
|
||||
- alternative: Stripped-down SELECT limited to req.target and ID
|
||||
- application service only
|
||||
- access rights of user respected
|
||||
- What about to-many relationships? For compositions essential, for associations to be questioned
|
||||
- Application Service Reads
|
||||
- outbound data for changes
|
||||
- call remote services
|
||||
- register new remote services dynamically
|
||||
- CAP provides an API on remote services - connect doesn't need to be done by extension developer
|
||||
- alternative: declarative remote services plumbing with CDS service facade
|
||||
- model looks like static internal services, remote calls done transparently behind the scenes
|
||||
|
||||
-Emit Events
|
||||
|
||||
- choreography of extension points
|
||||
- deep inserts vs. fine grained operations
|
||||
- input validation may be suited for fine grained operations
|
||||
- today not in scope for performance reasons
|
||||
- two different use case: Insert new page to book vs. update order-header with items-constraints in place
|
||||
- reject request, return errors and warnings - suitable for UI, too
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
Annotations available:
|
||||
|
||||
Entity level
|
||||
@expression.constraint : [{if: 'expression evaluates to bool'}, on: ['INSERT, UPDATE, DELETE'], error: 'Transaction Rollback and error message', warning: 'Transaction proceeds and warning message']
|
||||
@expression.computed : [{expression: 'ability to access request payload and modify it', on: ['INSERT, UPDATE']}]
|
||||
@event : [{if: 'expression evaluates to bool', on: ['INSERT, UPDATE, DELETE, READ'], when: 'before or after, default before', emit: 'Event Name', to: 'Messaging target, optional'}]
|
||||
@expresion.code :[{file: 'file name', on:['insert', 'update'], when: 'before or after, default before'},
|
||||
{source: 'each => { if (each.stock > 111) {each.title += `-- 11% discount!`; each.price= each.price*0.9}', on:['insert', 'update'], when: 'before or after'}]
|
||||
Atribute Level
|
||||
@assert.constraint : {if: 'stock>=0 OR stock <1000', error: 'i18n/error102'};
|
||||
@event : {if: 'expression evaluates to bool', on: ['INSERT, UPDATE, DELETE, READ'], when: 'before or after', emit: 'Event Name', to: 'Messaging target, optional' }
|
||||
|
||||
Functions available:
|
||||
EXISTS(association target)
|
||||
COUNT,AVG,MIN,MAX,SUM: Composition items, arrays etc
|
||||
OLD: before image
|
||||
EACH: loop over composition items
|
||||
|
||||
Events covered:
|
||||
CRUD --> Longhand and Shorthand supported?
|
||||
Upsert as one event?
|
||||
Before and after:
|
||||
Before can change change request payload and stop transaction
|
||||
After should trigger only asynchronous messages
|
||||
Specific Events for status changes? I think expression based event emitter suffices
|
||||
*/
|
||||
|
||||
//Entity level annotations
|
||||
@expression.constraint : [{if: 'stock>100 AND price>15)', on: ['INSERT', 'UPDATE'], error: 'No Book over price 15 should have more than 100 stock' }, // error, rollback transactions
|
||||
{if: 'stock>90 AND price>15)', on: ['I', 'U'], warning: 'No Book over price 15 should have more than 100 stock' }] //warning, proceed with transaction but report warning back to UI
|
||||
@expression.computed : {expression: 'if(stock>100) then price=price*0.9', on: ['INSERT']} //ability to modify the payload of the request, but nothing beyond it
|
||||
@expresion.code :[{file: 'sap.capire.bookshop-Books-beforeInsert', on:['insert', 'update'], when: 'before'}, //naming can be arbitrary?
|
||||
{source: 'each => { if (each.stock > 111) {each.title += `-- 11% discount!`; each.price= each.price*0.9}', on:['insert', 'update'], when: 'before'}] //alternative
|
||||
@event : { if:'price>200', emit: 'Expensive Book', to: 'RulesEngine'}
|
||||
entity Books : managed {
|
||||
key ID : Integer;
|
||||
title : localized String(111); @event : {if: 'old.title="Hello"', emit: 'Hello changed' } //old refers to before Image. No "to" clause means message is emitted to any subscriber interested
|
||||
descr : localized String(1111);
|
||||
author : Association to Authors @assert.constraint: 'exists(author)'; //function calls need to evaluate to bool
|
||||
genre : Association to Genres;
|
||||
stock : Integer @assert.constraint : {if: 'stock>=0 OR stock <1000', error: 'Stock not within permitted parameters'}; //when operand is used, no auto-insert
|
||||
price : Decimal(9,2) @assert.constraint : '>0'; //insert operand on left side by default
|
||||
currency : Currency;
|
||||
image : LargeBinary @Core.MediaType : 'image/png';
|
||||
stockWorth: Decimal(9,2) @expression.computed : 'stock*price'; //persisted on write. Overhead in runtime, but performance benefit on read. Payload ignored?
|
||||
// stockWorth2 = stock*price; -- long term goal from compiler team, not persisted on write, but calculated on read
|
||||
stockWorth3 : Decimal @expression.computed: 'if (stock*price>1000) then stockWorth3=stock.price else stockworth3=1000'; //which altenative?
|
||||
stockWorth4 : Decimal @expression.computed: {if: '(stock*price>1000)', then: 'stockWorth3=stock.price', else: 'stockworth3=1000'};
|
||||
}
|
||||
//@assert.expression: 'dateOfBirth<dateOfDeath'
|
||||
entity Authors : managed {
|
||||
key ID : Integer;
|
||||
name : String(111);
|
||||
dateOfBirth : Date ;
|
||||
dateOfDeath : Date @expression.constraint: '>dateOfBirth';
|
||||
placeOfBirth : String;
|
||||
placeOfDeath : String;
|
||||
books : Association to many Books on books.author = $self;
|
||||
}
|
||||
|
||||
/** Hierarchically organized Code List for Genres */
|
||||
entity Genres : sap.common.CodeList {
|
||||
key ID : Integer;
|
||||
parent : Association to Genres;
|
||||
children : Composition of many Genres on children.parent = $self;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -2,11 +2,18 @@
|
||||
"name": "@capire/bookshop",
|
||||
"version": "1.0.0",
|
||||
"description": "A simple self-contained bookshop service.",
|
||||
"files": [
|
||||
"app",
|
||||
"srv",
|
||||
"db",
|
||||
"index.cds",
|
||||
"index.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"@capire/common": "*",
|
||||
"@sap/cds": "^5.0.4",
|
||||
"@sap/cds": ">=5.9",
|
||||
"express": "^4.17.1",
|
||||
"passport": "0.4.1"
|
||||
"passport": ">=0.4.1",
|
||||
"vm2": ">=3.9.9"
|
||||
},
|
||||
"scripts": {
|
||||
"genres": "cds serve test/genres.cds",
|
||||
@@ -15,8 +22,12 @@
|
||||
},
|
||||
"cds": {
|
||||
"requires": {
|
||||
"code-extensibility" : true,
|
||||
"db": {
|
||||
"kind": "sql"
|
||||
"kind": "sqlite",
|
||||
"credentials": {
|
||||
"database": "sqlite.db"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
bookshop/sqlite.db
Normal file
BIN
bookshop/sqlite.db
Normal file
Binary file not shown.
@@ -1,5 +1,30 @@
|
||||
using { sap.capire.bookshop as my } from '../db/schema';
|
||||
service AdminService @(requires:'admin') {
|
||||
entity Books as projection on my.Books;
|
||||
entity Authors as projection on my.Authors;
|
||||
using {sap.capire.bookshop as my} from '../db/schema';
|
||||
|
||||
service AdminService // @(requires : 'admin')
|
||||
{
|
||||
entity Books as projection on my.Books actions {
|
||||
action increaseStock(count : Integer);
|
||||
function stock() returns Integer;
|
||||
};
|
||||
|
||||
@Extensibility : {
|
||||
Fields.Enabled : true,
|
||||
Fields.Quota: 100,
|
||||
Relations.Enabled : false,
|
||||
Annotations.Enabled : true,
|
||||
Logic.Enabled : true,
|
||||
Logic.constraints: true,
|
||||
Logic.calculations: true,
|
||||
Logic.Handler : [create, update, delete, read]
|
||||
}
|
||||
entity Authors as projection on my.Authors;
|
||||
|
||||
action renameAuthor(author : Authors:ID, newName : String) returns {
|
||||
msg : String
|
||||
};
|
||||
function getStock(book: Books:ID) returns Integer;
|
||||
event newBook : {
|
||||
book : Books:ID;
|
||||
name : Books:title
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,135 @@
|
||||
const cds = require('@sap/cds')
|
||||
const cds = require("@sap/cds")
|
||||
//const cds_sandbox = require("sap/cds/sandbox")
|
||||
const { VM, VMScript } = require("vm2")
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const { nextTick } = require("process")
|
||||
|
||||
module.exports = cds.service.impl (function(){
|
||||
this.before ('NEW','Authors', genid)
|
||||
this.before ('NEW','Books', genid)
|
||||
})
|
||||
class AdminService extends cds.ApplicationService {
|
||||
init() {
|
||||
this.after("READ", async (result, req) => {
|
||||
if (!(result === undefined || result == null)) {
|
||||
const code = getCode(req.target.name, "READ")
|
||||
if (code) {
|
||||
await executeCode.call(this, code, req, result)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/** Generate primary keys for target entity in request */
|
||||
async function genid (req) {
|
||||
const {ID} = await cds.tx(req).run (SELECT.one.from(req.target).columns('max(ID) as ID'))
|
||||
req.data.ID = ID - ID % 100 + 100 + 1
|
||||
this.before("CREATE", async (req) => {
|
||||
const code = getCode(req.target.name, "CREATE")
|
||||
if (code) {
|
||||
await executeCode.call(this, code, req)
|
||||
}
|
||||
})
|
||||
|
||||
this.before("UPDATE", async (req) => {
|
||||
const code = getCode(req.target.name, "CREATE")
|
||||
if (code) {
|
||||
await executeCode.call(this, code, req)
|
||||
}
|
||||
})
|
||||
|
||||
this.on("*", async (req, next) => {
|
||||
if (!(req.target === undefined || req.target == null)) return next()
|
||||
//ToDo: check whether action or event is part of an extension
|
||||
// DO NOT OVERWRITE EXISTING Action Implementations!
|
||||
// evaluate: Can we augment action implementation with super.next?
|
||||
if (req.constructor.name === "EventMessage") {
|
||||
const code = getCode(req.event, "ON")
|
||||
if (code) {
|
||||
await executeCode.call(this, code, req)
|
||||
}
|
||||
} else if (req.constructor.name === "ODataRequest") {
|
||||
var output = {}
|
||||
const code = getCode(this.name + "." + req.event, "ON")
|
||||
if (code) {
|
||||
await executeCode.call(this, code, req, {}, output)
|
||||
return output
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
//ToDo: Prefix for Service not in event emitter
|
||||
this.before("CREATE", "Authors", async (req) => {
|
||||
let Author = req.data
|
||||
await this.emit("createdAuthor", { Author })
|
||||
|
||||
})
|
||||
|
||||
return super.init()
|
||||
}
|
||||
}
|
||||
|
||||
var counter = 1;
|
||||
|
||||
function newLabel() {return "VM2 - req: " + counter++}
|
||||
|
||||
//should only work in local exection (cds watch)
|
||||
// alternative: Upon Bootstrapping, merge files into CSN
|
||||
function getCodeFromFile(name, operation) {
|
||||
const filename = name + "." + operation + ".js"
|
||||
const file = path.join(__dirname, "..", "handlers", filename)
|
||||
try {
|
||||
const code = fs.readFileSync(file, "utf8")
|
||||
return code
|
||||
} catch (error) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
//after push this should be the only thing that works
|
||||
function getCodeFromAnnotation(name, operation) {
|
||||
return ""
|
||||
}
|
||||
|
||||
function getCode(name, operation) {
|
||||
let code=getCodeFromAnnotation(name, operation)
|
||||
if (code==="") {code=getCodeFromFile(name, operation)}
|
||||
return code
|
||||
}
|
||||
|
||||
function scanCode(code) {
|
||||
//ESLINT
|
||||
}
|
||||
|
||||
async function executeCode(code, req, result, output) {
|
||||
const srv = this
|
||||
const label=newLabel()
|
||||
console.time(label)
|
||||
const vm = new VM({
|
||||
console: "inherit",
|
||||
timeout: 500,
|
||||
allowAsync: true,
|
||||
sandbox: { req, //todo: isolate req.data, req.reject, req.error, req.message
|
||||
result, //important for READ
|
||||
output, //used for Action Implementation
|
||||
SELECT : (class extends require('@sap/cds/lib/ql/SELECT') {then(r,e) {return srv.run(this).then(r,e)}})._api(),
|
||||
INSERT : (class extends require('@sap/cds/lib/ql/INSERT') {then(r,e) {return srv.run(this).then(r,e)}})._api(),
|
||||
UPDATE : (class extends require('@sap/cds/lib/ql/UPDATE') {then(r,e) {return srv.run(this).then(r,e)}})._api(),
|
||||
CREATE : (class extends require('@sap/cds/lib/ql/CREATE') {then(r,e) {return srv.run(this).then(r,e)}})._api(),
|
||||
//srv: this,
|
||||
JSON },
|
||||
})
|
||||
|
||||
try {
|
||||
await vm.run(code)
|
||||
return output
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
req.reject("409", "Error in VM")
|
||||
}
|
||||
finally {
|
||||
console.timeEnd(label)
|
||||
}
|
||||
// console.log(req.data)
|
||||
}
|
||||
/** Generate primary keys for target entity in request */
|
||||
async function genid(req) {
|
||||
const { ID } = await cds
|
||||
.tx(req)
|
||||
.run(SELECT.one.from(req.target).columns("max(ID) as ID"))
|
||||
req.data.ID = ID - (ID % 100) + 100 + 1
|
||||
}
|
||||
|
||||
module.exports = { AdminService }
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
using { sap.capire.bookshop as my } from '../db/schema';
|
||||
service CatalogService @(path:'/browse') {
|
||||
|
||||
@readonly entity Books as SELECT from my.Books { *,
|
||||
/** For displaying lists of Books */
|
||||
@readonly entity ListOfBooks as projection on Books
|
||||
excluding { descr };
|
||||
|
||||
/** For display in details pages */
|
||||
@readonly entity Books as projection on my.Books { *,
|
||||
author.name as author
|
||||
} excluding { createdBy, modifiedBy };
|
||||
|
||||
@readonly entity ListOfBooks as SELECT from Books
|
||||
excluding { descr };
|
||||
@readonly entity Publishers as projection on my.Publishers;
|
||||
|
||||
@requires: 'authenticated-user'
|
||||
action submitOrder ( book: Books:ID, amount: Integer ) returns { stock: Integer };
|
||||
event OrderedBook : { book: Books:ID; amount: Integer; buyer: String };
|
||||
action submitOrder ( book: Books:ID, quantity: Integer ) returns { stock: Integer };
|
||||
event OrderedBook : { book: Books:ID; quantity: Integer; buyer: String };
|
||||
}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
const cds = require('@sap/cds')
|
||||
const { Books } = cds.entities ('sap.capire.bookshop')
|
||||
|
||||
class CatalogService extends cds.ApplicationService { init(){
|
||||
|
||||
const { Books } = cds.entities ('sap.capire.bookshop')
|
||||
|
||||
// Reduce stock of ordered books if available stock suffices
|
||||
this.on ('submitOrder', async req => {
|
||||
const {book,amount} = req.data
|
||||
let {stock} = await SELECT `stock` .from (Books,book)
|
||||
if (stock >= amount) {
|
||||
await UPDATE (Books,book) .with (`stock -=`, amount)
|
||||
await this.emit ('OrderedBook', { book, amount, buyer:req.user.id })
|
||||
return { stock }
|
||||
}
|
||||
else return req.error (409,`${amount} exceeds stock for book #${book}`)
|
||||
const {book,quantity} = req.data
|
||||
if (quantity < 1) return req.reject (400,`quantity has to be 1 or more`)
|
||||
let b = await SELECT `stock` .from (Books,book)
|
||||
if (!b) return req.error (404,`Book #${book} doesn't exist`)
|
||||
let {stock} = b
|
||||
if (quantity > stock) return req.reject (409,`${quantity} exceeds stock for book #${book}`)
|
||||
await UPDATE (Books,book) .with ({ stock: stock -= quantity })
|
||||
await this.emit ('OrderedBook', { book, quantity, buyer:req.user.id })
|
||||
return { stock }
|
||||
})
|
||||
|
||||
// Add some discount for overstocked books
|
||||
|
||||
11
bookshop/srv/code-extensions.cds
Normal file
11
bookshop/srv/code-extensions.cds
Normal file
@@ -0,0 +1,11 @@
|
||||
//this file is machine-created during cds.build
|
||||
namespace sap.capire.bookshop; //> important for reflection
|
||||
using from '../db/schema';
|
||||
using from '../srv/cat-service';
|
||||
using from '../srv/admin-service';
|
||||
|
||||
annotate AdminService.Authors with @extension.logic: [{when: 'CREATE', code: 'async function run() {\r\n \/\/debugger\r\n \/\/while (true) {}\r\n \/\/process.exit()\r\n \/\/1.substring()\r\n \/\/ let res = await specialselect\r\n let res = await SELECT.one`title`.from(`Books`).where(`ID=201`)\r\n let { title } = res\r\n let Author = req.data\r\n Author.modifiedBy = \"Custom Event handler changed this!\"\r\n Author.placeOfDeath = \" --- Somewhere over \" + title + \" --- create in Sandbox\"\r\n \/\/await this.emit(\"createdAuthor\", { Author })\r\n return Author\r\n}\r\nrun()\r\n'},
|
||||
{when: 'READ', code: 'function getYear(v) {\r\n return parseInt(v.substr(0, 4))\r\n}\r\nfunction getMonth(v) {\r\n return parseInt(v.substr(5, 2))\r\n}\r\nfunction getDay(v) {\r\n return parseInt(v.substr(8, 2))\r\n}\r\n\r\nfunction getAge(from, to) {\r\n if (from === undefined || from == null) return 0\r\n if (to === undefined || to == null) to = new Date().toISOString()\r\n let year = getYear(to) - getYear(from) - 1\r\n if (\r\n getMonth(to) > getMonth(from) ||\r\n (getMonth(to) === getMonth(from) && getDay(to) >= getDay(from))\r\n ) {\r\n year++\r\n }\r\n return year\r\n}\r\n\r\nconst result_ = Array.isArray(result) ? result : [result]\r\nfor (const row of result_) {\r\n row.modifiedBy += \" --- read in sandbox\"\r\n row.age = getAge(row.dateOfBirth, row.dateOfDeath)\r\n}'}
|
||||
];
|
||||
annotate AdminService.Books with @extension.logic;
|
||||
annotate CatalogService.ListOfBooks with @extension.logic;
|
||||
16
bookshop/srv/user-service.cds
Normal file
16
bookshop/srv/user-service.cds
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Exposes user information
|
||||
*/
|
||||
@requires: 'authenticated-user'
|
||||
service UserService {
|
||||
|
||||
/**
|
||||
* The current user
|
||||
*/
|
||||
@odata.singleton entity me {
|
||||
id : String; // user id
|
||||
locale : String;
|
||||
tenant : String;
|
||||
}
|
||||
|
||||
}
|
||||
4
bookshop/srv/user-service.js
Normal file
4
bookshop/srv/user-service.js
Normal file
@@ -0,0 +1,4 @@
|
||||
const cds = require('@sap/cds')
|
||||
module.exports = cds.service.impl((srv) => {
|
||||
srv.on('READ', 'me', ({ tenant, user, locale }) => ({ id: user.id, locale, tenant }))
|
||||
})
|
||||
@@ -1,10 +1,46 @@
|
||||
@server = http://localhost:4004
|
||||
@me = Authorization: Basic {{$processEnv USER}}:
|
||||
@id = 2000
|
||||
|
||||
|
||||
### ------------------------------------------------------------------------
|
||||
# Fetch Authors
|
||||
GET {{server}}/admin/Authors
|
||||
|
||||
### ------------------------------------------------------------------------
|
||||
# Fetch one Author
|
||||
GET {{server}}/admin/Authors({{id}})
|
||||
|
||||
### ------------------------------------------------------------------------
|
||||
# Create Author
|
||||
POST {{server}}/admin/Authors
|
||||
Content-Type: application/json;IEEE754Compatible=true
|
||||
|
||||
{
|
||||
"ID": {{id}},
|
||||
"name": "Nick",
|
||||
"placeOfBirth": "Somewhere",
|
||||
"placeOfDeath": "over the Rainbox",
|
||||
"dateOfBirth" : "1975-05-27"
|
||||
}
|
||||
|
||||
### ------------------------------------------------------------------------
|
||||
# rename author via unbound action
|
||||
POST {{server}}/admin/renameAuthor
|
||||
Content-Type: application/json
|
||||
{{me}}
|
||||
|
||||
{ "author":{{id}}, "newName":"Super Nick" }
|
||||
|
||||
|
||||
### ------------------------------------------------------------------------
|
||||
# Get service info
|
||||
GET {{server}}/browse
|
||||
GET {{server}}/admin
|
||||
{{me}}
|
||||
|
||||
### ------------------------------------------------------------------------
|
||||
# Get $metadata document
|
||||
GET {{server}}/admin/$metadata
|
||||
{{me}}
|
||||
|
||||
|
||||
@@ -16,21 +52,19 @@ GET {{server}}/browse/$metadata
|
||||
|
||||
### ------------------------------------------------------------------------
|
||||
# Browse Books as any user
|
||||
GET {{server}}/browse/Books?
|
||||
GET {{server}}/browse/ListOfBooks?
|
||||
# &$select=title,stock
|
||||
# &$expand=currency
|
||||
&$expand=genre
|
||||
# &sap-language=de
|
||||
{{me}}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### ------------------------------------------------------------------------
|
||||
# Fetch Authors as admin
|
||||
GET {{server}}/admin/Authors?
|
||||
# &$select=name,dateOfBirth,placeOfBirth
|
||||
# &$expand=books($select=title;$expand=currency)
|
||||
# &$filter=ID eq 101
|
||||
# &sap-language=de
|
||||
Authorization: Basic alice:
|
||||
# Fetch Books as admin
|
||||
GET {{server}}/admin/Books
|
||||
|
||||
### ------------------------------------------------------------------------
|
||||
# Create book
|
||||
@@ -39,12 +73,12 @@ Content-Type: application/json;IEEE754Compatible=true
|
||||
Authorization: Basic alice:
|
||||
|
||||
{
|
||||
"ID": 2,
|
||||
"title": "Poems : Pocket Poets",
|
||||
"ID": 16,
|
||||
"title": "Deh4",
|
||||
"descr": "The Everyman's Library Pocket Poets hardcover series is popular for its compact size and reasonable price which does not compromise content. Poems: Bronte contains poems that demonstrate a sensibility elemental in its force with an imaginative discipline and flexibility of the highest order. Also included are an Editor's Note and an index of first lines.",
|
||||
"author": { "ID": 101 },
|
||||
"genre": { "ID": 12 },
|
||||
"stock": 5,
|
||||
"stock": -100,
|
||||
"price": "12.05",
|
||||
"currency": { "code": "USD" }
|
||||
}
|
||||
@@ -71,7 +105,7 @@ POST {{server}}/browse/submitOrder
|
||||
Content-Type: application/json
|
||||
{{me}}
|
||||
|
||||
{ "book":201, "amount":5 }
|
||||
{ "book":201, "quantity":5 }
|
||||
|
||||
|
||||
### ------------------------------------------------------------------------
|
||||
|
||||
2
bookstore/index.cds
Normal file
2
bookstore/index.cds
Normal file
@@ -0,0 +1,2 @@
|
||||
namespace sap.capire.bookshop; //> important for reflection
|
||||
using from './srv/mashup';
|
||||
34
bookstore/package.json
Normal file
34
bookstore/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@capire/bookstore",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@capire/bookshop": "*",
|
||||
"@capire/reviews": "*",
|
||||
"@capire/orders": "*",
|
||||
"@capire/common": "*",
|
||||
"@capire/data-viewer": "*",
|
||||
"@sap/cds": ">=5",
|
||||
"express": "^4.17.1"
|
||||
},
|
||||
"cds": {
|
||||
"requires": {
|
||||
"ReviewsService": {
|
||||
"kind": "odata",
|
||||
"model": "@capire/reviews"
|
||||
},
|
||||
"OrdersService": {
|
||||
"kind": "odata",
|
||||
"model": "@capire/orders"
|
||||
},
|
||||
"messaging": {
|
||||
"[development]": { "kind": "file-based-messaging" },
|
||||
"[hybrid]": { "kind": "enterprise-messaging-shared" },
|
||||
"[production]": { "kind": "enterprise-messaging" }
|
||||
},
|
||||
"db": {
|
||||
"kind": "sql"
|
||||
}
|
||||
},
|
||||
"log": { "service": true }
|
||||
}
|
||||
}
|
||||
22
bookstore/server.js
Normal file
22
bookstore/server.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const cds = require ('@sap/cds')
|
||||
|
||||
// Add mashup logic
|
||||
cds.once('served', require('./srv/mashup'))
|
||||
|
||||
// Add routes to UIs from imported packages
|
||||
cds.once('bootstrap',(app)=>{
|
||||
app.serve ('/bookshop') .from ('@capire/bookshop','app/vue')
|
||||
app.serve ('/reviews') .from ('@capire/reviews','app/vue')
|
||||
app.serve ('/orders') .from('@capire/orders','app/orders')
|
||||
app.serve ('/data') .from('@capire/data-viewer','app/viewer')
|
||||
})
|
||||
|
||||
// Add Swagger UI
|
||||
require('./srv/swagger-ui')
|
||||
|
||||
// Returning cds.server
|
||||
module.exports = cds.server
|
||||
|
||||
// For didactic reasons in capire
|
||||
const { ReviewsService, OrdersService } = cds.requires
|
||||
if (!ReviewsService.credentials && !OrdersService.credentials) cds.requires.messaging = false
|
||||
38
bookstore/srv/mashup.cds
Normal file
38
bookstore/srv/mashup.cds
Normal file
@@ -0,0 +1,38 @@
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Enhancing bookshop with Reviews and Orders provided through
|
||||
// respective reuse packages and services
|
||||
//
|
||||
|
||||
using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
||||
|
||||
//
|
||||
// Extend Books with access to Reviews and average ratings
|
||||
//
|
||||
using { ReviewsService.Reviews } from '@capire/reviews';
|
||||
extend Books with {
|
||||
reviews : Composition of many Reviews on reviews.subject = $self.ID;
|
||||
rating : Decimal;
|
||||
numberOfReviews : Integer;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Extend Orders with Books as Products
|
||||
//
|
||||
using { sap.capire.orders.Orders } from '@capire/orders';
|
||||
extend Orders with {
|
||||
extend Items with {
|
||||
book : Association to Books on product.ID = book.ID
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add orders fiori app (in case of embedded orders service)
|
||||
using from '@capire/orders/app/fiori';
|
||||
|
||||
// Add data browser
|
||||
using from '@capire/data-viewer';
|
||||
|
||||
// Incorporate pre-build extensions from...
|
||||
using from '@capire/common';
|
||||
@@ -1,6 +1,6 @@
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Mashing up provided and required services...
|
||||
// Mashing up bookshop services with required services...
|
||||
//
|
||||
module.exports = async()=>{ // called by server.js
|
||||
|
||||
@@ -20,30 +20,29 @@ module.exports = async()=>{ // called by server.js
|
||||
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
|
||||
console.debug ('> delegating request to ReviewsService')
|
||||
const [id] = req.params, { columns, limit } = req.query.SELECT
|
||||
return ReviewsService.tx(req).read ('Reviews',columns).limit(limit).where({subject:String(id)})
|
||||
return ReviewsService.read ('Reviews',columns).limit(limit).where({subject:String(id)})
|
||||
}))
|
||||
|
||||
//
|
||||
// Create an order with the OrdersService when CatalogService signals a new order
|
||||
//
|
||||
CatalogService.on ('OrderedBook', async (msg) => {
|
||||
const { book, amount, buyer } = msg.data
|
||||
const { book, quantity, buyer } = msg.data
|
||||
const { title, price } = await db.tx(msg).read (Books, book, b => { b.title, b.price })
|
||||
return OrdersService.tx(msg).create ('Orders').entries({
|
||||
OrderNo: 'Order at '+ (new Date).toLocaleString(),
|
||||
Items: [{ product:{ID:`${book}`}, title, price, amount }],
|
||||
Items: [{ product:{ID:`${book}`}, title, price, quantity }],
|
||||
buyer, createdBy: buyer
|
||||
})
|
||||
})
|
||||
|
||||
//
|
||||
// Update Books' average ratings when ReviewsService signals updatd reviews
|
||||
// Update Books' average ratings when ReviewsService signals updated reviews
|
||||
//
|
||||
ReviewsService.on ('reviewed', (msg) => {
|
||||
console.debug ('> received:', msg.event, msg.data)
|
||||
const { subject, rating } = msg.data
|
||||
return UPDATE(Books,subject).with({rating})
|
||||
// ^ Note: the framework will execute this and take care for db.tx
|
||||
const { subject, count, rating } = msg.data
|
||||
return UPDATE(Books,subject).with({ numberOfReviews:count, rating })
|
||||
})
|
||||
|
||||
//
|
||||
@@ -51,9 +50,9 @@ module.exports = async()=>{ // called by server.js
|
||||
//
|
||||
OrdersService.on ('OrderChanged', (msg) => {
|
||||
console.debug ('> received:', msg.event, msg.data)
|
||||
const { product, deltaAmount } = msg.data
|
||||
const { product, deltaQuantity } = msg.data
|
||||
return UPDATE (Books) .where ('ID =', product)
|
||||
.and ('stock >=', deltaAmount)
|
||||
.set ('stock -=', deltaAmount)
|
||||
.and ('stock >=', deltaQuantity)
|
||||
.set ('stock -=', deltaQuantity)
|
||||
})
|
||||
}
|
||||
10
bookstore/srv/swagger-ui.js
Normal file
10
bookstore/srv/swagger-ui.js
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Adding Swagger UI - see https://cap.cloud.sap/docs/advanced/openapi
|
||||
const cds = require ('@sap/cds')
|
||||
try {
|
||||
const cds_swagger = require ('cds-swagger-ui-express')
|
||||
cds.once ('bootstrap', app => app.use (cds_swagger()) )
|
||||
} catch (err) {
|
||||
if (err.code !== 'MODULE_NOT_FOUND') throw err
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
@bookshop = http://localhost:4004
|
||||
@reviews-service = {{bookshop}}/reviews
|
||||
# Uncomment this when running a separate reviews service
|
||||
@reviews-service = http://localhost:4005/reviews
|
||||
# @reviews-service = http://localhost:4005/reviews
|
||||
|
||||
|
||||
|
||||
@@ -38,7 +38,11 @@ GET {{bookshop}}/browse/Books(201)?
|
||||
&$select=ID,title,rating
|
||||
&$expand=reviews
|
||||
|
||||
###
|
||||
|
||||
GET {{bookshop}}/browse/Books?
|
||||
&$select=title,author&$expand=currency
|
||||
Accept-Language: de
|
||||
|
||||
#################################################
|
||||
#
|
||||
@@ -5,9 +5,11 @@ CAD;de;Kanadischer Dollar;Kanadischer Dollar
|
||||
AUD;de;Australischer Dollar;Australischer Dollar
|
||||
GBP;de;Pfund;Britische Pfund
|
||||
ILS;de;Schekel;Israelische Schekel
|
||||
JPY;de;Yen;Japanische Yen
|
||||
EUR;fr;euro;de la Zone euro
|
||||
USD;fr;dollar;dollar des États-Unis
|
||||
CAD;fr;dollar canadien;dollar canadien
|
||||
AUD;fr;dollar australien;dollar australien
|
||||
GBP;fr;livre sterling;pound sterling
|
||||
ILS;fr;Shekel;shekel israelien
|
||||
JPY;fr;Yen;Yen japonais
|
||||
|
500 Internal Server Error
Gitea Version: 1.24.4 |