Compare commits
113 Commits
key-user-e
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc69311ebe | ||
|
|
67643b8d95 | ||
|
|
8ba8381ca7 | ||
|
|
e7fbcc1ac1 | ||
|
|
d977c21483 | ||
|
|
2e10b1ac4d | ||
|
|
cabcd404cf | ||
|
|
5141df8e0f | ||
|
|
785ecbda8f | ||
|
|
2b9ceba62f | ||
|
|
c6e2fac860 | ||
|
|
f2877db34f | ||
|
|
d2bd4c5bb1 | ||
|
|
732b6b081c | ||
|
|
db595a9635 | ||
|
|
4a8b85f227 | ||
|
|
0cb7349e62 | ||
|
|
ba3eeff58b | ||
|
|
86e6983bed | ||
|
|
9bbc431448 | ||
|
|
097b74db03 | ||
|
|
870f8d063d | ||
|
|
4a0db3e259 | ||
|
|
5e76c7f52e | ||
|
|
31c110e9e2 | ||
|
|
8d67c0bf62 | ||
|
|
21359a7ad3 | ||
|
|
649b9c8ca8 | ||
|
|
11c54b29d0 | ||
|
|
80302a0a3d | ||
|
|
c8c10b7c8a | ||
|
|
d9d7203e49 | ||
|
|
4a8379e953 | ||
|
|
dd28bd04f6 | ||
|
|
5397df5e81 | ||
|
|
f57ae79e37 | ||
|
|
6412df75dd | ||
|
|
9277aa12da | ||
|
|
d41a9e10ab | ||
|
|
0b2182afcb | ||
|
|
6b74c23aa0 | ||
|
|
c854717359 | ||
|
|
9dfe62c5c7 | ||
|
|
5b4210bb38 | ||
|
|
48ee934b00 | ||
|
|
254bb40e38 | ||
|
|
fb5d00bbe0 | ||
|
|
7ae992c5bb | ||
|
|
016587094f | ||
|
|
d04cb801c4 | ||
|
|
902afd8a76 | ||
|
|
c4bee1f09a | ||
|
|
278258c436 | ||
|
|
8601bd8a46 | ||
|
|
98113c46fd | ||
|
|
eb4bc703dd | ||
|
|
de806a1e38 | ||
|
|
2999c8df83 | ||
|
|
87dbb4552a | ||
|
|
0bdf4bb93e | ||
|
|
a4810c3a7b | ||
|
|
c899843319 | ||
|
|
43e5f6faef | ||
|
|
1b5cc62d1c | ||
|
|
4b48d68ba6 | ||
|
|
d21a0e08ca | ||
|
|
50fd83fd19 | ||
|
|
3f20f4b9a2 | ||
|
|
5031779be8 | ||
|
|
ad385cbc6c | ||
|
|
2650db4ee9 | ||
|
|
e04e9d231d | ||
|
|
476724792e | ||
|
|
5ae31f7c67 | ||
|
|
edbca44a43 | ||
|
|
02f19e295d | ||
|
|
05cdb67329 | ||
|
|
4877386e86 | ||
|
|
4bd4446975 | ||
|
|
a54b0124ea | ||
|
|
944afe3bc8 | ||
|
|
92a83f71a1 | ||
|
|
5e5ace0dd4 | ||
|
|
593228a51e | ||
|
|
624cea6343 | ||
|
|
adfe170e8d | ||
|
|
6b08826af5 | ||
|
|
c6239f0375 | ||
|
|
0d19e56509 | ||
|
|
fc088015d3 | ||
|
|
c9f7dc68b8 | ||
|
|
7d9303635e | ||
|
|
b724ae900c | ||
|
|
2f96b92854 | ||
|
|
99dc8d31d7 | ||
|
|
c672e0f1fa | ||
|
|
0ddd70acbc | ||
|
|
3e52a9a102 | ||
|
|
b44701ef62 | ||
|
|
c23ddc7e54 | ||
|
|
66bd2f707c | ||
|
|
3320c7e5a2 | ||
|
|
a35782e775 | ||
|
|
e5bd8ec5a5 | ||
|
|
0aa95a0a67 | ||
|
|
5015eb8c52 | ||
|
|
6d3f4c689f | ||
|
|
f0fead2bc2 | ||
|
|
f1d780d6d9 | ||
|
|
796bf62bde | ||
|
|
5f176a0b88 | ||
|
|
a5c8b5101e | ||
|
|
d72ff809b0 |
4
.github/workflows/node.js.yml
vendored
4
.github/workflows/node.js.yml
vendored
@@ -5,9 +5,9 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ master ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches: [ master ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -1,24 +1,18 @@
|
|||||||
const { exec } = require ('child_process')
|
const { exec } = require ('child_process')
|
||||||
const isWin = process.platform === 'win32'
|
|
||||||
const express = require ('express')
|
const express = require ('express')
|
||||||
const fs = require ('fs')
|
const fs = require ('fs')
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
const { PORT=4444 } = process.env
|
const { PORT=4444 } = process.env
|
||||||
const [,,port=PORT,scope='@capire'] = process.argv
|
const [,,port=PORT] = process.argv
|
||||||
const cwd = __dirname
|
const cwd = __dirname
|
||||||
|
|
||||||
// clean up on start (exit handler might not complete on Windows)
|
|
||||||
exec(isWin ? 'del *.tgz' : 'rm *.tgz', {cwd})
|
|
||||||
|
|
||||||
|
|
||||||
app.use('/-/:tarball', (req,res,next) => {
|
app.use('/-/:tarball', (req,res,next) => {
|
||||||
console.debug ('GET', req.params)
|
console.debug ('GET', req.params)
|
||||||
try {
|
try {
|
||||||
const { tarball } = req.params
|
const { tarball } = req.params
|
||||||
const [, pkg ] = /^\w+-(\w+)/.exec(tarball)
|
const [, pkg ] = /^capire-(\w+)/.exec(tarball)
|
||||||
fs.lstat(tarball,(err => {
|
fs.lstat(tarball,(err => {
|
||||||
if (err) console.debug (`npm pack ../${pkg}`)
|
|
||||||
if (err) exec(`npm pack ../${pkg}`,{cwd},next)
|
if (err) exec(`npm pack ../${pkg}`,{cwd},next)
|
||||||
else next()
|
else next()
|
||||||
}))
|
}))
|
||||||
@@ -31,14 +25,12 @@ app.use('/-/:tarball', (req,res,next) => {
|
|||||||
app.use('/-', express.static(__dirname))
|
app.use('/-', express.static(__dirname))
|
||||||
|
|
||||||
app.get('/*', (req,res)=>{
|
app.get('/*', (req,res)=>{
|
||||||
const urlRegex = /^\/(@\w+)\/(\w+)/
|
|
||||||
const url = decodeURIComponent(req.url)
|
const url = decodeURIComponent(req.url)
|
||||||
console.debug ('GET',url)
|
console.debug ('GET',url)
|
||||||
try {
|
try {
|
||||||
if (!urlRegex.test(url)) return res.sendStatus(404)
|
const [, capire, pkg ] = /^\/(@capire)\/(\w+)/.exec(url)
|
||||||
const [, scpe, pkg ] = urlRegex.exec(url)
|
const package = require (`${capire}/${pkg}/package.json`)
|
||||||
const package = require (`${scpe}/${pkg}/package.json`)
|
const tarball = `capire-${pkg}-${package.version}.tgz`
|
||||||
const tarball = `${scpe.slice(1)}-${pkg}-${package.version}.tgz`
|
|
||||||
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
|
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
|
||||||
res.json({
|
res.json({
|
||||||
"name": package.name,
|
"name": package.name,
|
||||||
@@ -50,30 +42,29 @@ app.get('/*', (req,res)=>{
|
|||||||
"name": package.name,
|
"name": package.name,
|
||||||
"version": package.version,
|
"version": package.version,
|
||||||
"dist": {
|
"dist": {
|
||||||
"tarball": `${server.url}/-/${tarball}`
|
"tarball": `http://localhost:${port}/-/${tarball}`
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.code === 'MODULE_NOT_FOUND') return res.sendStatus(404)
|
console.error(e)
|
||||||
console.error(e); throw e
|
res.sendStatus(404)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const server = app.listen(port, ()=>{
|
app.listen(port, ()=>{
|
||||||
const url = server.url = `http://localhost:${server.address().port}`
|
console.log (`npm set @capire:registry=http://localhost:${port}`)
|
||||||
console.log (`npm set ${scope}:registry=${url}`)
|
console.log (`@capire registry listening on http://localhost:${port}`)
|
||||||
exec(`npm set ${scope}:registry=${url}`)
|
exec(`npm set @capire:registry=http://localhost:${port}`)
|
||||||
console.log (`${scope} registry listening on ${url}`)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const _exit = ()=>{
|
const _exit = ()=>{
|
||||||
server.close()
|
console.log ('\nnpm conf rm @capire:registry')
|
||||||
exec(`npm conf rm "${scope}:registry"`, ()=> { process.exit() })
|
exec('npm conf rm @capire:registry')
|
||||||
|
exec('rm *.tgz')
|
||||||
|
process.exit()
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on ('SIGTERM',_exit)
|
process.on ('SIGTERM',_exit)
|
||||||
process.on ('SIGHUP',_exit)
|
process.on ('SIGHUP',_exit)
|
||||||
process.on ('SIGINT',_exit)
|
process.on ('SIGINT',_exit)
|
||||||
|
|||||||
@@ -68,35 +68,26 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "fiori/package.json",
|
"file": "fiori/package.json",
|
||||||
"description": "#### Configuration\n\nThe `cds.requires` section in `package.json` is a place to configure which of the `db/sqlite` and `db/hana` folders are used for which database.\n\nWe use [Node.js profiles](https://cap.cloud.sap/docs/node.js/cds-env#profiles) to separate the configuration.\nIn the `development` profile, you can see that `db/sqlite` is set as the model, while the `db/hana` folder is configured in the `production` profile. `db-ext` is a pseudo datasource, its name doesn't matter.\n\nSee [`cds.resolve`](https://cap.cloud.sap/docs/node.js/cds-compile#cds-resolve) to learn more about how models are found.",
|
"description": "#### Configuration\n\nThe `cds` section in `package.json` is a place to configure which of the `db/sqlite` and `db/hana` folders are used for which database.\nWe use [Node.js profiles](https://cap.cloud.sap/docs/node.js/cds-env#profiles) to separate the configuration.\nIn the `development` profile, you can see that `db/sqlite` is set as the model, while the `db/hana` folder is configured in the `production` profile.",
|
||||||
"selection": {
|
"line": 17,
|
||||||
"start": {
|
|
||||||
"line": 41,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 48,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Configuration"
|
"title": "Configuration"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "fiori/package.json",
|
"file": "fiori/package.json",
|
||||||
"description": "#### Run with SQLite\n\nTo run with `development` and an in-memory SQLite database, you don't need to do anything special, because it's activated by default. Just run:\n\n>> cds watch fiori\n\nThen open [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) to see the two new fields.\n",
|
"description": "#### Run with SQLite\n\nTo run with `development` and an in-memory SQLite database, you don't need to do anything special, because it's activated by default. Just run:\n\n>> cds watch fiori\n\nThen open [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) to see the two new fields.\n",
|
||||||
"line": 43,
|
"line": 28,
|
||||||
"title": "Run with SQLite"
|
"title": "Run with SQLite"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "fiori/package.json",
|
"file": "fiori/package.json",
|
||||||
"description": "#### Deploy the CDS Model to SAP HANA\n\nTo 'activate' SAP HANA through the `production` profile, you can use the global `--production` flag:\n\n>> cd fiori; cds deploy --to hana --production\n\n[Learn more about SAP HANA deployment](https://cap.cloud.sap/docs/guides/databases#get-hana)\n\n#### Run the Application\n\n>> cd fiori; cds watch --production\n\nThe service on [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) is the same as before, but this time the `Authors` entity is backed by a database view with an SAP HANA function.\n\n#### More\n\nIf you don't see data, you can add some in the next step.",
|
"description": "#### Deploy the CDS Model to SAP HANA\n\nTo 'activate' SAP HANA through the `production` profile, you can use the global `--production` flag:\n\n>> cd fiori; cds deploy --to hana --production\n\n[Learn more about SAP HANA deployment](https://cap.cloud.sap/docs/guides/databases#get-hana)\n\n#### Run the Application\n\n>> cd fiori; cds watch --production\n\nThe service on [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) is the same as before, but this time the `Authors` entity is backed by a database view with an SAP HANA function.\n\n#### More\n\nIf you don't see data, you can add some in the next step.",
|
||||||
"line": 46,
|
"line": 31,
|
||||||
"title": "Run with SAP HANA"
|
"title": "Run with SAP HANA"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "fiori/test/requests.http",
|
"file": "fiori/test/requests.http",
|
||||||
"description": "### Add More Data\n\nOptionally you can add some `Authors` data by clicking on the _Send Request_ link (provided by the [REST client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension).",
|
"description": "### Add More Data\n\nOptionally you can add some `Authors` data by clicking on the _Send Request_ link (provided by the [REST client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension).",
|
||||||
"line": 72,
|
"line": 68,
|
||||||
"selection": {
|
"selection": {
|
||||||
"start": {
|
"start": {
|
||||||
"line": 67,
|
"line": 67,
|
||||||
@@ -113,5 +104,6 @@
|
|||||||
"title": "Wrap-up",
|
"title": "Wrap-up",
|
||||||
"description": "### Summary\n\nThat's it! You have seen: \n- How to integrate database-specific functions in a CDS model.\n- How to switch between the two implementations for SQLite and SAP HANA."
|
"description": "### Summary\n\nThat's it! You have seen: \n- How to integrate database-specific functions in a CDS model.\n- How to switch between the two implementations for SQLite and SAP HANA."
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"ref": "master"
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "hello/srv/world.cds",
|
"file": "hello/world.cds",
|
||||||
"description": "### Hello World!\n\nThis is a simplistic [Hello World](https://cap.cloud.sap/docs/get-started/hello-world) service using [CDS](https://cap.cloud.sap/docs/cds/) and [cds.services](https://cap.cloud.sap/docs/node.js/api#services-api).",
|
"description": "### Hello World!\n\nThis is a simplistic [Hello World](https://cap.cloud.sap/docs/get-started/hello-world) service using [CDS](https://cap.cloud.sap/docs/cds/) and [cds.services](https://cap.cloud.sap/docs/node.js/api#services-api).",
|
||||||
"line": 2,
|
"line": 2,
|
||||||
"selection": {
|
"selection": {
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "orders/db/schema.cds",
|
"file": "orders/db/schema.cds",
|
||||||
"description": "### Orders - Compositions and Serving Documents\n\nA standalone orders management service, demonstrating:\n- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with\n- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)\n",
|
"description": "### Compositions and Serving Documents\n\nA standalone orders management service, demonstrating:\n- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with\n- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)\n",
|
||||||
"line": 1,
|
"line": 1,
|
||||||
"selection": {
|
"selection": {
|
||||||
"start": {
|
"start": {
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"file": "reviews/db/schema.cds",
|
"file": "reviews/db/schema.cds",
|
||||||
"description": "### Reviews - More Modularity\n\nShows how to implement a modular service to manage product reviews, including:\n- Consuming other services synchronously and asynchronously\n- Serving requests synchronously\n- Emitting events asynchronously\n- Grow as you go, with:\n- Mocking app services\n- Running service meshes\n- Late-cut Micro Services\n- As well as managed data, input validations, and authorization\n",
|
"description": "### More Modularity\n\nShows how to implement a modular service to manage product reviews, including:\n- Consuming other services synchronously and asynchronously\n- Serving requests synchronously\n- Emitting events asynchronously\n- Grow as you go, with:\n- Mocking app services\n- Running service meshes\n- Late-cut Micro Services\n- As well as managed data, input validations, and authorization\n",
|
||||||
"line": 1,
|
"line": 1,
|
||||||
"selection": {
|
"selection": {
|
||||||
"start": {
|
"start": {
|
||||||
@@ -99,12 +99,8 @@
|
|||||||
"title": "Reviews"
|
"title": "Reviews"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Bookstore",
|
"file": "fiori/app/index.cds",
|
||||||
"description": "### Bookstore - Reuse and UI\n\n- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/reuse-and-compose) these packages:\n - [@capire/bookshop](bookshop)\n - [@capire/reviews](reviews)\n - [@capire/orders](orders)\n - [@capire/common](common)\n- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well\n- [The Vue.js app](reviews/app/vue) imported from reviews is served as well\n- [The Fiori app](orders/app) imported from orders is served as well\n- [OpenAPI export + Swagger UI](https://cap.cloud.sap/docs/advanced/openapi)"
|
"description": "### Annotations for SAP Fiori Elements\n\nA [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:\n - [@capire/bookshop](bookshop)\n - [@capire/reviews](reviews)\n - [@capire/orders](orders)\n - [@capire/common](common)\n\n[Adds a SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookshop, thereby introducing to:\n - [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files\n - Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)\n - Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)\n - Serving SAP Fiori apps locally\n\n[The Vue.js app](bookshop/app/vue) imported from bookshop is served as well.\n",
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "fiori/app/services.cds",
|
|
||||||
"description": "### Annotations for SAP Fiori Elements\n\n- [Adds an SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookstore, thereby introducing to:\n- [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files\n- Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)\n- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)\n- Serving SAP Fiori apps locally\n",
|
|
||||||
"line": 1,
|
"line": 1,
|
||||||
"selection": {
|
"selection": {
|
||||||
"start": {
|
"start": {
|
||||||
@@ -121,13 +117,14 @@
|
|||||||
{
|
{
|
||||||
"file": "package.json",
|
"file": "package.json",
|
||||||
"description": "### All-in-one Monorepo\n\nEach sample sub directory essentially is a standard npm package, some with standard npm dependencies to other samples. The root folder's [package.json](package.json) has local links to the sub folders, such that an `npm install` populates a local `node_modules` folder acts like a local npm registry to the individual sample packages.\n",
|
"description": "### All-in-one Monorepo\n\nEach sample sub directory essentially is a standard npm package, some with standard npm dependencies to other samples. The root folder's [package.json](package.json) has local links to the sub folders, such that an `npm install` populates a local `node_modules` folder acts like a local npm registry to the individual sample packages.\n",
|
||||||
|
"line": 8,
|
||||||
"selection": {
|
"selection": {
|
||||||
"start": {
|
"start": {
|
||||||
"line": 8,
|
"line": 8,
|
||||||
"character": 1
|
"character": 1
|
||||||
},
|
},
|
||||||
"end": {
|
"end": {
|
||||||
"line": 16,
|
"line": 15,
|
||||||
"character": 1
|
"character": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -136,4 +133,4 @@
|
|||||||
],
|
],
|
||||||
"isPrimary": true,
|
"isPrimary": true,
|
||||||
"description": "Overview of CAP Samples for Node.js"
|
"description": "Overview of CAP Samples for Node.js"
|
||||||
}
|
}
|
||||||
|
|||||||
34
README.md
34
README.md
@@ -17,7 +17,7 @@ Find here a collection of samples for the [SAP Cloud Application Programming Mod
|
|||||||
|
|
||||||
### Download
|
### Download
|
||||||
|
|
||||||
If you've [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, otherwise [download as ZIP file](archive/main.zip).
|
If you've [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, otherwise [download as ZIP file](archive/master.zip).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/sap-samples/cloud-cap-samples samples
|
git clone https://github.com/sap-samples/cloud-cap-samples samples
|
||||||
@@ -84,3 +84,35 @@ In case you've a question, find a bug, or otherwise need support, use our [commu
|
|||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (c) 2021 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](LICENSE.txt) file.
|
Copyright (c) 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.
|
||||||
|
|
||||||
|
# Suppliers - in progress for Messaging & Service Consumption -
|
||||||
|
|
||||||
|
## TODOs
|
||||||
|
|
||||||
|
1. Fix issues when running in same process
|
||||||
|
2. Automated tests
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Run:
|
||||||
|
|
||||||
|
```
|
||||||
|
CDS_ENV=local-hybrid cds mock API_BUSINESS_PARTNER -p 5001
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Wait until startup is completed
|
||||||
|
3. Run in a 2nd terminal:
|
||||||
|
|
||||||
|
```
|
||||||
|
CDS_ENV=local-hybrid cds serve all --with-mocks --in-memory
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Now, you can issues the requests listed in `suppliers/requests.http`
|
||||||
|
|
||||||
|
## Request Sequence
|
||||||
|
|
||||||
|
* TODO
|
||||||
|
## URLs
|
||||||
|
|
||||||
|
* Get books with their replicated supplier: http://localhost:4004/browse/Books?$expand=supplier
|
||||||
|
* Get remote suppliers: http://localhost:4004/admin/Suppliers?$top=11
|
||||||
@@ -10,7 +10,7 @@ const books = new Vue ({
|
|||||||
data: {
|
data: {
|
||||||
list: [],
|
list: [],
|
||||||
book: undefined,
|
book: undefined,
|
||||||
order: { quantity:1, succeeded:'', failed:'' }
|
order: { amount:1, succeeded:'', failed:'' }
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -26,18 +26,18 @@ const books = new Vue ({
|
|||||||
const book = books.book = books.list [eve.currentTarget.rowIndex-1]
|
const book = books.book = books.list [eve.currentTarget.rowIndex-1]
|
||||||
const res = await GET(`/Books/${book.ID}?$select=descr,stock,image`)
|
const res = await GET(`/Books/${book.ID}?$select=descr,stock,image`)
|
||||||
Object.assign (book, res.data)
|
Object.assign (book, res.data)
|
||||||
books.order = { quantity:1 }
|
books.order = { amount:1 }
|
||||||
setTimeout (()=> $('form > input').focus(), 111)
|
setTimeout (()=> $('form > input').focus(), 111)
|
||||||
},
|
},
|
||||||
|
|
||||||
async submitOrder () {
|
async submitOrder () {
|
||||||
const {book,order} = books, quantity = parseInt (order.quantity) || 1 // REVISIT: Okra should be less strict
|
const {book,order} = books, amount = parseInt (order.amount) || 1 // REVISIT: Okra should be less strict
|
||||||
try {
|
try {
|
||||||
const res = await POST(`/submitOrder`, { quantity, book: book.ID })
|
const res = await POST(`/submitOrder`, { amount, book: book.ID })
|
||||||
book.stock = res.data.stock
|
book.stock = res.data.stock
|
||||||
books.order = { quantity, succeeded: `Successfully ordered ${quantity} item(s).` }
|
books.order = { amount, succeeded: `Successfully ordered ${amount} item(s).` }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
books.order = { quantity, failed: e.response.data.error.message }
|
books.order = { amount, failed: e.response.data.error.message }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<td>{{ book.author }}</td>
|
<td>{{ book.author }}</td>
|
||||||
<td>{{ book.genre.name }}</td>
|
<td>{{ book.genre.name }}</td>
|
||||||
<td class="rating-stars">
|
<td class="rating-stars">
|
||||||
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }} ({{ book.numberOfReviews }})
|
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
|
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
{{ book.stock }} in stock
|
{{ book.stock }} in stock
|
||||||
</label>
|
</label>
|
||||||
<form @submit.prevent="submitOrder" style="float:right; display:flex; flex-direction:row-reverse">
|
<form @submit.prevent="submitOrder" style="float:right; display:flex; flex-direction:row-reverse">
|
||||||
<input type="number" v-model="order.quantity" v-bind:class="{ failed: order.failed }" style="width:5em">
|
<input type="number" v-model="order.amount" v-bind:class="{ failed: order.failed }" style="width:5em">
|
||||||
<input type="submit" value="Order:" class="muted-button">
|
<input type="submit" value="Order:" class="muted-button">
|
||||||
</form>
|
</form>
|
||||||
<h4> {{ book.title }} </h4>
|
<h4> {{ book.title }} </h4>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using { Currency, managed, sap, extensible } from '@sap/cds/common';
|
using { Currency, managed, sap } from '@sap/cds/common';
|
||||||
namespace sap.capire.bookshop;
|
namespace sap.capire.bookshop;
|
||||||
|
|
||||||
entity Books : managed, extensible {
|
entity Books : managed {
|
||||||
key ID : Integer;
|
key ID : Integer;
|
||||||
title : localized String(111);
|
title : localized String(111);
|
||||||
descr : localized String(1111);
|
descr : localized String(1111);
|
||||||
@@ -13,7 +13,7 @@ entity Books : managed, extensible {
|
|||||||
image : LargeBinary @Core.MediaType : 'image/png';
|
image : LargeBinary @Core.MediaType : 'image/png';
|
||||||
}
|
}
|
||||||
|
|
||||||
entity Authors : managed, extensible {
|
entity Authors : managed {
|
||||||
key ID : Integer;
|
key ID : Integer;
|
||||||
name : String(111);
|
name : String(111);
|
||||||
dateOfBirth : Date;
|
dateOfBirth : Date;
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
const { CatalogService } = require('./srv/cat-service')
|
exports.CatalogService = require('./srv/cat-service')
|
||||||
module.exports = { CatalogService }
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ using { sap.capire.bookshop as my } from '../db/schema';
|
|||||||
service CatalogService @(path:'/browse') {
|
service CatalogService @(path:'/browse') {
|
||||||
|
|
||||||
/** For displaying lists of Books */
|
/** For displaying lists of Books */
|
||||||
@readonly entity ListOfBooks as projection on Books
|
@readonly entity ListOfBooks as projection on Books {
|
||||||
excluding { descr };
|
ID, title, author, genre, price, currency
|
||||||
|
}
|
||||||
|
|
||||||
/** For display in details pages */
|
/** For display in details pages */
|
||||||
@readonly entity Books as projection on my.Books { *,
|
@readonly entity Books as projection on my.Books { *,
|
||||||
@@ -11,6 +12,6 @@ service CatalogService @(path:'/browse') {
|
|||||||
} excluding { createdBy, modifiedBy };
|
} excluding { createdBy, modifiedBy };
|
||||||
|
|
||||||
@requires: 'authenticated-user'
|
@requires: 'authenticated-user'
|
||||||
action submitOrder ( book: Books:ID, quantity: Integer ) returns { stock: Integer };
|
action submitOrder ( book: Books:ID, amount: Integer ) returns { stock: Integer };
|
||||||
event OrderedBook : { book: Books:ID; quantity: Integer; buyer: String };
|
event OrderedBook : { book: Books:ID; amount: Integer; buyer: String };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
const cds = require('@sap/cds')
|
const cds = require('@sap/cds')
|
||||||
|
const { Books } = cds.entities ('sap.capire.bookshop')
|
||||||
|
|
||||||
class CatalogService extends cds.ApplicationService { init(){
|
class CatalogService extends cds.ApplicationService { init(){
|
||||||
|
|
||||||
const { Books } = cds.entities ('sap.capire.bookshop')
|
|
||||||
|
|
||||||
// Reduce stock of ordered books if available stock suffices
|
// Reduce stock of ordered books if available stock suffices
|
||||||
this.on ('submitOrder', async req => {
|
this.on ('submitOrder', async req => {
|
||||||
const {book,quantity} = req.data
|
const {book,amount} = req.data
|
||||||
if (quantity < 1) return req.reject (400,`quantity has to be 1 or more`)
|
|
||||||
let {stock} = await SELECT `stock` .from (Books,book)
|
let {stock} = await SELECT `stock` .from (Books,book)
|
||||||
if (quantity > stock) return req.reject (409,`${quantity} exceeds stock for book #${book}`)
|
if (stock >= amount) {
|
||||||
await UPDATE (Books,book) .with ({ stock: stock -= quantity })
|
await UPDATE (Books,book) .with (`stock -=`, amount)
|
||||||
await this.emit ('OrderedBook', { book, quantity, buyer:req.user.id })
|
await this.emit ('OrderedBook', { book, amount, buyer:req.user.id })
|
||||||
return { stock }
|
return { stock }
|
||||||
|
}
|
||||||
|
else return req.error (409,`${amount} exceeds stock for book #${book}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add some discount for overstocked books
|
// Add some discount for overstocked books
|
||||||
|
|||||||
@@ -32,19 +32,6 @@ GET {{server}}/admin/Authors?
|
|||||||
# &sap-language=de
|
# &sap-language=de
|
||||||
Authorization: Basic alice:
|
Authorization: Basic alice:
|
||||||
|
|
||||||
### ------------------------------------------------------------------------
|
|
||||||
# Create Author
|
|
||||||
POST {{server}}/admin/Authors
|
|
||||||
Content-Type: application/json;IEEE754Compatible=true
|
|
||||||
Authorization: Basic alice:
|
|
||||||
|
|
||||||
{
|
|
||||||
"ID": 112,
|
|
||||||
"name": "Shakespeeeeere",
|
|
||||||
"age": 22
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
### ------------------------------------------------------------------------
|
### ------------------------------------------------------------------------
|
||||||
# Create book
|
# Create book
|
||||||
POST {{server}}/admin/Books
|
POST {{server}}/admin/Books
|
||||||
@@ -84,7 +71,7 @@ POST {{server}}/browse/submitOrder
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
{{me}}
|
{{me}}
|
||||||
|
|
||||||
{ "book":201, "quantity":5 }
|
{ "book":201, "amount":5 }
|
||||||
|
|
||||||
|
|
||||||
### ------------------------------------------------------------------------
|
### ------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
namespace sap.capire.bookshop; //> important for reflection
|
|
||||||
using from './srv/mashup';
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@capire/bookstore",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@capire/bookshop": "*",
|
|
||||||
"@capire/reviews": "*",
|
|
||||||
"@capire/orders": "*",
|
|
||||||
"@capire/common": "*",
|
|
||||||
"@sap/cds": "^5",
|
|
||||||
"express": "^4.17.1"
|
|
||||||
},
|
|
||||||
"cds": {
|
|
||||||
"requires": {
|
|
||||||
"ReviewsService": {
|
|
||||||
"kind": "odata",
|
|
||||||
"model": "@capire/reviews"
|
|
||||||
},
|
|
||||||
"OrdersService": {
|
|
||||||
"kind": "odata",
|
|
||||||
"model": "@capire/orders"
|
|
||||||
},
|
|
||||||
"messaging": {
|
|
||||||
"[development]": { "kind": "file-based-messaging" },
|
|
||||||
"[hybrid]": { "kind": "enterprise-messaging-shared" },
|
|
||||||
"[production]": { "kind": "enterprise-messaging" }
|
|
||||||
},
|
|
||||||
"db": {
|
|
||||||
"kind": "sql"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"log": { "service": true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
const cds = require ('@sap/cds')
|
|
||||||
|
|
||||||
// Add mashup logic
|
|
||||||
cds.once('served', require('./srv/mashup'))
|
|
||||||
|
|
||||||
// Add routes to UIs from imported packages
|
|
||||||
cds.once('bootstrap',(app)=>{
|
|
||||||
app.serve ('/bookshop') .from ('@capire/bookshop','app/vue')
|
|
||||||
app.serve ('/reviews') .from ('@capire/reviews','app/vue')
|
|
||||||
app.serve ('/orders') .from('@capire/orders','app/orders')
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add Swagger UI
|
|
||||||
require('./srv/swagger-ui')
|
|
||||||
|
|
||||||
// Returning cds.server
|
|
||||||
module.exports = cds.server
|
|
||||||
|
|
||||||
// For didactic reasons in capire
|
|
||||||
const { ReviewsService, OrdersService } = cds.requires
|
|
||||||
if (!ReviewsService.credentials && !OrdersService.credentials) cds.requires.messaging = false
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// 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';
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// Adding Swagger UI - see https://cap.cloud.sap/docs/advanced/openapi
|
|
||||||
const cds = require ('@sap/cds')
|
|
||||||
try {
|
|
||||||
const cds_swagger = require ('cds-swagger-ui-express')
|
|
||||||
cds.once ('bootstrap', app => app.use (cds_swagger()) )
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code !== 'MODULE_NOT_FOUND') throw err
|
|
||||||
}
|
|
||||||
2
fiori/.env
Normal file
2
fiori/.env
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
cds.requires.messaging.kind = file-based-messaging
|
||||||
|
PORT = 4004
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
using {AdminService} from '@capire/bookshop';
|
|
||||||
|
|
||||||
annotate AdminService.Authors with @odata.draft.enabled;
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Authors Object Page
|
|
||||||
//
|
|
||||||
annotate AdminService.Authors with @(UI : {
|
|
||||||
HeaderInfo : {
|
|
||||||
TypeName : 'Author',
|
|
||||||
TypeNamePlural : 'Authors',
|
|
||||||
Description : {Value : lifetime}
|
|
||||||
},
|
|
||||||
Facets : [
|
|
||||||
{
|
|
||||||
$Type : 'UI.ReferenceFacet',
|
|
||||||
Label : '{i18n>Details}',
|
|
||||||
Target : '@UI.FieldGroup#Details'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$Type : 'UI.ReferenceFacet',
|
|
||||||
Label : '{i18n>Books}',
|
|
||||||
Target : 'books/@UI.LineItem'
|
|
||||||
},
|
|
||||||
],
|
|
||||||
FieldGroup #Details : {Data : [
|
|
||||||
{Value : placeOfBirth},
|
|
||||||
{Value : placeOfDeath},
|
|
||||||
{Value : dateOfBirth},
|
|
||||||
{Value : dateOfDeath},
|
|
||||||
{
|
|
||||||
Value : age,
|
|
||||||
Label : '{i18n>Age}'
|
|
||||||
},
|
|
||||||
]},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Workaround to avoid errors for unknown db-specific calculated fields above
|
|
||||||
extend sap.capire.bookshop.Authors with {
|
|
||||||
virtual age : Integer;
|
|
||||||
virtual lifetime : String;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Workaround for Fiori popup for asking user to enter a new UUID on Create
|
|
||||||
annotate AdminService.Authors with { ID @Core.Computed; }
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
sap.ui.define(["sap/fe/core/AppComponent"], function (AppComponent) {
|
|
||||||
"use strict";
|
|
||||||
return AppComponent.extend("authors.Component", {
|
|
||||||
metadata: { manifest: "json" },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
/* eslint no-undef:0 */
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
{
|
|
||||||
"_version": "1.28.0",
|
|
||||||
"sap.app": {
|
|
||||||
"id": "authors",
|
|
||||||
"type": "application",
|
|
||||||
"title": "Manage Authors",
|
|
||||||
"description": "Sample Application",
|
|
||||||
"i18n": "i18n/i18n.properties",
|
|
||||||
"applicationVersion": {
|
|
||||||
"version": "1.0.0"
|
|
||||||
},
|
|
||||||
"dataSources": {
|
|
||||||
"AdminService": {
|
|
||||||
"uri": "/admin/",
|
|
||||||
"type": "OData",
|
|
||||||
"settings": {
|
|
||||||
"odataVersion": "4.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sourceTemplate": {
|
|
||||||
"id": "ui5template.basicSAPUI5ApplicationProject",
|
|
||||||
"-id": "ui5template.smartTemplate",
|
|
||||||
"version": "1.40.12"
|
|
||||||
},
|
|
||||||
"crossNavigation": {
|
|
||||||
"inbounds": {
|
|
||||||
"intent1": {
|
|
||||||
"signature": {
|
|
||||||
"parameters": {
|
|
||||||
"Books.author.ID":{
|
|
||||||
"renameTo": "ID"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalParameters": "ignored"
|
|
||||||
},
|
|
||||||
"semanticObject": "Authors",
|
|
||||||
"action": "display",
|
|
||||||
"title": "{{appTitle}}",
|
|
||||||
"info": "{{appInfo}}",
|
|
||||||
"subTitle": "{{appSubTitle}}",
|
|
||||||
"icon": "sap-icon://SAP-icons-TNT/user",
|
|
||||||
"indicatorDataSource": {
|
|
||||||
"dataSource": "AdminService",
|
|
||||||
"path": "Authors/$count",
|
|
||||||
"refresh": 1800
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sap.ui5": {
|
|
||||||
"flexEnabled": true,
|
|
||||||
"config": {
|
|
||||||
"experimentalCAPScenario": true
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"minUI5Version": "1.81.0",
|
|
||||||
"libs": {
|
|
||||||
"sap.fe.templates": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"models": {
|
|
||||||
"i18n": {
|
|
||||||
"type": "sap.ui.model.resource.ResourceModel",
|
|
||||||
"uri": "i18n/i18n.properties"
|
|
||||||
},
|
|
||||||
"": {
|
|
||||||
"dataSource": "AdminService",
|
|
||||||
"settings": {
|
|
||||||
"synchronizationMode": "None",
|
|
||||||
"operationMode": "Server",
|
|
||||||
"autoExpandSelect": true,
|
|
||||||
"earlyRequests": true,
|
|
||||||
"groupProperties": {
|
|
||||||
"default": {
|
|
||||||
"submit": "Auto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"routing": {
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"pattern": ":?query:",
|
|
||||||
"name": "AuthorsList",
|
|
||||||
"target": "AuthorsList"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pattern": "Authors({key}):?query:",
|
|
||||||
"name": "AuthorsDetails",
|
|
||||||
"target": "AuthorsDetails"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"targets": {
|
|
||||||
"AuthorsList": {
|
|
||||||
"type": "Component",
|
|
||||||
"id": "AuthorsList",
|
|
||||||
"name": "sap.fe.templates.ListReport",
|
|
||||||
"options": {
|
|
||||||
"settings": {
|
|
||||||
"entitySet": "Authors",
|
|
||||||
"initialLoad": true,
|
|
||||||
"navigation": {
|
|
||||||
"Authors": {
|
|
||||||
"detail": {
|
|
||||||
"route": "AuthorsDetails"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AuthorsDetails": {
|
|
||||||
"type": "Component",
|
|
||||||
"id": "AuthorsDetailsList",
|
|
||||||
"name": "sap.fe.templates.ObjectPage",
|
|
||||||
"options": {
|
|
||||||
"settings": {
|
|
||||||
"entitySet": "Authors"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"contentDensities": {
|
|
||||||
"compact": true,
|
|
||||||
"cozy": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sap.ui": {
|
|
||||||
"technology": "UI5",
|
|
||||||
"fullWidth": false,
|
|
||||||
"deviceTypes":{
|
|
||||||
"desktop": true,
|
|
||||||
"tablet": true,
|
|
||||||
"phone": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sap.fiori": {
|
|
||||||
"registrationIds": [],
|
|
||||||
"archeType": "transactional"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,21 +10,32 @@
|
|||||||
<script>
|
<script>
|
||||||
window["sap-ushell-config"] = {
|
window["sap-ushell-config"] = {
|
||||||
defaultRenderer: "fiori2",
|
defaultRenderer: "fiori2",
|
||||||
applications: {},
|
applications: {
|
||||||
bootstrapPlugins: {
|
"browse-books": {
|
||||||
RuntimeAuthoringPlugin: {
|
title: "Browse Books",
|
||||||
component: "sap.ushell.plugins.rta",
|
description: "w/ SAP Fiori Elements",
|
||||||
config: {
|
additionalInformation: "SAPUI5.Component=bookshop",
|
||||||
validateAppVersion: false,
|
applicationType : "URL",
|
||||||
},
|
url: "/browse/webapp",
|
||||||
},
|
navigationMode: "embedded"
|
||||||
PersonalizePlugin: {
|
},
|
||||||
component: "sap.ushell.plugins.rta-personalize",
|
"manage-books": {
|
||||||
config: {
|
title: "Manage Books",
|
||||||
validateAppVersion: false,
|
description: "w/ SAP Fiori Elements",
|
||||||
},
|
additionalInformation: "SAPUI5.Component=admin",
|
||||||
},
|
applicationType : "URL",
|
||||||
}
|
url: "/admin/webapp",
|
||||||
|
navigationMode: "embedded"
|
||||||
|
},
|
||||||
|
"manage-orders": {
|
||||||
|
title: "Manage Orders",
|
||||||
|
description: "w/ SAP Fiori Elements",
|
||||||
|
additionalInformation: "SAPUI5.Component=orders",
|
||||||
|
applicationType : "URL",
|
||||||
|
url: "/orders/webapp",
|
||||||
|
navigationMode: "embedded"
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -36,13 +47,8 @@
|
|||||||
data-sap-ui-frameOptions="allow"
|
data-sap-ui-frameOptions="allow"
|
||||||
></script>
|
></script>
|
||||||
<script>
|
<script>
|
||||||
sap.ui.getCore().attachInit(()=> sap.ushell.Container.createRenderer().placeAt("content"));
|
sap.ui.getCore().attachInit(()=> sap.ushell.Container.createRenderer().placeAt("content"))
|
||||||
sap.ui
|
</script>
|
||||||
.getCore()
|
|
||||||
.getConfiguration()
|
|
||||||
.setFlexibilityServices([{ connector: "SessionStorageConnector" }]);
|
|
||||||
sap.ui.getCore().getConfiguration().setLanguage("en");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body class="sapUiBody" id="content"></body>
|
<body class="sapUiBody" id="content"></body>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using { AdminService } from '@capire/bookstore';
|
using { AdminService, sap.capire.bookshop } from '../../db/schema';
|
||||||
using from '../common'; // to help UI linter get the complete annotations
|
using from '../common'; // to help UI linter get the complete annotations
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -40,6 +40,27 @@ annotate AdminService.Books with @(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
annotate AdminService.Authors with @(
|
||||||
|
UI: {
|
||||||
|
HeaderInfo: {
|
||||||
|
Description: {Value: lifetime}
|
||||||
|
},
|
||||||
|
Facets: [
|
||||||
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
|
||||||
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Books}', Target: 'books/@UI.LineItem'},
|
||||||
|
],
|
||||||
|
FieldGroup#Details: {
|
||||||
|
Data: [
|
||||||
|
{Value: placeOfBirth},
|
||||||
|
{Value: placeOfDeath},
|
||||||
|
{Value: dateOfBirth},
|
||||||
|
{Value: dateOfDeath},
|
||||||
|
{Value: age, Label: '{i18n>Age}'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
@@ -64,14 +85,11 @@ annotate AdminService.Books.texts with @(
|
|||||||
|
|
||||||
// Add Value Help for Locales
|
// Add Value Help for Locales
|
||||||
annotate AdminService.Books.texts {
|
annotate AdminService.Books.texts {
|
||||||
locale @ValueList:{entity:'Languages'};
|
locale @ValueList:{entity:'Languages',type:#fixed}
|
||||||
locale @Common.ValueListWithFixedValues:true; //show as drop down, not a dialog
|
|
||||||
}
|
}
|
||||||
// In addition we need to expose Languages through AdminService as a target for ValueList
|
// In addition we need to expose Languages and Books.texts through AdminService
|
||||||
using { sap } from '@sap/cds/common';
|
using { sap } from '@sap/cds/common';
|
||||||
extend service AdminService {
|
extend service AdminService {
|
||||||
@readonly entity Languages as projection on sap.common.Languages;
|
entity Languages as projection on sap.common.Languages;
|
||||||
|
entity Books.texts as projection on bookshop.Books.texts;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workaround for Fiori popup for asking user to enter a new UUID on Create
|
|
||||||
annotate AdminService.Books with { ID @Core.Computed; }
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
|
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
|
||||||
"use strict";
|
"use strict";
|
||||||
return AppComponent.extend("books.Component", {
|
return AppComponent.extend("admin.Component", {
|
||||||
metadata: { manifest: "json" }
|
metadata: { manifest: "json" }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_version": "1.8.0",
|
"_version": "1.8.0",
|
||||||
"sap.app": {
|
"sap.app": {
|
||||||
"id": "books",
|
"id": "admin",
|
||||||
"type": "application",
|
"type": "application",
|
||||||
"title": "Manage Books",
|
"title": "Manage Books",
|
||||||
"description": "Sample Application",
|
"description": "Sample Application",
|
||||||
@@ -22,10 +22,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sap.ui5": {
|
"sap.ui5": {
|
||||||
"flexEnabled": true,
|
|
||||||
"config": {
|
|
||||||
"experimentalCAPScenario": true
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"libs": {
|
"libs": {
|
||||||
"sap.fe.templates": {}
|
"sap.fe.templates": {}
|
||||||
@@ -77,7 +73,6 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"settings" : {
|
"settings" : {
|
||||||
"entitySet" : "Books",
|
"entitySet" : "Books",
|
||||||
"initialLoad": true,
|
|
||||||
"navigation" : {
|
"navigation" : {
|
||||||
"Books" : {
|
"Books" : {
|
||||||
"detail" : {
|
"detail" : {
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
{
|
|
||||||
"services": {
|
|
||||||
"LaunchPage": {
|
|
||||||
"adapter": {
|
|
||||||
"config": {
|
|
||||||
"catalogs": [],
|
|
||||||
"groups": [
|
|
||||||
{
|
|
||||||
"id": "Bookshop",
|
|
||||||
"title": "Bookshop",
|
|
||||||
"isPreset": true,
|
|
||||||
"isVisible": true,
|
|
||||||
"isGroupLocked": false,
|
|
||||||
"tiles": [
|
|
||||||
{
|
|
||||||
"id": "BrowseBooks",
|
|
||||||
"tileType": "sap.ushell.ui.tile.StaticTile",
|
|
||||||
"properties": {
|
|
||||||
"title": "Browse Books",
|
|
||||||
"targetURL": "#Books-display"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "Administration",
|
|
||||||
"title": "Administration",
|
|
||||||
"isPreset": true,
|
|
||||||
"isVisible": true,
|
|
||||||
"isGroupLocked": false,
|
|
||||||
"tiles": [
|
|
||||||
{
|
|
||||||
"id": "ManageBooks",
|
|
||||||
"tileType": "sap.ushell.ui.tile.StaticTile",
|
|
||||||
"properties": {
|
|
||||||
"title": "Manage Books",
|
|
||||||
"targetURL": "#Books-manage"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ManageAuthors",
|
|
||||||
"tileType": "sap.ushell.ui.tile.StaticTile",
|
|
||||||
"properties": {
|
|
||||||
"title": "Manage Authors",
|
|
||||||
"targetURL": "#Authors-display"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ManageOrders",
|
|
||||||
"tileType": "sap.ushell.ui.tile.StaticTile",
|
|
||||||
"properties": {
|
|
||||||
"title": "Manage Orders",
|
|
||||||
"targetURL": "#Orders-manage"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"NavTargetResolution": {
|
|
||||||
"config": {
|
|
||||||
"enableClientSideTargetResolution": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ClientSideTargetResolution": {
|
|
||||||
"adapter": {
|
|
||||||
"config": {
|
|
||||||
"inbounds": {
|
|
||||||
"BrowseBooks": {
|
|
||||||
"semanticObject": "Books",
|
|
||||||
"action": "display",
|
|
||||||
"title": "Browse Books",
|
|
||||||
"signature": {
|
|
||||||
"parameters": {
|
|
||||||
"Books.ID": {
|
|
||||||
"renameTo": "ID"
|
|
||||||
},
|
|
||||||
"Authors.books.ID": {
|
|
||||||
"renameTo": "ID"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalParameters": "ignored"
|
|
||||||
},
|
|
||||||
"resolutionResult": {
|
|
||||||
"applicationType": "SAPUI5",
|
|
||||||
"additionalInformation": "SAPUI5.Component=bookshop",
|
|
||||||
"url": "/browse/webapp"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"BrowseAuthors": {
|
|
||||||
"semanticObject": "Authors",
|
|
||||||
"action": "display",
|
|
||||||
"title": "Browse Authors",
|
|
||||||
"signature": {
|
|
||||||
"parameters": {
|
|
||||||
"Books.author.ID":{
|
|
||||||
"renameTo": "ID"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalParameters": "ignored"
|
|
||||||
},
|
|
||||||
"resolutionResult": {
|
|
||||||
"applicationType": "SAPUI5",
|
|
||||||
"additionalInformation": "SAPUI5.Component=authors",
|
|
||||||
"url": "/admin-authors/webapp"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ManageBooks": {
|
|
||||||
"semanticObject": "Books",
|
|
||||||
"action": "manage",
|
|
||||||
"title": "Manage Books",
|
|
||||||
"signature": {
|
|
||||||
"parameters": {},
|
|
||||||
"additionalParameters": "allowed"
|
|
||||||
},
|
|
||||||
"resolutionResult": {
|
|
||||||
"applicationType": "SAPUI5",
|
|
||||||
"additionalInformation": "SAPUI5.Component=books",
|
|
||||||
"url": "/admin-books/webapp"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ManageOrders": {
|
|
||||||
"semanticObject": "Orders",
|
|
||||||
"action": "manage",
|
|
||||||
"signature": {
|
|
||||||
"parameters": {},
|
|
||||||
"additionalParameters": "allowed"
|
|
||||||
},
|
|
||||||
"resolutionResult": {
|
|
||||||
"applicationType": "SAPUI5",
|
|
||||||
"additionalInformation": "SAPUI5.Component=orders",
|
|
||||||
"url": "/orders/webapp"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +1,50 @@
|
|||||||
using CatalogService from '@capire/bookstore';
|
using CatalogService from '@capire/bookshop';
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Books Object Page
|
// Books Object Page
|
||||||
//
|
//
|
||||||
annotate CatalogService.Books with @(UI : {
|
annotate CatalogService.Books with @(
|
||||||
HeaderInfo : {
|
UI: {
|
||||||
TypeName : 'Book',
|
HeaderInfo: {
|
||||||
TypeNamePlural : 'Books',
|
TypeName: 'Book',
|
||||||
Description : {Value : author}
|
TypeNamePlural: 'Books',
|
||||||
},
|
Description: {Value: author}
|
||||||
HeaderFacets : [{
|
},
|
||||||
$Type : 'UI.ReferenceFacet',
|
HeaderFacets: [
|
||||||
Label : '{i18n>Description}',
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Description}', Target: '@UI.FieldGroup#Descr'},
|
||||||
Target : '@UI.FieldGroup#Descr'
|
],
|
||||||
}, ],
|
Facets: [
|
||||||
Facets : [{
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Price'},
|
||||||
$Type : 'UI.ReferenceFacet',
|
],
|
||||||
Label : '{i18n>Details}',
|
FieldGroup#Descr: {
|
||||||
Target : '@UI.FieldGroup#Price'
|
Data: [
|
||||||
}, ],
|
{Value: descr},
|
||||||
FieldGroup #Descr : {Data : [{Value : descr}, ]},
|
]
|
||||||
FieldGroup #Price : {Data : [
|
},
|
||||||
{Value : price},
|
FieldGroup#Price: {
|
||||||
{
|
Data: [
|
||||||
Value : currency.symbol,
|
{Value: price},
|
||||||
Label : '{i18n>Currency}'
|
{Value: currency.symbol, Label: '{i18n>Currency}'},
|
||||||
},
|
]
|
||||||
]},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Books Object Page
|
// Books Object Page
|
||||||
//
|
//
|
||||||
annotate CatalogService.Books with @(UI : {
|
annotate CatalogService.Books with @(
|
||||||
SelectionFields : [
|
UI: {
|
||||||
ID,
|
SelectionFields: [ ID, price, currency_code ],
|
||||||
price,
|
LineItem: [
|
||||||
currency_code
|
{Value: title},
|
||||||
],
|
{Value: author, Label:'{i18n>Author}'},
|
||||||
LineItem : [
|
{Value: genre.name},
|
||||||
{
|
{Value: price},
|
||||||
Value : ID,
|
{Value: currency.symbol, Label:' '},
|
||||||
Label : '{i18n>Title}'
|
]
|
||||||
},
|
},
|
||||||
{
|
);
|
||||||
Value : author,
|
|
||||||
Label : '{i18n>Author}'
|
|
||||||
},
|
|
||||||
{Value : genre.name},
|
|
||||||
{Value : price},
|
|
||||||
{
|
|
||||||
Value : currency.symbol,
|
|
||||||
Label : ' '
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}, );
|
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
{
|
{
|
||||||
"_version": "1.28.0",
|
"_version": "1.8.0",
|
||||||
"sap.app": {
|
"sap.app": {
|
||||||
"id": "bookshop",
|
"id": "bookshop",
|
||||||
"type": "application",
|
"type": "application",
|
||||||
"title": "Browse Books",
|
"title": "Browse Books",
|
||||||
"description": "Sample Application",
|
"description": "Sample Application",
|
||||||
"i18n": "i18n/i18n.properties",
|
"i18n": "i18n/i18n.properties",
|
||||||
"applicationVersion": {
|
|
||||||
"version": "1.0.0"
|
|
||||||
},
|
|
||||||
"dataSources": {
|
"dataSources": {
|
||||||
"CatalogService": {
|
"CatalogService": {
|
||||||
"uri": "/browse/",
|
"uri": "/browse/",
|
||||||
@@ -18,43 +15,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sourceTemplate": {
|
"-sourceTemplate": {
|
||||||
"id": "ui5template.basicSAPUI5ApplicationProject",
|
"id": "ui5template.basicSAPUI5ApplicationProject",
|
||||||
"-id": "ui5template.smartTemplate",
|
"-id": "ui5template.smartTemplate",
|
||||||
"version": "1.40.12"
|
"-version": "1.40.12"
|
||||||
},
|
|
||||||
"crossNavigation": {
|
|
||||||
"inbounds": {
|
|
||||||
"intent1": {
|
|
||||||
"signature": {
|
|
||||||
"parameters": {
|
|
||||||
"Books.ID":{
|
|
||||||
"renameTo": "ID"
|
|
||||||
},
|
|
||||||
"Authors.books.ID": {
|
|
||||||
"renameTo": "ID"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalParameters": "ignored"
|
|
||||||
},
|
|
||||||
"semanticObject": "Books",
|
|
||||||
"action": "display",
|
|
||||||
"title": "{{appTitle}}",
|
|
||||||
"info": "{{appInfo}}",
|
|
||||||
"subTitle": "{{appSubTitle}}",
|
|
||||||
"icon": "sap-icon://course-book",
|
|
||||||
"indicatorDataSource": {
|
|
||||||
"dataSource": "CatalogService",
|
|
||||||
"path": "Books/$count",
|
|
||||||
"refresh": 1800
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sap.ui5": {
|
"sap.ui5": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minUI5Version": "1.81.0",
|
|
||||||
"libs": {
|
"libs": {
|
||||||
"sap.fe.templates": {}
|
"sap.fe.templates": {}
|
||||||
}
|
}
|
||||||
@@ -100,7 +68,6 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"settings": {
|
"settings": {
|
||||||
"entitySet": "Books",
|
"entitySet": "Books",
|
||||||
"initialLoad": true,
|
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"Books": {
|
"Books": {
|
||||||
"detail": {
|
"detail": {
|
||||||
@@ -130,12 +97,7 @@
|
|||||||
},
|
},
|
||||||
"sap.ui": {
|
"sap.ui": {
|
||||||
"technology": "UI5",
|
"technology": "UI5",
|
||||||
"fullWidth": false,
|
"fullWidth": false
|
||||||
"deviceTypes":{
|
|
||||||
"desktop": true,
|
|
||||||
"tablet": true,
|
|
||||||
"phone": true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"sap.fiori": {
|
"sap.fiori": {
|
||||||
"registrationIds": [],
|
"registrationIds": [],
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
Common Annotations shared by all apps
|
Common Annotations shared by all apps
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using { sap.capire.bookshop as my } from '@capire/bookstore';
|
using { sap.capire.bookshop as my } from '@capire/bookshop';
|
||||||
using { sap.common } from '@capire/common';
|
using { sap.common } from '@capire/common';
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -10,52 +10,39 @@ using { sap.common } from '@capire/common';
|
|||||||
// Books Lists
|
// Books Lists
|
||||||
//
|
//
|
||||||
annotate my.Books with @(
|
annotate my.Books with @(
|
||||||
Common.SemanticKey : [ID],
|
Common.SemanticKey: [title],
|
||||||
UI : {
|
UI: {
|
||||||
Identification : [{Value : title}],
|
Identification: [{Value:title}],
|
||||||
SelectionFields : [
|
SelectionFields: [ ID, author_ID, price, currency_code ],
|
||||||
ID,
|
LineItem: [
|
||||||
author_ID,
|
{Value: ID},
|
||||||
price,
|
{Value: title},
|
||||||
currency_code
|
{Value: author.name, Label:'{i18n>Author}'},
|
||||||
],
|
{Value: genre.name},
|
||||||
LineItem : [
|
{Value: stock},
|
||||||
{
|
{Value: price},
|
||||||
Value : ID,
|
{Value: currency.symbol, Label:' '},
|
||||||
Label : '{i18n>Title}'
|
]
|
||||||
},
|
}
|
||||||
{
|
|
||||||
Value : author.ID,
|
|
||||||
Label : '{i18n>Author}'
|
|
||||||
},
|
|
||||||
{Value : genre.name},
|
|
||||||
{Value : stock},
|
|
||||||
{Value : price},
|
|
||||||
{
|
|
||||||
Value : currency.symbol,
|
|
||||||
Label : ' '
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
ID @Common: {
|
author @ValueList.entity:'Authors';
|
||||||
SemanticObject : 'Books',
|
|
||||||
Text: title,
|
|
||||||
TextArrangement : #TextOnly
|
|
||||||
};
|
|
||||||
author @ValueList.entity : 'Authors';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Books Details
|
// Books Details
|
||||||
//
|
//
|
||||||
annotate my.Books with @(UI : {HeaderInfo : {
|
annotate my.Books with @(
|
||||||
TypeName : '{i18n>Book}',
|
UI: {
|
||||||
TypeNamePlural : '{i18n>Books}',
|
HeaderInfo: {
|
||||||
Title : {Value : title},
|
TypeName: '{i18n>Book}',
|
||||||
Description : {Value : author.name}
|
TypeNamePlural: '{i18n>Books}',
|
||||||
}, });
|
Title: {Value: title},
|
||||||
|
Description: {Value: author.name}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -63,19 +50,13 @@ annotate my.Books with @(UI : {HeaderInfo : {
|
|||||||
// Books Elements
|
// Books Elements
|
||||||
//
|
//
|
||||||
annotate my.Books with {
|
annotate my.Books with {
|
||||||
ID @title : '{i18n>ID}';
|
ID @title:'{i18n>ID}' @UI.HiddenFilter;
|
||||||
title @title : '{i18n>Title}';
|
title @title:'{i18n>Title}';
|
||||||
genre @title : '{i18n>Genre}' @Common : {
|
genre @title:'{i18n>Genre}' @Common: { Text: genre.name, TextArrangement: #TextOnly };
|
||||||
Text : genre.name,
|
author @title:'{i18n>Author}' @Common: { Text: author.name, TextArrangement: #TextOnly };
|
||||||
TextArrangement : #TextOnly
|
price @title:'{i18n>Price}' @Measures.ISOCurrency: currency_code;
|
||||||
};
|
stock @title:'{i18n>Stock}';
|
||||||
author @title : '{i18n>Author}' @Common : {
|
descr @UI.MultiLineText;
|
||||||
Text : author.name,
|
|
||||||
TextArrangement : #TextOnly
|
|
||||||
};
|
|
||||||
price @title : '{i18n>Price}' @Measures.ISOCurrency : currency_code;
|
|
||||||
stock @title : '{i18n>Stock}';
|
|
||||||
descr @UI.MultiLineText;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -83,45 +64,42 @@ annotate my.Books with {
|
|||||||
// Genres List
|
// Genres List
|
||||||
//
|
//
|
||||||
annotate my.Genres with @(
|
annotate my.Genres with @(
|
||||||
Common.SemanticKey : [name],
|
Common.SemanticKey: [name],
|
||||||
UI : {
|
UI: {
|
||||||
SelectionFields : [name],
|
SelectionFields: [ name ],
|
||||||
LineItem : [
|
LineItem:[
|
||||||
{Value : name},
|
{Value: name},
|
||||||
{
|
{Value: parent.name, Label: 'Main Genre'},
|
||||||
Value : parent.name,
|
],
|
||||||
Label : 'Main Genre'
|
}
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Genre Details
|
// Genre Details
|
||||||
//
|
//
|
||||||
annotate my.Genres with @(UI : {
|
annotate my.Genres with @(
|
||||||
Identification : [{Value : name}],
|
UI: {
|
||||||
HeaderInfo : {
|
Identification: [{Value:name}],
|
||||||
TypeName : '{i18n>Genre}',
|
HeaderInfo: {
|
||||||
TypeNamePlural : '{i18n>Genres}',
|
TypeName: '{i18n>Genre}',
|
||||||
Title : {Value : name},
|
TypeNamePlural: '{i18n>Genres}',
|
||||||
Description : {Value : ID}
|
Title: {Value: name},
|
||||||
},
|
Description: {Value: ID}
|
||||||
Facets : [{
|
},
|
||||||
$Type : 'UI.ReferenceFacet',
|
Facets: [
|
||||||
Label : '{i18n>SubGenres}',
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>SubGenres}', Target: 'children/@UI.LineItem'},
|
||||||
Target : 'children/@UI.LineItem'
|
],
|
||||||
}, ],
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Genres Elements
|
// Genres Elements
|
||||||
//
|
//
|
||||||
annotate my.Genres with {
|
annotate my.Genres with {
|
||||||
ID @title : '{i18n>ID}';
|
ID @title: '{i18n>ID}';
|
||||||
name @title : '{i18n>Genre}';
|
name @title: '{i18n>Genre}';
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -129,42 +107,38 @@ annotate my.Genres with {
|
|||||||
// Authors List
|
// Authors List
|
||||||
//
|
//
|
||||||
annotate my.Authors with @(
|
annotate my.Authors with @(
|
||||||
Common.SemanticKey : [ID],
|
Common.SemanticKey: [name],
|
||||||
UI : {
|
UI: {
|
||||||
Identification : [{Value : name}],
|
Identification: [{Value:name}],
|
||||||
SelectionFields : [name],
|
SelectionFields: [ name ],
|
||||||
LineItem : [
|
LineItem:[
|
||||||
{Value : ID},
|
{Value: ID},
|
||||||
{Value : dateOfBirth},
|
{Value: name},
|
||||||
{Value : dateOfDeath},
|
{Value: dateOfBirth},
|
||||||
{Value : placeOfBirth},
|
{Value: dateOfDeath},
|
||||||
{Value : placeOfDeath},
|
{Value: placeOfBirth},
|
||||||
],
|
{Value: placeOfDeath},
|
||||||
}
|
],
|
||||||
) {
|
}
|
||||||
ID @Common: {
|
);
|
||||||
SemanticObject : 'Authors',
|
|
||||||
Text: name,
|
|
||||||
TextArrangement : #TextOnly,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Author Details
|
// Author Details
|
||||||
//
|
//
|
||||||
annotate my.Authors with @(UI : {
|
annotate my.Authors with @(
|
||||||
HeaderInfo : {
|
UI: {
|
||||||
TypeName : '{i18n>Author}',
|
HeaderInfo: {
|
||||||
TypeNamePlural : '{i18n>Authors}',
|
TypeName: '{i18n>Author}',
|
||||||
Title : {Value : name},
|
TypeNamePlural: '{i18n>Authors}',
|
||||||
Description : {Value : dateOfBirth}
|
Title: {Value: name},
|
||||||
},
|
Description: {Value: dateOfBirth}
|
||||||
Facets : [{
|
},
|
||||||
$Type : 'UI.ReferenceFacet',
|
Facets: [
|
||||||
Target : 'books/@UI.LineItem'
|
{$Type: 'UI.ReferenceFacet', Target: 'books/@UI.LineItem'},
|
||||||
}, ],
|
],
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -172,12 +146,12 @@ annotate my.Authors with @(UI : {
|
|||||||
// Authors Elements
|
// Authors Elements
|
||||||
//
|
//
|
||||||
annotate my.Authors with {
|
annotate my.Authors with {
|
||||||
ID @title : '{i18n>ID}';
|
ID @title:'{i18n>ID}' @UI.HiddenFilter;
|
||||||
name @title : '{i18n>Name}';
|
name @title:'{i18n>Name}';
|
||||||
dateOfBirth @title : '{i18n>DateOfBirth}';
|
dateOfBirth @title:'{i18n>DateOfBirth}';
|
||||||
dateOfDeath @title : '{i18n>DateOfDeath}';
|
dateOfDeath @title:'{i18n>DateOfDeath}';
|
||||||
placeOfBirth @title : '{i18n>PlaceOfBirth}';
|
placeOfBirth @title:'{i18n>PlaceOfBirth}';
|
||||||
placeOfDeath @title : '{i18n>PlaceOfDeath}';
|
placeOfDeath @title:'{i18n>PlaceOfDeath}';
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -185,105 +159,99 @@ annotate my.Authors with {
|
|||||||
// Languages List
|
// Languages List
|
||||||
//
|
//
|
||||||
annotate common.Languages with @(
|
annotate common.Languages with @(
|
||||||
Common.SemanticKey : [code],
|
Common.SemanticKey: [code],
|
||||||
Identification : [{Value : code}],
|
Identification: [{Value:code}],
|
||||||
UI : {
|
UI: {
|
||||||
SelectionFields : [
|
SelectionFields: [ name, descr ],
|
||||||
name,
|
LineItem:[
|
||||||
descr
|
{Value: code},
|
||||||
],
|
{Value: name},
|
||||||
LineItem : [
|
],
|
||||||
{Value : code},
|
}
|
||||||
{Value : name},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Language Details
|
// Language Details
|
||||||
//
|
//
|
||||||
annotate common.Languages with @(UI : {
|
annotate common.Languages with @(
|
||||||
HeaderInfo : {
|
UI: {
|
||||||
TypeName : '{i18n>Language}',
|
HeaderInfo: {
|
||||||
TypeNamePlural : '{i18n>Languages}',
|
TypeName: '{i18n>Language}',
|
||||||
Title : {Value : name},
|
TypeNamePlural: '{i18n>Languages}',
|
||||||
Description : {Value : descr}
|
Title: {Value: name},
|
||||||
},
|
Description: {Value: descr}
|
||||||
Facets : [{
|
},
|
||||||
$Type : 'UI.ReferenceFacet',
|
Facets: [
|
||||||
Label : '{i18n>Details}',
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
|
||||||
Target : '@UI.FieldGroup#Details'
|
],
|
||||||
}, ],
|
FieldGroup#Details: {
|
||||||
FieldGroup #Details : {Data : [
|
Data: [
|
||||||
{Value : code},
|
{Value: code},
|
||||||
{Value : name},
|
{Value: name},
|
||||||
{Value : descr}
|
{Value: descr}
|
||||||
]},
|
]
|
||||||
});
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Currencies List
|
// Currencies List
|
||||||
//
|
//
|
||||||
annotate common.Currencies with @(
|
annotate common.Currencies with @(
|
||||||
Common.SemanticKey : [code],
|
Common.SemanticKey: [code],
|
||||||
Identification : [{Value : code}],
|
Identification: [{Value:code}],
|
||||||
UI : {
|
UI: {
|
||||||
SelectionFields : [
|
SelectionFields: [ name, descr ],
|
||||||
name,
|
LineItem:[
|
||||||
descr
|
{Value: descr},
|
||||||
],
|
{Value: symbol},
|
||||||
LineItem : [
|
{Value: code},
|
||||||
{Value : descr},
|
],
|
||||||
{Value : symbol},
|
}
|
||||||
{Value : code},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Currency Details
|
// Currency Details
|
||||||
//
|
//
|
||||||
annotate common.Currencies with @(UI : {
|
annotate common.Currencies with @(
|
||||||
HeaderInfo : {
|
UI: {
|
||||||
TypeName : '{i18n>Currency}',
|
HeaderInfo: {
|
||||||
TypeNamePlural : '{i18n>Currencies}',
|
TypeName: '{i18n>Currency}',
|
||||||
Title : {Value : descr},
|
TypeNamePlural: '{i18n>Currencies}',
|
||||||
Description : {Value : code}
|
Title: {Value: descr},
|
||||||
},
|
Description: {Value: code}
|
||||||
Facets : [
|
},
|
||||||
{
|
Facets: [
|
||||||
$Type : 'UI.ReferenceFacet',
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
|
||||||
Label : '{i18n>Details}',
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Extended}', Target: '@UI.FieldGroup#Extended'},
|
||||||
Target : '@UI.FieldGroup#Details'
|
],
|
||||||
},
|
FieldGroup#Details: {
|
||||||
{
|
Data: [
|
||||||
$Type : 'UI.ReferenceFacet',
|
{Value: name},
|
||||||
Label : '{i18n>Extended}',
|
{Value: symbol},
|
||||||
Target : '@UI.FieldGroup#Extended'
|
{Value: code},
|
||||||
},
|
{Value: descr}
|
||||||
],
|
]
|
||||||
FieldGroup #Details : {Data : [
|
},
|
||||||
{Value : name},
|
FieldGroup#Extended: {
|
||||||
{Value : symbol},
|
Data: [
|
||||||
{Value : code},
|
{Value: numcode},
|
||||||
{Value : descr}
|
{Value: minor},
|
||||||
]},
|
{Value: exponent}
|
||||||
FieldGroup #Extended : {Data : [
|
]
|
||||||
{Value : numcode},
|
},
|
||||||
{Value : minor},
|
}
|
||||||
{Value : exponent}
|
);
|
||||||
]},
|
|
||||||
});
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Currencies Elements
|
// Currencies Elements
|
||||||
//
|
//
|
||||||
annotate common.Currencies with {
|
annotate common.Currencies with {
|
||||||
numcode @title : '{i18n>NumCode}';
|
numcode @title:'{i18n>NumCode}';
|
||||||
minor @title : '{i18n>MinorUnit}';
|
minor @title:'{i18n>MinorUnit}';
|
||||||
exponent @title : '{i18n>Exponent}';
|
exponent @title:'{i18n>Exponent}';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
This model controls what gets served to Fiori frontends...
|
This model controls what gets served to Fiori frontends...
|
||||||
*/
|
*/
|
||||||
|
|
||||||
using from './admin-authors/fiori-service';
|
using from './admin/fiori-service';
|
||||||
using from './admin-books/fiori-service';
|
|
||||||
using from './browse/fiori-service';
|
using from './browse/fiori-service';
|
||||||
using from './common';
|
using from './common';
|
||||||
using from '@capire/bookstore/srv/mashup';
|
|
||||||
|
using from '@capire/common';
|
||||||
|
|
||||||
|
// only works in case of embedded orders service
|
||||||
|
using from '@capire/orders/app/orders/fiori-service';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Add Author.age and .lifetime with a DB-specific function
|
// Add Author.age and .lifetime with a DB-specific function
|
||||||
//
|
//
|
||||||
|
|
||||||
using { AdminService } from '@capire/bookshop';
|
using { AdminService } from '../schema';
|
||||||
|
|
||||||
extend projection AdminService.Authors with {
|
extend projection AdminService.Authors with {
|
||||||
YEARS_BETWEEN(dateOfBirth, dateOfDeath) as age: Integer,
|
YEARS_BETWEEN(dateOfBirth, dateOfDeath) as age: Integer,
|
||||||
|
|||||||
8
fiori/db/schema.cds
Normal file
8
fiori/db/schema.cds
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using { sap.capire.bookshop } from '@capire/bookshop';
|
||||||
|
|
||||||
|
// Forward-declare calculated fields to be filled in database-specific ways
|
||||||
|
// TODO find a better way to have 'default' fields that still can be overwritten.
|
||||||
|
extend bookshop.Authors with {
|
||||||
|
virtual age: Integer;
|
||||||
|
virtual lifetime: String;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// Add Author.age and .lifetime with a DB-specific function
|
// Add Author.age and .lifetime with a DB-specific function
|
||||||
//
|
//
|
||||||
|
|
||||||
using { AdminService } from '@capire/bookshop';
|
using { AdminService } from '../schema';
|
||||||
|
|
||||||
extend projection AdminService.Authors with {
|
extend projection AdminService.Authors with {
|
||||||
strftime('%Y',dateOfDeath)-strftime('%Y',dateOfBirth) as age: Integer,
|
strftime('%Y',dateOfDeath)-strftime('%Y',dateOfBirth) as age: Integer,
|
||||||
|
|||||||
@@ -2,8 +2,12 @@
|
|||||||
"name": "@capire/fiori",
|
"name": "@capire/fiori",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capire/bookstore": "*",
|
"@capire/bookshop": "*",
|
||||||
"@sap/cds": "^5",
|
"@capire/common": "*",
|
||||||
|
"@capire/orders": "*",
|
||||||
|
"@capire/reviews": "*",
|
||||||
|
"@capire/suppliers": "*",
|
||||||
|
"@sap/cds": ">=5",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"passport": "^0.4.1"
|
"passport": "^0.4.1"
|
||||||
},
|
},
|
||||||
@@ -12,46 +16,34 @@
|
|||||||
"watch": "cds watch"
|
"watch": "cds watch"
|
||||||
},
|
},
|
||||||
"cds": {
|
"cds": {
|
||||||
|
"hana": {
|
||||||
|
"deploy-format": "hdbtable"
|
||||||
|
},
|
||||||
"requires": {
|
"requires": {
|
||||||
"extensibility": {
|
"API_BUSINESS_PARTNER": {
|
||||||
"kind": "uiflex"
|
"kind": "odata-v2",
|
||||||
|
"model": "@capire/suppliers"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"strategy": "dummy"
|
"strategy": "dummy"
|
||||||
},
|
},
|
||||||
"ReviewsService": {
|
"ReviewsService": {
|
||||||
"kind": "odata",
|
"kind": "odata-v4",
|
||||||
"model": "@capire/reviews"
|
"model": "@capire/reviews"
|
||||||
},
|
},
|
||||||
"OrdersService": {
|
"OrdersService": {
|
||||||
"kind": "odata",
|
"kind": "odata-v4",
|
||||||
"model": "@capire/orders"
|
"model": "@capire/orders"
|
||||||
},
|
},
|
||||||
"messaging": {
|
|
||||||
"[production]": {
|
|
||||||
"kind": "enterprise-messaging"
|
|
||||||
},
|
|
||||||
"[development]": {
|
|
||||||
"kind": "file-based-messaging"
|
|
||||||
},
|
|
||||||
"[hybrid!]": {
|
|
||||||
"kind": "enterprise-messaging-shared"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"db": {
|
"db": {
|
||||||
"kind": "sql"
|
"kind": "sql",
|
||||||
},
|
|
||||||
"db-ext": {
|
|
||||||
"[development]": {
|
"[development]": {
|
||||||
"model": "db/sqlite"
|
"model": "db/sqlite"
|
||||||
},
|
},
|
||||||
"[production]": {
|
"[production]": {
|
||||||
"model": "db/hana"
|
"model": "db/hana"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"hana": {
|
|
||||||
"deploy-format": "hdbtable"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,16 @@
|
|||||||
module.exports = require('@capire/bookstore/server.js')
|
const cds = require ('@sap/cds')
|
||||||
|
module.exports = cds.server
|
||||||
|
|
||||||
|
cds.once('bootstrap',(app)=>{
|
||||||
|
app.serve ('/orders/webapp').from('@capire/orders','app/orders/webapp')
|
||||||
|
app.serve ('/bookshop').from('@capire/bookshop','app/vue')
|
||||||
|
app.serve ('/reviews').from('@capire/reviews','app/vue')
|
||||||
|
})
|
||||||
|
|
||||||
|
cds.once('served', require('./srv/mashup'))
|
||||||
|
cds.once('served', require('@capire/suppliers/srv/mashup'))
|
||||||
|
|
||||||
|
// Swagger UI - see https://cap.cloud.sap/docs/advanced/openapi
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
cds.once ('bootstrap', app => app.use (require ('cds-swagger-ui-express')()) )
|
||||||
|
}
|
||||||
|
|||||||
31
fiori/srv/mashup.cds
Normal file
31
fiori/srv/mashup.cds
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Mashing up imported models...
|
||||||
|
//
|
||||||
|
|
||||||
|
//
|
||||||
|
// Extend Books with access to Reviews and average ratings
|
||||||
|
//
|
||||||
|
|
||||||
|
using { CatalogService.ListOfBooks, sap.capire.bookshop.Books } from '@capire/bookshop';
|
||||||
|
using { ReviewsService.Reviews } from '@capire/reviews';
|
||||||
|
extend Books with {
|
||||||
|
reviews : Composition of many Reviews on reviews.subject = $self.ID;
|
||||||
|
rating : Reviews:rating;
|
||||||
|
}
|
||||||
|
extend projection ListOfBooks with { rating }
|
||||||
|
|
||||||
|
//
|
||||||
|
// Extend Orders with Books as Products
|
||||||
|
//
|
||||||
|
|
||||||
|
using { sap.capire.orders.Orders_Items } from '@capire/orders';
|
||||||
|
extend Orders_Items with {
|
||||||
|
book : Association to Books on product.ID = book.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
using { OrdersService.Orders } from '@capire/orders';
|
||||||
|
// extend service CatalogService with {
|
||||||
|
// entity OrdereredBooks as projection on Orders;
|
||||||
|
// }
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Mashing up bookshop services with required services...
|
// Mashing up provided and required services...
|
||||||
//
|
//
|
||||||
module.exports = async()=>{ // called by server.js
|
module.exports = async()=>{ // called by server.js
|
||||||
|
|
||||||
@@ -20,29 +20,30 @@ module.exports = async()=>{ // called by server.js
|
|||||||
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
|
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
|
||||||
console.debug ('> delegating request to ReviewsService')
|
console.debug ('> delegating request to ReviewsService')
|
||||||
const [id] = req.params, { columns, limit } = req.query.SELECT
|
const [id] = req.params, { columns, limit } = req.query.SELECT
|
||||||
return ReviewsService.read ('Reviews',columns).limit(limit).where({subject:String(id)})
|
return ReviewsService.tx(req).read ('Reviews',columns).limit(limit).where({subject:String(id)})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
//
|
//
|
||||||
// Create an order with the OrdersService when CatalogService signals a new order
|
// Create an order with the OrdersService when CatalogService signals a new order
|
||||||
//
|
//
|
||||||
CatalogService.on ('OrderedBook', async (msg) => {
|
CatalogService.on ('OrderedBook', async (msg) => {
|
||||||
const { book, quantity, buyer } = msg.data
|
const { book, amount, buyer } = msg.data
|
||||||
const { title, price } = await db.tx(msg).read (Books, book, b => { b.title, b.price })
|
const { title, price } = await db.tx(msg).read (Books, book, b => { b.title, b.price })
|
||||||
return OrdersService.tx(msg).create ('Orders').entries({
|
return OrdersService.tx(msg).create ('Orders').entries({
|
||||||
OrderNo: 'Order at '+ (new Date).toLocaleString(),
|
OrderNo: 'Order at '+ (new Date).toLocaleString(),
|
||||||
Items: [{ product:{ID:`${book}`}, title, price, quantity }],
|
Items: [{ product:{ID:`${book}`}, title, price, amount }],
|
||||||
buyer, createdBy: buyer
|
buyer, createdBy: buyer
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
//
|
//
|
||||||
// Update Books' average ratings when ReviewsService signals updated reviews
|
// Update Books' average ratings when ReviewsService signals updatd reviews
|
||||||
//
|
//
|
||||||
ReviewsService.on ('reviewed', (msg) => {
|
ReviewsService.on ('reviewed', (msg) => {
|
||||||
console.debug ('> received:', msg.event, msg.data)
|
console.debug ('> received:', msg.event, msg.data)
|
||||||
const { subject, count, rating } = msg.data
|
const { subject, rating } = msg.data
|
||||||
return UPDATE(Books,subject).with({ numberOfReviews:count, rating })
|
return UPDATE(Books,subject).with({rating})
|
||||||
|
// ^ Note: the framework will execute this and take care for db.tx
|
||||||
})
|
})
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -50,9 +51,9 @@ module.exports = async()=>{ // called by server.js
|
|||||||
//
|
//
|
||||||
OrdersService.on ('OrderChanged', (msg) => {
|
OrdersService.on ('OrderChanged', (msg) => {
|
||||||
console.debug ('> received:', msg.event, msg.data)
|
console.debug ('> received:', msg.event, msg.data)
|
||||||
const { product, deltaQuantity } = msg.data
|
const { product, deltaAmount } = msg.data
|
||||||
return UPDATE (Books) .where ('ID =', product)
|
return UPDATE (Books) .where ('ID =', product)
|
||||||
.and ('stock >=', deltaQuantity)
|
.and ('stock >=', deltaAmount)
|
||||||
.set ('stock -=', deltaQuantity)
|
.set ('stock -=', deltaAmount)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
@bookshop = http://localhost:4004
|
@bookshop = http://localhost:4004
|
||||||
@reviews-service = {{bookshop}}/reviews
|
@reviews-service = {{bookshop}}/reviews
|
||||||
# Uncomment this when running a separate reviews service
|
# Uncomment this when running a separate reviews service
|
||||||
# @reviews-service = http://localhost:4005/reviews
|
@reviews-service = http://localhost:4005/reviews
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Hello World Getting Started Sample
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
- To run the JavaScript implementation, open a new terminal and run `cds watch`.
|
|
||||||
- To run the TypeScript implementation, open a new terminal and run `cds-ts watch`.
|
|
||||||
|
|
||||||
Then call the service at: http://localhost:4004/say/hello(to='world')
|
|
||||||
|
|
||||||
## Learn More
|
|
||||||
|
|
||||||
Learn more about:
|
|
||||||
|
|
||||||
- [Hello World!](https://cap.cloud.sap/docs/get-started/hello-world)
|
|
||||||
- [Using TypeScript](https://cap.cloud.sap/docs/get-started/using-typescript)
|
|
||||||
@@ -3,12 +3,12 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npx jest --silent",
|
"test": "npx jest --silent",
|
||||||
"start": "cds serve srv/world.cds",
|
"watch": "cds serve world.cds",
|
||||||
"start:ts": "cds-ts serve srv/world.cds"
|
"watch:ts": "cds-ts serve world.cds"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^27.0.2",
|
"@types/jest": "^26.0.23",
|
||||||
"@types/node": "^16.11.6",
|
"@types/node": "^15.12.0",
|
||||||
"ts-jest": "^27.0.2",
|
"ts-jest": "^27.0.2",
|
||||||
"typescript": "^4.3.5"
|
"typescript": "^4.3.5"
|
||||||
},
|
},
|
||||||
@@ -25,30 +25,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": "eslint:recommended",
|
|
||||||
"env": {
|
|
||||||
"es2020": true,
|
|
||||||
"node": true,
|
|
||||||
"jest": true,
|
|
||||||
"mocha": true
|
|
||||||
},
|
|
||||||
"globals": {
|
|
||||||
"SELECT": true,
|
|
||||||
"INSERT": true,
|
|
||||||
"UPDATE": true,
|
|
||||||
"DELETE": true,
|
|
||||||
"CREATE": true,
|
|
||||||
"DROP": true,
|
|
||||||
"CDL": true,
|
|
||||||
"CQL": true,
|
|
||||||
"CXL": true,
|
|
||||||
"cds": true
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"no-console": "off",
|
|
||||||
"require-atomic-updates": "off"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { Request } from "@sap/cds/apis/services"
|
|
||||||
|
|
||||||
module.exports = class say {
|
|
||||||
hello(req: Request) {
|
|
||||||
return `Hello ${req.data.to} from a TypeScript file!`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ process.env.CDS_TYPESCRIPT = 'true';
|
|||||||
import * as cds from '@sap/cds';
|
import * as cds from '@sap/cds';
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const {GET} = cds.test.in(__dirname,'../srv').run('serve', 'world.cds');
|
const {GET} = cds.test.in(__dirname,'..').run('serve', 'world.cds');
|
||||||
|
|
||||||
describe('Hello world!', () => {
|
describe('Hello world!', () => {
|
||||||
afterAll(() => { delete process.env.CDS_TYPESCRIPT; });
|
afterAll(() => { delete process.env.CDS_TYPESCRIPT; });
|
||||||
|
|||||||
5
hello/world.ts
Normal file
5
hello/world.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = class say {
|
||||||
|
hello(req: any) {
|
||||||
|
return `Hello ${req.data.to} from a typescript file!`
|
||||||
|
}
|
||||||
|
}
|
||||||
9
orders/api/external.cds
Normal file
9
orders/api/external.cds
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
context external {
|
||||||
|
|
||||||
|
/** This is a stand-in for arbitrary ordered Products */
|
||||||
|
entity Products @(cds.persistence.skip:'always') {
|
||||||
|
key ID : String;
|
||||||
|
data : String;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
21
orders/api/orders-service-comp.cds
Normal file
21
orders/api/orders-service-comp.cds
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using { cuid, managed, User, Currency } from '@sap/cds/common';
|
||||||
|
using { sap.capire.orders as my } from '../db/schema';
|
||||||
|
using { external.Products } from './external';
|
||||||
|
|
||||||
|
|
||||||
|
service OrdersService {
|
||||||
|
|
||||||
|
entity Orders : cuid, managed {
|
||||||
|
OrderNo : String @title:'Order Number'; //> readable key
|
||||||
|
Items : Composition of many {
|
||||||
|
key ID : UUID;
|
||||||
|
product : Association to Products;
|
||||||
|
amount : Integer;
|
||||||
|
title : String; //> intentionally replicated as snapshot from product.title
|
||||||
|
price : Double;
|
||||||
|
};
|
||||||
|
buyer : User;
|
||||||
|
currency : Currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
40
orders/api/orders-service.cds
Normal file
40
orders/api/orders-service.cds
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using { cuid, managed, User, Currency } from '@sap/cds/common';
|
||||||
|
using { sap.capire.orders as my } from '../db/schema';
|
||||||
|
using { external.Products } from './external';
|
||||||
|
|
||||||
|
|
||||||
|
// GET Orders
|
||||||
|
// GET Orders()/Items
|
||||||
|
// GET Orders?$Items
|
||||||
|
|
||||||
|
service OwnService {
|
||||||
|
entity Orders as projection on OrdersService.Orders;
|
||||||
|
entity Products as projection on OrdersService.Products;
|
||||||
|
}
|
||||||
|
|
||||||
|
service OrdersService {
|
||||||
|
|
||||||
|
entity Orders : cuid, managed {
|
||||||
|
OrderNo : String @title:'Order Number'; //> readable key
|
||||||
|
Items : Composition of many Orders_Items; // on Items.up_ = $self;
|
||||||
|
// MoreItems : Composition of many Orders_Items; // on Items.up_ = $self;
|
||||||
|
// Items_ID
|
||||||
|
buyer : User;
|
||||||
|
currency : Currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
entity Orders_Items {
|
||||||
|
key ID : UUID;
|
||||||
|
// up_ : Association to Orders;
|
||||||
|
product : Association to Products; //> {ID}
|
||||||
|
// product_ID
|
||||||
|
amount : Integer;
|
||||||
|
title : String; //> intentionally replicated as snapshot from product.title
|
||||||
|
price : Double;
|
||||||
|
}
|
||||||
|
|
||||||
|
// entity Products {
|
||||||
|
// key ID : UUID;
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
||||||
5
orders/app/index.cds
Normal file
5
orders/app/index.cds
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/*
|
||||||
|
This model controls what gets served to Fiori frontends...
|
||||||
|
*/
|
||||||
|
|
||||||
|
using from './orders/fiori-service';
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
using { OrdersService } from '../srv/orders-service';
|
using { OrdersService } from '../../srv/orders-service';
|
||||||
|
|
||||||
|
|
||||||
@odata.draft.enabled
|
@odata.draft.enabled
|
||||||
@@ -68,16 +68,16 @@ annotate OrdersService.Orders with @(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
annotate OrdersService.Orders.Items with @(
|
annotate OrdersService.Orders_Items with @(
|
||||||
UI: {
|
UI: {
|
||||||
LineItem: [
|
LineItem: [
|
||||||
{Value: product_ID, Label:'Product ID'},
|
{Value: product_ID, Label:'Product ID'},
|
||||||
{Value: title, Label:'Product Title'},
|
{Value: title, Label:'Product Title'},
|
||||||
{Value: price, Label:'Unit Price'},
|
{Value: price, Label:'Unit Price'},
|
||||||
{Value: quantity, Label:'Quantity'},
|
{Value: amount, Label:'Quantity'},
|
||||||
],
|
],
|
||||||
Identification: [ //Is the main field group
|
Identification: [ //Is the main field group
|
||||||
{Value: quantity, Label:'Quantity'},
|
{Value: amount, Label:'Amount'},
|
||||||
{Value: title, Label:'Product'},
|
{Value: title, Label:'Product'},
|
||||||
{Value: price, Label:'Unit Price'},
|
{Value: price, Label:'Unit Price'},
|
||||||
],
|
],
|
||||||
@@ -86,7 +86,7 @@ annotate OrdersService.Orders.Items with @(
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
quantity @(
|
amount @(
|
||||||
Common.FieldControl: #Mandatory
|
Common.FieldControl: #Mandatory
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
ID;up__ID;quantity;product_ID;title;price
|
ID;up__ID;amount;product_ID;title;price
|
||||||
58040e66-1dcd-4ffb-ab10-fdce32028b79;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;201;Wuthering Heights;11.11
|
58040e66-1dcd-4ffb-ab10-fdce32028b79;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;201;Wuthering Heights;11.11
|
||||||
64e718c9-ff99-47f1-8ca3-950c850777d4;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;271;Catweazle;15
|
64e718c9-ff99-47f1-8ca3-950c850777d4;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;271;Catweazle;15
|
||||||
e9641166-e050-4261-bfee-d1e797e6cb7f;64e718c9-ff99-47f1-8ca3-950c850777d4;2;252;Eleonora;28
|
e9641166-e050-4261-bfee-d1e797e6cb7f;64e718c9-ff99-47f1-8ca3-950c850777d4;2;252;Eleonora;28
|
||||||
|
@@ -3,22 +3,21 @@ namespace sap.capire.orders;
|
|||||||
|
|
||||||
entity Orders : cuid, managed {
|
entity Orders : cuid, managed {
|
||||||
OrderNo : String @title:'Order Number'; //> readable key
|
OrderNo : String @title:'Order Number'; //> readable key
|
||||||
Items : Composition of many {
|
Items : Composition of many Orders_Items on Items.up_ = $self;
|
||||||
key ID : UUID;
|
|
||||||
product : Association to Products;
|
|
||||||
quantity : Integer;
|
|
||||||
title : String; //> intentionally replicated as snapshot from product.title
|
|
||||||
price : Double; //> materialized calculated field
|
|
||||||
};
|
|
||||||
buyer : User;
|
buyer : User;
|
||||||
currency : Currency;
|
currency : Currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity Orders_Items {
|
||||||
|
key ID : UUID;
|
||||||
|
up_ : Association to Orders;
|
||||||
|
product : Association to Products @assert.integrity:false; // REVISIT: this is a temporary workaround for a glitch in cds-runtime
|
||||||
|
amount : Integer;
|
||||||
|
title : String; //> intentionally replicated as snapshot from product.title
|
||||||
|
price : Double;
|
||||||
|
}
|
||||||
|
|
||||||
/** This is a stand-in for arbitrary ordered Products */
|
/** This is a stand-in for arbitrary ordered Products */
|
||||||
entity Products @(cds.persistence.skip:'always') {
|
entity Products @(cds.persistence.skip:'always') {
|
||||||
key ID : String;
|
key ID : String;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// this is to ensure we have filled-in currencies
|
|
||||||
using from '@capire/common';
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"name": "@capire/orders",
|
"name": "@capire/orders",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capire/common": "*",
|
|
||||||
"@sap/cds": "^5"
|
"@sap/cds": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,34 +3,34 @@ class OrdersService extends cds.ApplicationService {
|
|||||||
|
|
||||||
/** register custom handlers */
|
/** register custom handlers */
|
||||||
init(){
|
init(){
|
||||||
const { 'Orders.Items':OrderItems } = this.entities
|
const { Orders_Items:OrderItems } = this.entities
|
||||||
|
|
||||||
this.before ('UPDATE', 'Orders', async function(req) {
|
this.before ('UPDATE', 'Orders', async function(req) {
|
||||||
const { ID, Items } = req.data
|
const { ID, Items } = req.data
|
||||||
if (Items) for (let { product_ID, quantity } of Items) {
|
if (Items) for (let { product_ID, amount } of Items) {
|
||||||
const { quantity:before } = await cds.tx(req).run (
|
const { amount:before } = await cds.tx(req).run (
|
||||||
SELECT.one.from (OrderItems, oi => oi.quantity) .where ({up__ID:ID, product_ID})
|
SELECT.one.from (OrderItems, oi => oi.amount) .where ({up__ID:ID, product_ID})
|
||||||
)
|
)
|
||||||
if (quantity != before) await this.orderChanged (product_ID, quantity-before)
|
if (amount != before) await this.orderChanged (product_ID, amount-before)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.before ('DELETE', 'Orders', async function(req) {
|
this.before ('DELETE', 'Orders', async function(req) {
|
||||||
const { ID } = req.data
|
const { ID } = req.data
|
||||||
const Items = await cds.tx(req).run (
|
const Items = await cds.tx(req).run (
|
||||||
SELECT.from (OrderItems, oi => { oi.product_ID, oi.quantity }) .where ({up__ID:ID})
|
SELECT.from (OrderItems, oi => { oi.product_ID, oi.amount }) .where ({up__ID:ID})
|
||||||
)
|
)
|
||||||
if (Items) await Promise.all (Items.map(it => this.orderChanged (it.product_ID, -it.quantity)))
|
if (Items) await Promise.all (Items.map(it => this.orderChanged (it.product_ID, -it.amount)))
|
||||||
})
|
})
|
||||||
|
|
||||||
return super.init()
|
return super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** order changed -> broadcast event */
|
/** order changed -> broadcast event */
|
||||||
orderChanged (product, deltaQuantity) {
|
orderChanged (product, deltaAmount) {
|
||||||
// Emit events to inform subscribers about changes in orders
|
// Emit events to inform subscribers about changes in orders
|
||||||
console.log ('> emitting:', 'OrderChanged', { product, deltaQuantity })
|
console.log ('> emitting:', 'OrderChanged', { product, deltaAmount })
|
||||||
return this.emit ('OrderChanged', { product, deltaQuantity })
|
return this.emit ('OrderChanged', { product, deltaAmount })
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
23502
package-lock.json
generated
23502
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -5,7 +5,6 @@
|
|||||||
"repository": "https://github.com/sap-samples/cloud-cap-samples.git",
|
"repository": "https://github.com/sap-samples/cloud-cap-samples.git",
|
||||||
"author": "daniel.hutzel@sap.com",
|
"author": "daniel.hutzel@sap.com",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capire/bookstore": "./bookstore",
|
|
||||||
"@capire/bookshop": "./bookshop",
|
"@capire/bookshop": "./bookshop",
|
||||||
"@capire/common": "./common",
|
"@capire/common": "./common",
|
||||||
"@capire/fiori": "./fiori",
|
"@capire/fiori": "./fiori",
|
||||||
@@ -13,33 +12,31 @@
|
|||||||
"@capire/media": "./media",
|
"@capire/media": "./media",
|
||||||
"@capire/orders": "./orders",
|
"@capire/orders": "./orders",
|
||||||
"@capire/reviews": "./reviews",
|
"@capire/reviews": "./reviews",
|
||||||
"@sap/cds": "^5.5.3"
|
"@capire/suppliers": "./suppliers",
|
||||||
|
"@sap/cds": "^5",
|
||||||
|
"express": "^4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^4.3.4",
|
"cds-swagger-ui-express": "^0.2.0",
|
||||||
|
"chai": "^4.2.0",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"chai-subset": "^1.6.0",
|
"chai-subset": "^1.6.0",
|
||||||
"sqlite3": "^5"
|
"sqlite3": "^5.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"cleanup": "rm -rf node_modules && rm -rf */node_modules && rm -rf */*/node_modules",
|
"cleanup": "rm -rf node_modules && rm -rf */node_modules && rm -rf */*/node_modules",
|
||||||
"registry": "node .registry/server.js",
|
"registry": "node .registry/server.js",
|
||||||
"bookshop": "cds watch bookshop",
|
"bookshop": "cds watch bookshop",
|
||||||
"fiori": "cds watch fiori",
|
"fiori": "cds watch fiori",
|
||||||
"hello": "cds watch hello",
|
|
||||||
"media": "cds watch media",
|
"media": "cds watch media",
|
||||||
"mocha": "npx mocha || echo",
|
"mocha": "npx mocha || echo",
|
||||||
"jest": "npx jest",
|
"jest": "npx jest",
|
||||||
"start": "cds watch fiori",
|
"test": "npm run jest --silent && npm run test:hello",
|
||||||
"test": "npm run jest -- --silent",
|
|
||||||
"test:hello": "cd hello && npm test"
|
"test:hello": "cd hello && npm test"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"testEnvironment": "node",
|
"testEnvironment": "node",
|
||||||
"testTimeout": 20000,
|
"testMatch": ["**/*.test.js"]
|
||||||
"testMatch": [
|
|
||||||
"**/*.test.js"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"mocha": {
|
"mocha": {
|
||||||
"parallel": true
|
"parallel": true
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
# cds.requires.messaging.kind = file-based-messaging
|
cds.requires.messaging.kind = file-based-messaging
|
||||||
PORT = 4005
|
PORT = 4005
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
subject;rating;reviewer;title;text
|
subject;rating;title;text
|
||||||
201;5;adam;Intriguing;Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
|
201;5;Intriguing;Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
|
||||||
201;4;bob;Fascinating;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Id diam maecenas ultricies mi eget mauris pharetra et. Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque. Pulvinar mattis nunc sed blandit libero. Facilisis magna etiam tempor orci eu. Nec sagittis aliquam malesuada bibendum arcu. Eu consequat ac felis donec. Ultricies tristique nulla aliquet enim tortor at auctor urna nunc. Tortor posuere ac ut consequat semper viverra nam libero. Amet nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Scelerisque purus semper eget duis at tellus. Elementum tempus egestas sed sed risus pretium. Arcu dictum varius duis at. Amet luctus venenatis lectus magna fringilla urna. Eget velit aliquet sagittis id consectetur purus ut faucibus. Vitae auctor eu augue ut lectus. Fermentum iaculis eu non diam phasellus vestibulum.
|
201;4;Fascinating;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Id diam maecenas ultricies mi eget mauris pharetra et. Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque. Pulvinar mattis nunc sed blandit libero. Facilisis magna etiam tempor orci eu. Nec sagittis aliquam malesuada bibendum arcu. Eu consequat ac felis donec. Ultricies tristique nulla aliquet enim tortor at auctor urna nunc. Tortor posuere ac ut consequat semper viverra nam libero. Amet nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Scelerisque purus semper eget duis at tellus. Elementum tempus egestas sed sed risus pretium. Arcu dictum varius duis at. Amet luctus venenatis lectus magna fringilla urna. Eget velit aliquet sagittis id consectetur purus ut faucibus. Vitae auctor eu augue ut lectus. Fermentum iaculis eu non diam phasellus vestibulum.
|
||||||
207;2;bob;What is this?;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero justo laoreet sit amet cursus sit amet dictum. Nunc faucibus a pellentesque sit. Dis parturient montes nascetur ridiculus mus mauris vitae ultricies. Enim nunc faucibus a pellentesque. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien. Cras ornare arcu dui vivamus. Facilisi etiam dignissim diam quis enim lobortis. Et molestie ac feugiat sed. Urna neque viverra justo nec ultrices dui. Ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis. Dignissim sodales ut eu sem. Feugiat in fermentum posuere urna nec. At augue eget arcu dictum varius.
|
207;2;What is this?;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero justo laoreet sit amet cursus sit amet dictum. Nunc faucibus a pellentesque sit. Dis parturient montes nascetur ridiculus mus mauris vitae ultricies. Enim nunc faucibus a pellentesque. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien. Cras ornare arcu dui vivamus. Facilisi etiam dignissim diam quis enim lobortis. Et molestie ac feugiat sed. Urna neque viverra justo nec ultrices dui. Ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis. Dignissim sodales ut eu sem. Feugiat in fermentum posuere urna nec. At augue eget arcu dictum varius.
|
||||||
251;3;bob;It's dark...;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Suscipit tellus mauris a diam. Velit aliquet sagittis id consectetur purus ut. Viverra adipiscing at in tellus integer. Vitae elementum curabitur vitae nunc. Mattis ullamcorper velit sed ullamcorper morbi. Diam quis enim lobortis scelerisque. Auctor neque vitae tempus quam pellentesque nec nam aliquam. Semper auctor neque vitae tempus. Quis eleifend quam adipiscing vitae proin. Neque convallis a cras semper auctor neque vitae. Imperdiet massa tincidunt nunc pulvinar sapien et ligula. Sit amet consectetur adipiscing elit ut aliquam purus. Pretium quam vulputate dignissim suspendisse.
|
251;3;It's dark...;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Suscipit tellus mauris a diam. Velit aliquet sagittis id consectetur purus ut. Viverra adipiscing at in tellus integer. Vitae elementum curabitur vitae nunc. Mattis ullamcorper velit sed ullamcorper morbi. Diam quis enim lobortis scelerisque. Auctor neque vitae tempus quam pellentesque nec nam aliquam. Semper auctor neque vitae tempus. Quis eleifend quam adipiscing vitae proin. Neque convallis a cras semper auctor neque vitae. Imperdiet massa tincidunt nunc pulvinar sapien et ligula. Sit amet consectetur adipiscing elit ut aliquam purus. Pretium quam vulputate dignissim suspendisse.
|
||||||
|
@@ -10,14 +10,15 @@
|
|||||||
"@sap/cds": "^5",
|
"@sap/cds": "^5",
|
||||||
"express": "^4.17.1"
|
"express": "^4.17.1"
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"reviews-service": "cds watch",
|
||||||
|
"books-reviewed": "cds watch ../reviewed"
|
||||||
|
},
|
||||||
"cds": {
|
"cds": {
|
||||||
"requires": {
|
"requires": {
|
||||||
"messaging": {
|
"db": {
|
||||||
"[development]": { "kind": "file-based-messaging" },
|
"kind": "sql"
|
||||||
"[hybrid]": { "kind": "enterprise-messaging-shared" },
|
}
|
||||||
"[production]": { "kind": "enterprise-messaging" }
|
|
||||||
},
|
|
||||||
"db": { "kind": "sql" }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,11 +8,10 @@ service ReviewsService {
|
|||||||
action unlike (review: type of Reviews:ID);
|
action unlike (review: type of Reviews:ID);
|
||||||
|
|
||||||
// Async API
|
// Async API
|
||||||
event reviewed : {
|
event reviewed : projection on Reviews {
|
||||||
subject : type of Reviews:subject;
|
subject, //> recieved new reviews
|
||||||
count : Integer;
|
rating //> new avg rating
|
||||||
rating : Decimal;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Input validation
|
// Input validation
|
||||||
annotate Reviews with {
|
annotate Reviews with {
|
||||||
@@ -28,7 +27,14 @@ service ReviewsService {
|
|||||||
annotate ReviewsService.Reviews with @restrict:[
|
annotate ReviewsService.Reviews with @restrict:[
|
||||||
{ grant:'READ', to:'any' }, // everybody can read reviews
|
{ grant:'READ', to:'any' }, // everybody can read reviews
|
||||||
{ grant:'CREATE', to:'authenticated-user' }, // users must login to add reviews
|
{ grant:'CREATE', to:'authenticated-user' }, // users must login to add reviews
|
||||||
{ grant:'UPDATE', to:'authenticated-user', where:'reviewer=$user' },
|
/////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Temporarily disabling this due to glitch in CAP Node.js runtime:
|
||||||
|
// { grant:'UPDATE', to:'authenticated-user', where:'reviewer=$user' },
|
||||||
|
// -> reenable it when the issue is fixed
|
||||||
|
{ grant:'UPDATE', to:'authenticated-user' },
|
||||||
|
//
|
||||||
|
////////////////////////////////////////////////////
|
||||||
{ grant:'DELETE', to:'admin' },
|
{ grant:'DELETE', to:'admin' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ module.exports = cds.service.impl (function(){
|
|||||||
// Emit an event to inform subscribers about new avg ratings for reviewed subjects
|
// Emit an event to inform subscribers about new avg ratings for reviewed subjects
|
||||||
this.after (['CREATE','UPDATE','DELETE'], 'Reviews', async function(_,req) {
|
this.after (['CREATE','UPDATE','DELETE'], 'Reviews', async function(_,req) {
|
||||||
const {subject} = req.data
|
const {subject} = req.data
|
||||||
const { count, rating } = await cds.tx(req) .run (
|
const {rating} = await cds.tx(req) .run (
|
||||||
SELECT.one `round(avg(rating),2) as rating, count(*) as count` .from (Reviews) .where ({subject})
|
SELECT.one (['round(avg(rating),2) as rating']) .from (Reviews) .where ({subject})
|
||||||
)
|
)
|
||||||
global.it || console.log ('< emitting:', 'reviewed', { subject, count, rating })
|
global.it || console.log ('< emitting:', 'reviewed', { subject, rating })
|
||||||
await this.emit ('reviewed', { subject, count, rating })
|
await this.emit ('reviewed', { subject, rating })
|
||||||
})
|
})
|
||||||
|
|
||||||
// Increment counter for reviews considered helpful
|
// Increment counter for reviews considered helpful
|
||||||
|
|||||||
2
reviews/test/bookshop/.env
Normal file
2
reviews/test/bookshop/.env
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
cds.requires.messaging.kind = file-based-messaging
|
||||||
|
PORT = 4004
|
||||||
19
reviews/test/bookshop/package.json
Normal file
19
reviews/test/bookshop/package.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "@capire/fiori",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@capire/bookshop": "*",
|
||||||
|
"@capire/reviews": "*",
|
||||||
|
"@sap/cds": "^5",
|
||||||
|
"express": "^4.17.1"
|
||||||
|
},
|
||||||
|
"cds": {
|
||||||
|
"requires": {
|
||||||
|
"auth": { "strategy": "dummy" },
|
||||||
|
"ReviewsService": {
|
||||||
|
"kind": "odata",
|
||||||
|
"model": "@capire/reviews"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
reviews/test/bookshop/server.js
Normal file
20
reviews/test/bookshop/server.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const cds = require ('@sap/cds')
|
||||||
|
|
||||||
|
cds.once('bootstrap',(app)=>{
|
||||||
|
// Delegate to imported apps (reviews only when mocked)
|
||||||
|
app.serve ('/bookshop').from ('@capire/bookshop','app/vue')
|
||||||
|
app.serve ('/reviews',).from ('@capire/reviews','app/vue')
|
||||||
|
})
|
||||||
|
|
||||||
|
cds.once('served', async ()=>{
|
||||||
|
// Update Books' average ratings when ReviewsService signals updated reviews
|
||||||
|
const ReviewsService = await cds.connect.to ('ReviewsService')
|
||||||
|
ReviewsService.on ('reviewed', (msg) => {
|
||||||
|
console.debug ('> received:', msg.event, msg.data)
|
||||||
|
const { subject, rating } = msg.data
|
||||||
|
return UPDATE('Books',subject).with({rating})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = cds.server
|
||||||
11
reviews/test/bookshop/services.cds
Normal file
11
reviews/test/bookshop/services.cds
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace sap.capire.bookshop; //> allows UPDATE('Books')...
|
||||||
|
//
|
||||||
|
// Extend Books with access to Reviews and average ratings
|
||||||
|
//
|
||||||
|
using { CatalogService.ListOfBooks, sap.capire.bookshop.Books } from '@capire/bookshop';
|
||||||
|
using { ReviewsService.Reviews } from '@capire/reviews';
|
||||||
|
extend Books with {
|
||||||
|
reviews : Composition of many Reviews on reviews.subject = $self.ID;
|
||||||
|
rating : Reviews:rating;
|
||||||
|
}
|
||||||
|
extend projection ListOfBooks with { rating }
|
||||||
35
samples.md
35
samples.md
@@ -50,28 +50,29 @@ Each sub directory essentially is an individual npm package arranged in an [all-
|
|||||||
- Late-cut Micro Services
|
- Late-cut Micro Services
|
||||||
- As well as managed data, input validations, and authorization
|
- As well as managed data, input validations, and authorization
|
||||||
|
|
||||||
|
## [@capire/suppliers](suppliers)
|
||||||
|
|
||||||
## [@capire/bookstore](bookstore)
|
- Shows how to integrate remote services, in this case the BusinessPartner service from SAP S/4HANA.
|
||||||
|
- Extending [@capire/bookshop](bookshop) with suppliers from SAP S/4HANA
|
||||||
- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:
|
- Providing that as a pre-built integration & extension package
|
||||||
- [@capire/bookshop](bookshop)
|
- Used in [@capire/fiori](fiori)
|
||||||
- [@capire/reviews](reviews)
|
|
||||||
- [@capire/orders](orders)
|
|
||||||
- [@capire/common](common)
|
|
||||||
- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well
|
|
||||||
- [The Vue.js app](reviews/app/vue) imported from reviews is served as well
|
|
||||||
- [The Fiori app](orders/app) imported from orders is served as well
|
|
||||||
- [OpenAPI export + Swagger UI](https://cap.cloud.sap/docs/advanced/openapi)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [@capire/fiori](fiori)
|
## [@capire/fiori](fiori)
|
||||||
|
|
||||||
- [Adds an SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookstore, thereby introducing to:
|
- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:
|
||||||
- [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files
|
- [@capire/bookshop](bookshop)
|
||||||
- Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)
|
- [@capire/common](common)
|
||||||
- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)
|
- [@capire/orders](orders)
|
||||||
- Serving SAP Fiori apps locally
|
- [@capire/reviews](reviews)
|
||||||
|
- [@capire/suppliers](suppliers)
|
||||||
|
- [Adds an SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookshop, thereby introducing to:
|
||||||
|
- [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files
|
||||||
|
- Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)
|
||||||
|
- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)
|
||||||
|
- Serving SAP Fiori apps locally
|
||||||
|
- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well
|
||||||
|
- [OpenAPI export + Swagger UI](https://cap.cloud.sap/docs/advanced/openapi)
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
|||||||
1
suppliers/.env
Normal file
1
suppliers/.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
PORT = 4006
|
||||||
31
suppliers/app/_i18n/i18n.properties
Normal file
31
suppliers/app/_i18n/i18n.properties
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
Books = Books
|
||||||
|
Book = Book
|
||||||
|
ID = ID
|
||||||
|
Title = Title
|
||||||
|
Author = Author
|
||||||
|
AuthorID = Author ID
|
||||||
|
Stock = Stock
|
||||||
|
Name = Name
|
||||||
|
AuthorName = Author's Name
|
||||||
|
DateOfBirth = Date of Birth
|
||||||
|
DateOfDeath = Date of Death
|
||||||
|
PlaceOfBirth = Place of Birth
|
||||||
|
PlaceOfDeath = Place of Death
|
||||||
|
Age = Age
|
||||||
|
Authors = Authors
|
||||||
|
Order = Order
|
||||||
|
Orders = Orders
|
||||||
|
Price = Price
|
||||||
|
|
||||||
|
Genre = Genre
|
||||||
|
Genres = Genres
|
||||||
|
SubGenres = Sub Genres
|
||||||
|
|
||||||
|
NumCode = Numeric Code
|
||||||
|
MinorUnit = Minor Unit
|
||||||
|
Exponent = Exponent
|
||||||
|
|
||||||
|
Supplier = Supplier
|
||||||
|
SupplierName = Supplier Name
|
||||||
|
Id = Id
|
||||||
|
Name = Name
|
||||||
18
suppliers/app/_i18n/i18n_de.properties
Normal file
18
suppliers/app/_i18n/i18n_de.properties
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
Books = Bücher
|
||||||
|
Book = Buch
|
||||||
|
ID = ID
|
||||||
|
Title = Titel
|
||||||
|
Authors = Autoren
|
||||||
|
Author = Autor
|
||||||
|
AuthorID = ID des Autors
|
||||||
|
AuthorName = Name des Autors
|
||||||
|
Age = Alter
|
||||||
|
Name = Name
|
||||||
|
Stock = Bestand
|
||||||
|
Order = Bestellung
|
||||||
|
Orders = Bestellungen
|
||||||
|
Price = Preis
|
||||||
|
Supplier = Lieferant
|
||||||
|
SupplierName = Lieferantenname
|
||||||
|
Id = Id
|
||||||
|
Name = Name
|
||||||
55
suppliers/app/admin-fiori.html
Normal file
55
suppliers/app/admin-fiori.html
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Bookshop</title>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window["sap-ushell-config"] = {
|
||||||
|
defaultRenderer: "fiori2",
|
||||||
|
applications: {
|
||||||
|
"browse-books": {
|
||||||
|
title: "Browse Books",
|
||||||
|
description: "w/ SAP Fiori Elements",
|
||||||
|
additionalInformation: "SAPUI5.Component=bookshop",
|
||||||
|
applicationType : "URL",
|
||||||
|
url: "/browse/webapp",
|
||||||
|
navigationMode: "embedded"
|
||||||
|
},
|
||||||
|
"manage-books": {
|
||||||
|
title: "Manage Books",
|
||||||
|
description: "w/ SAP Fiori Elements",
|
||||||
|
additionalInformation: "SAPUI5.Component=admin",
|
||||||
|
applicationType : "URL",
|
||||||
|
url: "/admin/webapp",
|
||||||
|
navigationMode: "embedded"
|
||||||
|
},
|
||||||
|
"manage-orders": {
|
||||||
|
title: "Manage Orders",
|
||||||
|
description: "w/ SAP Fiori Elements",
|
||||||
|
additionalInformation: "SAPUI5.Component=orders",
|
||||||
|
applicationType : "URL",
|
||||||
|
url: "/orders/webapp",
|
||||||
|
navigationMode: "embedded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="sap-ushell-bootstrap" src="https://sapui5.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js"></script>
|
||||||
|
<script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
|
||||||
|
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout"
|
||||||
|
data-sap-ui-compatVersion="edge"
|
||||||
|
data-sap-ui-theme="sap_fiori_3"
|
||||||
|
data-sap-ui-frameOptions="allow"
|
||||||
|
></script>
|
||||||
|
<script>
|
||||||
|
sap.ui.getCore().attachInit(()=> sap.ushell.Container.createRenderer().placeAt("content"))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body class="sapUiBody" id="content"></body>
|
||||||
|
</html>
|
||||||
105
suppliers/app/admin/fiori-service.cds
Normal file
105
suppliers/app/admin/fiori-service.cds
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
//using { AdminService } from '../../db/schema';
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Books Object Page
|
||||||
|
//
|
||||||
|
|
||||||
|
annotate AdminService.Books with @(
|
||||||
|
UI: {
|
||||||
|
Facets: [
|
||||||
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>General}', Target: '@UI.FieldGroup#General'},
|
||||||
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Translations}', Target: 'texts/@UI.LineItem'},
|
||||||
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
|
||||||
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Supplier}', Target: '@UI.FieldGroup#Supplier'},
|
||||||
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Admin}', Target: '@UI.FieldGroup#Admin'},
|
||||||
|
],
|
||||||
|
FieldGroup#General: {
|
||||||
|
Data: [
|
||||||
|
{Value: title},
|
||||||
|
{Value: author_ID},
|
||||||
|
{Value: genre_ID},
|
||||||
|
{Value: descr},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
FieldGroup#Details: {
|
||||||
|
Data: [
|
||||||
|
{Value: stock},
|
||||||
|
{Value: price},
|
||||||
|
{Value: currency_code, Label: '{i18n>Currency}'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
FieldGroup#Supplier: {
|
||||||
|
Data: [
|
||||||
|
{Value: supplier_ID, Label: '{i18n>Id}'},
|
||||||
|
{Value: supplier.name, Label: '{i18n>Name}'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
FieldGroup#Admin: {
|
||||||
|
Data: [
|
||||||
|
{Value: createdBy},
|
||||||
|
{Value: createdAt},
|
||||||
|
{Value: modifiedBy},
|
||||||
|
{Value: modifiedAt}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
annotate AdminService.Authors with @(
|
||||||
|
UI: {
|
||||||
|
HeaderInfo: {
|
||||||
|
Description: {Value: lifetime}
|
||||||
|
},
|
||||||
|
Facets: [
|
||||||
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
|
||||||
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Books}', Target: 'books/@UI.LineItem'},
|
||||||
|
],
|
||||||
|
FieldGroup#Details: {
|
||||||
|
Data: [
|
||||||
|
{Value: placeOfBirth},
|
||||||
|
{Value: placeOfDeath},
|
||||||
|
{Value: dateOfBirth},
|
||||||
|
{Value: dateOfDeath},
|
||||||
|
{Value: age, Label: '{i18n>Age}'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Draft for Localized Data
|
||||||
|
//
|
||||||
|
|
||||||
|
annotate sap.capire.bookshop.Books with @fiori.draft.enabled;
|
||||||
|
annotate AdminService.Books with @odata.draft.enabled;
|
||||||
|
|
||||||
|
annotate AdminService.Books.texts with @(
|
||||||
|
UI: {
|
||||||
|
Identification: [{Value:title}],
|
||||||
|
SelectionFields: [ locale, title ],
|
||||||
|
LineItem: [
|
||||||
|
{Value: locale, Label: 'Locale'},
|
||||||
|
{Value: title, Label: 'Title'},
|
||||||
|
{Value: descr, Label: 'Description'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add Value Help for Locales
|
||||||
|
annotate AdminService.Books.texts {
|
||||||
|
locale @ValueList:{entity:'Languages',type:#fixed}
|
||||||
|
}
|
||||||
|
// In addition we need to expose Languages through AdminService
|
||||||
|
using { sap } from '@sap/cds/common';
|
||||||
|
extend service AdminService {
|
||||||
|
entity Languages as projection on sap.common.Languages;
|
||||||
|
}
|
||||||
|
|
||||||
|
annotate AdminService.Suppliers with
|
||||||
|
@(Capabilities.SearchRestrictions : {
|
||||||
|
Searchable : false
|
||||||
|
});
|
||||||
8
suppliers/app/admin/webapp/Component.js
Normal file
8
suppliers/app/admin/webapp/Component.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
|
||||||
|
"use strict";
|
||||||
|
return AppComponent.extend("admin.Component", {
|
||||||
|
metadata: { manifest: "json" }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* eslint no-undef:0 */
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
# __ldi.translation.uuid=c3431418-9caf-11e8-98d0-529269fb1459
|
# __ldi.translation.uuid=c3431418-9caf-11e8-98d0-529269fb1459
|
||||||
|
|
||||||
# JCI app descriptor contains lower case TITLE
|
# JCI app descriptor contains lower case TITLE
|
||||||
appTitle=Bookshop Authors
|
appTitle=Bookshop Sample
|
||||||
|
|
||||||
# JCI app descriptor contains lower case DESCRIPTION
|
# JCI app descriptor contains lower case DESCRIPTION
|
||||||
appSubTitle=Bookshop Authors
|
appSubTitle=CAP Sample Application
|
||||||
|
|
||||||
# JCI app descriptor contains lower case DESCRIPTION
|
# JCI app descriptor contains lower case DESCRIPTION
|
||||||
appDescription=Bookshop Authors
|
appDescription=CDS Sample Service
|
||||||
128
suppliers/app/admin/webapp/manifest.json
Normal file
128
suppliers/app/admin/webapp/manifest.json
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
{
|
||||||
|
"_version": "1.8.0",
|
||||||
|
"sap.app": {
|
||||||
|
"id": "admin",
|
||||||
|
"type": "application",
|
||||||
|
"title": "Manage Books",
|
||||||
|
"description": "Sample Application",
|
||||||
|
"i18n": "i18n/i18n.properties",
|
||||||
|
"dataSources": {
|
||||||
|
"AdminService": {
|
||||||
|
"uri": "/admin/",
|
||||||
|
"type": "OData",
|
||||||
|
"settings": {
|
||||||
|
"odataVersion": "4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"-sourceTemplate": {
|
||||||
|
"id": "ui5template.basicSAPUI5ApplicationProject",
|
||||||
|
"-id": "ui5template.smartTemplate",
|
||||||
|
"-version": "1.40.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sap.ui5": {
|
||||||
|
"dependencies": {
|
||||||
|
"libs": {
|
||||||
|
"sap.fe.templates": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"i18n": {
|
||||||
|
"type": "sap.ui.model.resource.ResourceModel",
|
||||||
|
"uri": "i18n/i18n.properties"
|
||||||
|
},
|
||||||
|
"": {
|
||||||
|
"dataSource": "AdminService",
|
||||||
|
"settings": {
|
||||||
|
"synchronizationMode": "None",
|
||||||
|
"operationMode": "Server",
|
||||||
|
"autoExpandSelect" : true,
|
||||||
|
"earlyRequests": true,
|
||||||
|
"groupProperties": {
|
||||||
|
"default": {
|
||||||
|
"submit": "Auto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"routing": {
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"pattern": ":?query:",
|
||||||
|
"name": "BooksList",
|
||||||
|
"target": "BooksList"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "Books({key}):?query:",
|
||||||
|
"name": "BooksDetails",
|
||||||
|
"target": "BooksDetails"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "Books({key}/author({key2}):?query:",
|
||||||
|
"name": "AuthorsDetails",
|
||||||
|
"target": "AuthorsDetails"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"targets": {
|
||||||
|
"BooksList": {
|
||||||
|
"type": "Component",
|
||||||
|
"id": "BooksList",
|
||||||
|
"name": "sap.fe.templates.ListReport",
|
||||||
|
"options": {
|
||||||
|
"settings" : {
|
||||||
|
"entitySet" : "Books",
|
||||||
|
"navigation" : {
|
||||||
|
"Books" : {
|
||||||
|
"detail" : {
|
||||||
|
"route" : "BooksDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"BooksDetails": {
|
||||||
|
"type": "Component",
|
||||||
|
"id": "BooksDetailsList",
|
||||||
|
"name": "sap.fe.templates.ObjectPage",
|
||||||
|
"options": {
|
||||||
|
"settings" : {
|
||||||
|
"entitySet" : "Books",
|
||||||
|
"navigation" : {
|
||||||
|
"Authors" : {
|
||||||
|
"detail" : {
|
||||||
|
"route" : "AuthorsDetails"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AuthorsDetails": {
|
||||||
|
"type": "Component",
|
||||||
|
"id": "AuthorsDetailsList",
|
||||||
|
"name": "sap.fe.templates.ObjectPage",
|
||||||
|
"options": {
|
||||||
|
"settings" : {
|
||||||
|
"entitySet" : "Authors"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contentDensities": {
|
||||||
|
"compact": true,
|
||||||
|
"cozy": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sap.ui": {
|
||||||
|
"technology": "UI5",
|
||||||
|
"fullWidth": false
|
||||||
|
},
|
||||||
|
"sap.fiori": {
|
||||||
|
"registrationIds": [],
|
||||||
|
"archeType": "transactional"
|
||||||
|
}
|
||||||
|
}
|
||||||
265
suppliers/app/common.cds
Normal file
265
suppliers/app/common.cds
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
/*
|
||||||
|
Common Annotations shared by all apps
|
||||||
|
*/
|
||||||
|
|
||||||
|
using { sap.capire.bookshop as my } from '@capire/bookshop';
|
||||||
|
using { sap.common } from '@capire/common';
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Books Lists
|
||||||
|
//
|
||||||
|
annotate my.Books with @(
|
||||||
|
Common.SemanticKey: [title],
|
||||||
|
UI: {
|
||||||
|
Identification: [{Value:title}],
|
||||||
|
SelectionFields: [ ID, author_ID, price, currency_code, supplier.ID, supplier.name ],
|
||||||
|
LineItem: [
|
||||||
|
{Value: ID},
|
||||||
|
{Value: title},
|
||||||
|
{Value: author.name, Label:'{i18n>Author}'},
|
||||||
|
{Value: genre.name},
|
||||||
|
{Value: stock},
|
||||||
|
{Value: price},
|
||||||
|
{Value: currency.symbol, Label:' '},
|
||||||
|
{Value: supplier.ID},
|
||||||
|
{Value: supplier.name}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
author @ValueList.entity:'Authors';
|
||||||
|
};
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Books Details
|
||||||
|
//
|
||||||
|
annotate my.Books with @(
|
||||||
|
UI: {
|
||||||
|
HeaderInfo: {
|
||||||
|
TypeName: '{i18n>Book}',
|
||||||
|
TypeNamePlural: '{i18n>Books}',
|
||||||
|
Title: {Value: title},
|
||||||
|
Description: {Value: author.name}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Books Elements
|
||||||
|
//
|
||||||
|
annotate my.Books with {
|
||||||
|
ID @title:'{i18n>ID}' @UI.HiddenFilter;
|
||||||
|
title @title:'{i18n>Title}';
|
||||||
|
genre @title:'{i18n>Genre}' @Common: { Text: genre.name, TextArrangement: #TextOnly };
|
||||||
|
author @title:'{i18n>Author}' @Common: { Text: author.name, TextArrangement: #TextOnly };
|
||||||
|
price @title:'{i18n>Price}' @Measures.ISOCurrency: currency_code;
|
||||||
|
stock @title:'{i18n>Stock}';
|
||||||
|
descr @UI.MultiLineText;
|
||||||
|
supplier_ID @title:'{i18n>Supplier}';
|
||||||
|
}
|
||||||
|
|
||||||
|
annotate my.Suppliers with {
|
||||||
|
name @title:'{i18n>SupplierName}';
|
||||||
|
ID @title:'{i18n>Supplier}';
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Genres List
|
||||||
|
//
|
||||||
|
annotate my.Genres with @(
|
||||||
|
Common.SemanticKey: [name],
|
||||||
|
UI: {
|
||||||
|
SelectionFields: [ name ],
|
||||||
|
LineItem:[
|
||||||
|
{Value: name},
|
||||||
|
{Value: parent.name, Label: 'Main Genre'},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Genre Details
|
||||||
|
//
|
||||||
|
annotate my.Genres with @(
|
||||||
|
UI: {
|
||||||
|
Identification: [{Value:name}],
|
||||||
|
HeaderInfo: {
|
||||||
|
TypeName: '{i18n>Genre}',
|
||||||
|
TypeNamePlural: '{i18n>Genres}',
|
||||||
|
Title: {Value: name},
|
||||||
|
Description: {Value: ID}
|
||||||
|
},
|
||||||
|
Facets: [
|
||||||
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>SubGenres}', Target: 'children/@UI.LineItem'},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Genres Elements
|
||||||
|
//
|
||||||
|
annotate my.Genres with {
|
||||||
|
ID @title: '{i18n>ID}';
|
||||||
|
name @title: '{i18n>Genre}';
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Authors List
|
||||||
|
//
|
||||||
|
annotate my.Authors with @(
|
||||||
|
Common.SemanticKey: [name],
|
||||||
|
UI: {
|
||||||
|
Identification: [{Value:name}],
|
||||||
|
SelectionFields: [ name ],
|
||||||
|
LineItem:[
|
||||||
|
{Value: ID},
|
||||||
|
{Value: name},
|
||||||
|
{Value: dateOfBirth},
|
||||||
|
{Value: dateOfDeath},
|
||||||
|
{Value: placeOfBirth},
|
||||||
|
{Value: placeOfDeath},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Author Details
|
||||||
|
//
|
||||||
|
annotate my.Authors with @(
|
||||||
|
UI: {
|
||||||
|
HeaderInfo: {
|
||||||
|
TypeName: '{i18n>Author}',
|
||||||
|
TypeNamePlural: '{i18n>Authors}',
|
||||||
|
Title: {Value: name},
|
||||||
|
Description: {Value: dateOfBirth}
|
||||||
|
},
|
||||||
|
Facets: [
|
||||||
|
{$Type: 'UI.ReferenceFacet', Target: 'books/@UI.LineItem'},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Authors Elements
|
||||||
|
//
|
||||||
|
annotate my.Authors with {
|
||||||
|
ID @title:'{i18n>ID}' @UI.HiddenFilter;
|
||||||
|
name @title:'{i18n>Name}';
|
||||||
|
dateOfBirth @title:'{i18n>DateOfBirth}';
|
||||||
|
dateOfDeath @title:'{i18n>DateOfDeath}';
|
||||||
|
placeOfBirth @title:'{i18n>PlaceOfBirth}';
|
||||||
|
placeOfDeath @title:'{i18n>PlaceOfDeath}';
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Languages List
|
||||||
|
//
|
||||||
|
annotate common.Languages with @(
|
||||||
|
Common.SemanticKey: [code],
|
||||||
|
Identification: [{Value:code}],
|
||||||
|
UI: {
|
||||||
|
SelectionFields: [ name, descr ],
|
||||||
|
LineItem:[
|
||||||
|
{Value: code},
|
||||||
|
{Value: name},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Language Details
|
||||||
|
//
|
||||||
|
annotate common.Languages with @(
|
||||||
|
UI: {
|
||||||
|
HeaderInfo: {
|
||||||
|
TypeName: '{i18n>Language}',
|
||||||
|
TypeNamePlural: '{i18n>Languages}',
|
||||||
|
Title: {Value: name},
|
||||||
|
Description: {Value: descr}
|
||||||
|
},
|
||||||
|
Facets: [
|
||||||
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
|
||||||
|
],
|
||||||
|
FieldGroup#Details: {
|
||||||
|
Data: [
|
||||||
|
{Value: code},
|
||||||
|
{Value: name},
|
||||||
|
{Value: descr}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Currencies List
|
||||||
|
//
|
||||||
|
annotate common.Currencies with @(
|
||||||
|
Common.SemanticKey: [code],
|
||||||
|
Identification: [{Value:code}],
|
||||||
|
UI: {
|
||||||
|
SelectionFields: [ name, descr ],
|
||||||
|
LineItem:[
|
||||||
|
{Value: descr},
|
||||||
|
{Value: symbol},
|
||||||
|
{Value: code},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Currency Details
|
||||||
|
//
|
||||||
|
annotate common.Currencies with @(
|
||||||
|
UI: {
|
||||||
|
HeaderInfo: {
|
||||||
|
TypeName: '{i18n>Currency}',
|
||||||
|
TypeNamePlural: '{i18n>Currencies}',
|
||||||
|
Title: {Value: descr},
|
||||||
|
Description: {Value: code}
|
||||||
|
},
|
||||||
|
Facets: [
|
||||||
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
|
||||||
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Extended}', Target: '@UI.FieldGroup#Extended'},
|
||||||
|
],
|
||||||
|
FieldGroup#Details: {
|
||||||
|
Data: [
|
||||||
|
{Value: name},
|
||||||
|
{Value: symbol},
|
||||||
|
{Value: code},
|
||||||
|
{Value: descr}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
FieldGroup#Extended: {
|
||||||
|
Data: [
|
||||||
|
{Value: numcode},
|
||||||
|
{Value: minor},
|
||||||
|
{Value: exponent}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Currencies Elements
|
||||||
|
//
|
||||||
|
annotate common.Currencies with {
|
||||||
|
numcode @title:'{i18n>NumCode}';
|
||||||
|
minor @title:'{i18n>MinorUnit}';
|
||||||
|
exponent @title:'{i18n>Exponent}';
|
||||||
|
}
|
||||||
12
suppliers/app/services.cds
Normal file
12
suppliers/app/services.cds
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
This model controls what gets served to Fiori frontends...
|
||||||
|
*/
|
||||||
|
|
||||||
|
using from './admin/fiori-service';
|
||||||
|
//using from './browse/fiori-service';
|
||||||
|
using from './common';
|
||||||
|
|
||||||
|
using from '@capire/common';
|
||||||
|
|
||||||
|
// only works in case of embedded orders service
|
||||||
|
using from '@capire/orders/app/orders/fiori-service';
|
||||||
1
suppliers/model.drawio
Normal file
1
suppliers/model.drawio
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<mxfile host="Electron" modified="2021-06-16T08:41:31.745Z" agent="5.0 (Macintosh; Intel Mac OS X 11_4_0) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.6.13 Chrome/89.0.4389.128 Electron/12.0.7 Safari/537.36" etag="keOnkQccb51SB06o_xsB" version="14.6.13" type="device"><diagram id="kIZwyAu_b989F7xyfxsf" name="Page-1">7Z1bc6M2GIZ/TabthXdAEpK4zGbTwzSdbutOu3u1g0G2mWBwAcdJf31FQNhI2NY6BuSWm10ksDDo4TvpxbmBd6vnH1JvvfwlCVh0A6zg+QZ+uAHAti3M/yt6Xqoe4pKyZ5GGQdln7Tqm4T+sOlD0bsKAZVVf2ZUnSZSH62ann8Qx8/NGn5emybZ52DyJgkbH2luwxtcoOqa+FzHlsL/CIF+WvRSQXf+PLFwsxZlt7JZ7Vp44uBo4W3pBst3rgvc38C5NkrzcWj3fsai4e8378v2BvfUXS1mc63zA/Xj/2wZtYn/rfdnmf9wv8gxOqlGevGhTXfCUj4B41/tNFsYsyz56aR6z9KYYHUf8TO9nvIEXxZboydZeXFxh/lLdNvz3pris9/Mkzifb6v7c8kPiJF150e4AMdK3fN/vbJXkjG9MWfoU+nwLe6s13xnPsuK/78Tp+Dcszyi+BWicHKTJJg5Ycdk2371dhjmbrj2/2LvlmPK+Zb6Kqt3ZI8v9YqYs3nhiaR7yyb+NwkXM+/KkONqrWhGbF+fzUr/ilJTX7ofxgrforvXweuQHXN2CafXdxN3mZ2HPB6fRruHgjxVLVixPX/gh1QeAWwFWPVEurNrbHZ7ARWXfch9NpwLRqx6JRT32jhq+UYHTDtGXT7/gzfa36f2E/BxMH371pg94golC0UPiF9NsffByb+Zl7BLsIMtqByereHk9Kpjxf/4zqChctNBzEBUM7QYqNkAKKsgmKirQsjtCRRhDLYPzzWhxTMCopqHCiFCsaXEQ6Qoj1W/VrIhJuv2iEHWIJv4dZnJfED61Q7fDJ1ytE34TA755+/Gno2an7m4MezGKBC8+n2V+oU0GQNEOo+guiZL09UxwTn3m+8UoeZo8sr09M+ogx6r3iJgH1N/3bShh0HReVLVINnVVlHBXvguA0yRNN+t1FLI0uzhBJTbZZpaxnG9ssleYrgejOcPtGAXEnVm9YUSxPTRGsAUjaV5YHNwWGQlvzaLEf+T3gndVht3GZfP7sDjx62zszdLBe5Ylm9RnGnznXrpguYZJZYFIiA7Mwf49FvczZZGXh0/NnKntJlfDfUzCON/NJxGBSTWfDoTNIcrLrD61n+NIA3GX0wTDlSa8vA3KQK9zXl/jGzBACga3wSqMd/FFp/GMiLrr042BcGsEg62WQJg4LRGMg7syGI5Cyh3PlaJkcUFWDmZMIyjtoNTVqSOg8JSpV1CwAsqYXBuACgans6K+k2u1DnMwlv0pnicXD2dTxsf2vTIlCjid1xTMBh6j89ZgFvuUzeZdBbNExI7HciLc4py6C2Zpp8Esv1Xpy6f9xuf9xofn6iNl60W0nsP80972573t3UeKhvhE4GXLmpw3RNBEN4IGw0XQkEqJtWxkdCNoCMjxgbqOoN3T7GVLb11s+i9RyI1DCk9bhllpRh5mdYfnPy5ejcuvm5wPw4QJKQlGsjW5wIPuulLWaqlZq/D2+8856Oo5h2oxVvEX75Pk8fJ1j6iKYEYfcZ6PaC149OojxNLCHjtZI7Q47C+SNYtlj7DvKqQw73zLLfg+bbmJpuWupmNivUNAxHvCUCoTItbb9ufDuYy5l1d2XAucZ+4pbBZMbBtIIx2w97uRxIHJfP5axby8T4DgIGtv4awPsHRDgh1YPOVseuCJDfpEi0J6mUiC0vMiiV7Jaivb9lb9n3PnAaw/i3P/yKL1VTlCh9EAtTlCCmYQ464cISRNe4Wx+nD07AjVku8xo9Rf5V+wbXTegokU2FBpyVjX2jjEPj5Qx3kLVOu5PcXS7HmdXNm64VDWwyHNmM0A66FWdxVsvn0Is4L0ZM4n2BohGhgiKsXeBkDUVvc1wgU5mi5IBNRDuCBE8DunMaMEofOckDoUdXp2Q92Wbt+AAr4CFAjE75BlE4hsC1lIUpmcjYULrHeOSwF2qOu4jpRg9Y0Iaqv6DZJFa1sHqInELot2MGnKpCe2ujzXYRZNoLSoc24W7WC50mNeFi0WxMe3CK5m9RraTawmiELl+WgV9Xa3fI1Ai2EaRb3mi3pJ009OuEtTWOpXjokGreuNqt7LcIQsOjRHphb3kG5xTxjVYcLpZrF2QqCkktPW9UoCYZ7yulph0MVCHLW8Nwp7DQxjMFZdT8/KXqSW9EZl7+CkUIucJqVnaS8a35s1khVZXdGaHPWs7XU0qjZD6OIMEUg5wJGmbGhxnaPxiuoorjOBHblW15539Lqi46j567CKJ0Hz6Xzj62u1gyueULMgP4GYNsfQljxJrw+pIxlQrXXactpR86Rhn4bSPFEp+cUilh3OPrWpXUyoiwi4L2enuhA9Ucnh1G9vfPXqELVPjNRxXcTR0K+MipWhZU+SVN0EA6LxquKoezKKIorkWtvwFJmqdnF01S4irh5E+ERltdKkTqO/WvmkjoVEFN+XK9J4c3AYGHRfIB0SBoKOSZ/OB8OFx7RPvUOCTRE/6VsIpAmFQeInJMe35ybUsqrfyIQaq5W+Uf5kdoVfXjds+2XMntVPwuKO6qfrVj9B4VEGE63gUfx0feInIv2ckAEYmap9wrraJ2FSjdA+2eIXAt6qfQJALwS6WHgzSp+uIYSpiyHDKZ/wqHwyEBRF+dQCSs/CJ6xRt/sfi1moHFKKH0AZSsuC20pro5blCrQstvhrIYPV6IlacRtWyiJgPh0+Xr+UBcAzg05ZyaIMZEDdjah1t1HIYvIKIhz8l02FYzUupxUsX84o9aBbAecuCsmyFWWgjlNaolEhG/UGpqhWhrMWGkrJUaRiFDS1SGU4aEyVRhLtsqk7nItRdSTnSiPVkWy3Zy/Tpo00ggRdkeyQJBwXpJxLxXE5Su+EfN0Pd3WXE2vbBhGnXrEa5eycWBaj9JsT8+bubwWXh+/+5DK8/xc=</diagram></mxfile>
|
||||||
32
suppliers/package.json
Normal file
32
suppliers/package.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "@capire/suppliers",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Shows integration with SAP S/4HANA, in turn provided as a reusable extension package to bookshop.",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@capire/common": "*",
|
||||||
|
"@sap/cds": ">=4",
|
||||||
|
"express": "^4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "cds run --in-memory?",
|
||||||
|
"watch": "cds watch",
|
||||||
|
"mocked-s4": "cds mock API_BUSINESS_PARTNER"
|
||||||
|
},
|
||||||
|
"cds": {
|
||||||
|
"requires": {
|
||||||
|
"API_BUSINESS_PARTNER": {
|
||||||
|
"kind": "odata",
|
||||||
|
"model": "srv/external/API_BUSINESS_PARTNER",
|
||||||
|
"[hybrid]": {
|
||||||
|
"credentials": {
|
||||||
|
"destination": "cap-api098",
|
||||||
|
"path": "/sap/opu/odata/sap/API_BUSINESS_PARTNER"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"[local-hybrid]": { "messaging": { "kind": "file-based-messaging" }},
|
||||||
|
"[hybrid]": { "messaging": { "kind": "enterprise-messaging-shared" }}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
suppliers/requests.http
Normal file
51
suppliers/requests.http
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
|
||||||
|
@server = http://localhost:4006
|
||||||
|
@bpServer = http://localhost:5001
|
||||||
|
@authAlice = Authorization: Basic alice:
|
||||||
|
|
||||||
|
###
|
||||||
|
### Replication on changed Business Partner
|
||||||
|
###
|
||||||
|
|
||||||
|
### 1. Check supplier name before update ("A Company Making Everything")
|
||||||
|
|
||||||
|
GET {{server}}/admin/Books(299)?$expand=supplier
|
||||||
|
{{authAlice}}
|
||||||
|
|
||||||
|
### 2. Change Business Partner -> Triggers event that updates supplier replication
|
||||||
|
|
||||||
|
PATCH {{bpServer}}/api-business-partner/A_BusinessPartner('ACME')
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"BusinessPartnerFullName": "A Company Making Everything *better*"
|
||||||
|
}
|
||||||
|
|
||||||
|
### 3. Check supplier name after update ("A Company Making Everything *better*")
|
||||||
|
|
||||||
|
GET {{server}}/admin/Books(299)?$expand=supplier
|
||||||
|
{{authAlice}}
|
||||||
|
|
||||||
|
###
|
||||||
|
### Replication on new assigned supplier
|
||||||
|
###
|
||||||
|
|
||||||
|
### 1. No supplier is assigned to "Wuthering Heights"
|
||||||
|
|
||||||
|
GET {{server}}/admin/Books(201)?$expand=supplier
|
||||||
|
{{authAlice}}
|
||||||
|
|
||||||
|
### 2. Assign supplier ID "PNG"
|
||||||
|
|
||||||
|
PATCH {{server}}/admin/Books(201)
|
||||||
|
{{authAlice}}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"supplier_ID": "PNG"
|
||||||
|
}
|
||||||
|
|
||||||
|
### 3. Supplier information is replicated
|
||||||
|
|
||||||
|
GET {{server}}/admin/Books(201)?$expand=supplier
|
||||||
|
{{authAlice}}
|
||||||
3
suppliers/server.js
Normal file
3
suppliers/server.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
const cds = require ('@sap/cds')
|
||||||
|
cds.once('served', require('./srv/mashup'))
|
||||||
|
module.exports = cds.server
|
||||||
2425
suppliers/srv/external/API_BUSINESS_PARTNER.csn
vendored
Normal file
2425
suppliers/srv/external/API_BUSINESS_PARTNER.csn
vendored
Normal file
File diff suppressed because it is too large
Load Diff
72
suppliers/srv/mashup.cds
Normal file
72
suppliers/srv/mashup.cds
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
using { API_BUSINESS_PARTNER as S4 } from './external/API_BUSINESS_PARTNER.csn';
|
||||||
|
using { CatalogService, sap.capire.bookshop.Books } from '@capire/bookshop';
|
||||||
|
namespace sap.capire.bookshop;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
Add projections to external entities to capture the subset of fields we're
|
||||||
|
actually interested in. This fosters both: (a) minimized network traffic as
|
||||||
|
well as (b) options to dynamically add extension fields by SaaS tenants.
|
||||||
|
*/
|
||||||
|
entity Suppliers as projection on S4.A_BusinessPartner {
|
||||||
|
key BusinessPartner as ID,
|
||||||
|
BusinessPartnerFullName as name,
|
||||||
|
// to_BusinessPartnerAddress[1: ValidityStartDate <= $now and $now < ValidityEndDate].CityName,
|
||||||
|
// to_BusinessPartnerAddress[1: ValidityStartDate <= $now and $now < ValidityEndDate].Country,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
We can mashup entities from external services, or projections thereof, with
|
||||||
|
our project's own entities, e.g. by adding relationships as below.
|
||||||
|
*/
|
||||||
|
extend Books with {
|
||||||
|
supplier : Association to Suppliers;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Optionally expose external entities through own services, e.g. for Value Helps,
|
||||||
|
or to display details fetched on demand.
|
||||||
|
For this to work, we need to delegate the respective calls addressed to our
|
||||||
|
services into calls to the external service.
|
||||||
|
*/
|
||||||
|
extend service AdminService with {
|
||||||
|
@cds.odata.valuelist
|
||||||
|
entity Suppliers as projection on bookshop.Suppliers;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Optionally add local persistency to replicate data for fast access,
|
||||||
|
e.g. to display lists containing remote data.
|
||||||
|
*/
|
||||||
|
annotate Suppliers with @cds.persistence: {table,skip:false};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
Having locally cached replicas also allows to display supplier data in lists
|
||||||
|
of books, which otherwise would generate unwanted traffic on S4 backends.
|
||||||
|
*/
|
||||||
|
extend projection CatalogService.ListOfBooks with {
|
||||||
|
supplier.name as supplier
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
Optionally declare events emitted from the source, but not included in
|
||||||
|
imported APIs (e.g. as in case of EDMXes from API Hub).
|
||||||
|
|
||||||
|
This allows CAP's advanced support for events and messaging to kick in,
|
||||||
|
e.g. to automatically emit to and subscribe to events from message brokers
|
||||||
|
behind the scenes.
|
||||||
|
|
||||||
|
Note: as sync and async APIs from S/4 sources are not correlated, we have
|
||||||
|
to specify the event type names, e.g. as be found at:
|
||||||
|
https://api.sap.com/event/SAPS4HANACloudBusinessEvents_BusinessPartner/resource
|
||||||
|
*/
|
||||||
|
extend service S4 {
|
||||||
|
event BusinessPartner.Changed @(type: 'sap.s4.beh.businesspartner.v1.BusinessPartner.Changed.v1') {
|
||||||
|
BusinessPartner: S4.A_BusinessPartner:BusinessPartner;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
suppliers/srv/mashup.js
Normal file
50
suppliers/srv/mashup.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Mashing up provided and required services...
|
||||||
|
//
|
||||||
|
module.exports = async()=>{ // called by server.js
|
||||||
|
|
||||||
|
if (!cds.services.AdminService) return //> mocking S4 service only
|
||||||
|
|
||||||
|
// Connect to services we want to mashup below...
|
||||||
|
const S4bupa = await cds.connect.to('API_BUSINESS_PARTNER') //> external S4 service
|
||||||
|
const admin = await cds.connect.to('AdminService') //> local domain service
|
||||||
|
const db = await cds.connect.to('db') //> our primary database
|
||||||
|
|
||||||
|
// Reflect CDS definition of the Suppliers entity
|
||||||
|
const { Suppliers } = db.entities
|
||||||
|
|
||||||
|
admin.prepend (()=>{ //> to ensure our .on handlers below go before the default ones
|
||||||
|
|
||||||
|
// Delegate Value Help reads for Suppliers to S4 backend
|
||||||
|
admin.on ('READ', 'Suppliers', req => {
|
||||||
|
console.log ('>> delegating to S4 service...')
|
||||||
|
return S4bupa.run(req.query)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Replicate Supplier data when edited Books have suppliers
|
||||||
|
admin.after (['CREATE','UPDATE'], 'Books', async data => {
|
||||||
|
const { supplier_ID: ID } = data
|
||||||
|
if (ID) {
|
||||||
|
let replicated = await db.exists (Suppliers,ID)
|
||||||
|
if (!replicated) { // initially replicate Supplier info
|
||||||
|
let supplier = await S4bupa.read (Suppliers,ID)
|
||||||
|
await INSERT(supplier) .into (Suppliers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// Subscribe to changes in the S4 origin of Suppliers data
|
||||||
|
S4bupa.on ('BusinessPartner.Changed', async msg => { //> would be great if we had batch events from S/4
|
||||||
|
console.log(">>", msg.event, msg.data)
|
||||||
|
const ID = msg.data.BusinessPartner;
|
||||||
|
let replicated = await db.exists (Suppliers,ID)
|
||||||
|
if (replicated) { // update replicated Supplier info
|
||||||
|
let supplier = await S4bupa.read (Suppliers,ID)
|
||||||
|
await UPDATE(Suppliers,ID) .with (supplier)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
BusinessPartner;BusinessPartnerFullName
|
||||||
|
ACME;A Company Making Everything
|
||||||
|
B4U;Books for You
|
||||||
|
S&C;Shakespeare & Co.
|
||||||
|
WSL;Waterstones
|
||||||
|
TLD;Thalia
|
||||||
|
PNG;Penguin Books
|
||||||
|
2
suppliers/test/data/sap.capire.bookshop-Books.csv
Normal file
2
suppliers/test/data/sap.capire.bookshop-Books.csv
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ID;title;descr;author_ID;stock;price;currency_code;genre_ID;supplier_ID;
|
||||||
|
299;Mobby Dick;"""Moby-Dick""" or """The Whale""" is an 1851 novel by American writer Herman Melville. The book is the sailor Ishmael's narrative of the obsessive quest of Ahab, captain of the whaling ship Pequod, for revenge on Moby Dick, the giant white sperm whale that on the ship's previous voyage bit off Ahab's leg at the knee. A contribution to the literature of the American Renaissance, Moby-Dick was published to mixed reviews, was a commercial failure, and was out of print at the time of the author's death in 1891.;105;99;15.20;GBP;11;ACME
|
||||||
|
Can't render this file because it contains an unexpected character in line 2 and column 30.
|
3
suppliers/test/data/sap.capire.bookshop-Suppliers.csv
Normal file
3
suppliers/test/data/sap.capire.bookshop-Suppliers.csv
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ID;name
|
||||||
|
ACME;A Company Making Everything (replicated)
|
||||||
|
|
||||||
|
1
suppliers/test/index.cds
Normal file
1
suppliers/test/index.cds
Normal file
@@ -0,0 +1 @@
|
|||||||
|
using from '../srv/mashup';
|
||||||
7
suppliers/test/mocked.js
Normal file
7
suppliers/test/mocked.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
let { API_BUSINESS_PARTNER:S4 } = cds.services
|
||||||
|
|
||||||
|
// Mocking event emitter according to:
|
||||||
|
// https://api.sap.com/event/SAPS4HANACloudBusinessEvents_BusinessPartner/resource
|
||||||
|
S4.after ('UPDATE', 'A_BusinessPartner', data =>
|
||||||
|
S4.emit ('BusinessPartner.Changed', { BusinessPartner: data.BusinessPartner })
|
||||||
|
)
|
||||||
22
test/cds.events.test.js
Normal file
22
test/cds.events.test.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const cds = require ('@sap/cds/lib')
|
||||||
|
const { expect } = cds.test.in (__dirname)
|
||||||
|
|
||||||
|
describe('cds.events tests', ()=>{
|
||||||
|
|
||||||
|
let m; before (async ()=> m = await cds.load('events.cds'))
|
||||||
|
|
||||||
|
it ('should have the model loaded', ()=>{
|
||||||
|
expect(m.definitions).to.have.property('Sue.Foo')
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('should compile the model to edmx', ()=>{
|
||||||
|
const edmx = cds.compile(m).to.edmx({service:'Sue'})
|
||||||
|
expect(edmx).to.match(/<EntitySet Name="Foo" EntityType="Sue.Foo"\/>/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('should compile the model to sql', ()=>{
|
||||||
|
const sql = cds.compile(m).to.sql().join(';\n')
|
||||||
|
expect(sql).not.to.match(/CREATE TABLE Sue_Foo/)
|
||||||
|
expect(sql).to.match(/CREATE TABLE Sue_Bar/)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,267 +1,96 @@
|
|||||||
|
const { expect } = require('../test')
|
||||||
const cds = require('@sap/cds/lib')
|
const cds = require('@sap/cds/lib')
|
||||||
const { expect } = cds.test
|
const CQL = ([cql]) => cds.parse.cql(cql)
|
||||||
const { cdr } = cds.ql
|
|
||||||
const Foo = { name: 'Foo' }
|
const Foo = { name: 'Foo' }
|
||||||
const Books = { name: 'capire.bookshop.Books' }
|
const Books = { name: 'capire.bookshop.Books' }
|
||||||
|
|
||||||
|
const { parse:cdr } = cds.ql
|
||||||
|
|
||||||
const STAR = cdr ? '*' : { ref: ['*'] }
|
// while jest has 'test' as alias to 'it', mocha doesn't
|
||||||
const skip = {to:{eql:()=>skip}}
|
if (!global.test) global.test = it
|
||||||
const srv = new cds.Service
|
|
||||||
let cqn
|
|
||||||
|
|
||||||
expect.plain = (cqn) => !cqn.SELECT.one && !cqn.SELECT.distinct ? expect(cqn) : skip
|
|
||||||
expect.one = (cqn) => !cqn.SELECT.distinct ? expect(cqn) : skip
|
|
||||||
|
|
||||||
describe('cds.ql → cqn', () => {
|
describe('cds.ql → cqn', () => {
|
||||||
//
|
//
|
||||||
|
let cqn
|
||||||
|
|
||||||
for (let each of ['SELECT', 'SELECT one', 'SELECT distinct']) {
|
describe.skip(`BUGS + GAPS...`, () => {
|
||||||
let SELECT; beforeEach(()=> SELECT = (
|
|
||||||
each === 'SELECT distinct' ? cds.ql.SELECT.distinct :
|
|
||||||
each === 'SELECT one' ? cds.ql.SELECT.one :
|
|
||||||
cds.ql.SELECT
|
|
||||||
))
|
|
||||||
describe(`${each}...`, () => {
|
|
||||||
|
|
||||||
test(`from Foo`, () => {
|
it('should consistently handle *', () => {
|
||||||
expect(cqn = SELECT `from Foo`)
|
expect({
|
||||||
.to.eql(SELECT.from `Foo`)
|
SELECT: { from: { ref: ['Foo'] }, columns: ['*'] },
|
||||||
.to.eql(SELECT.from('Foo'))
|
|
||||||
.to.eql(SELECT.from(Foo))
|
|
||||||
.to.eql(SELECT`Foo`)
|
|
||||||
.to.eql(SELECT('Foo'))
|
|
||||||
.to.eql(SELECT(Foo))
|
|
||||||
expect.plain(cqn)
|
|
||||||
.to.eql(CQL`SELECT from Foo`)
|
|
||||||
.to.eql(srv.read `Foo`)
|
|
||||||
.to.eql(srv.read('Foo'))
|
|
||||||
.to.eql(srv.read(Foo))
|
|
||||||
.to.eql({
|
|
||||||
SELECT: { from: { ref: ['Foo'] } },
|
|
||||||
})
|
})
|
||||||
|
.to.eql(CQL`SELECT * from Foo`)
|
||||||
|
.to.eql(CQL`SELECT from Foo{*}`)
|
||||||
|
.to.eql(SELECT('*').from(Foo))
|
||||||
|
.to.eql(SELECT.from(Foo,['*']))
|
||||||
})
|
})
|
||||||
|
|
||||||
if (each === 'SELECT')
|
|
||||||
test('SELECT ( Foo )', () => {
|
it('should consistently handle lists', () => {
|
||||||
expect({
|
const ID = 11, args = [`foo`, "'bar'", 3]
|
||||||
SELECT: { from: { ref: ['Foo'] } },
|
const cqn = CQL`SELECT from Foo where ID=11 and x in (foo,'bar',3)`
|
||||||
})
|
expect(SELECT.from(Foo).where(`ID=${ID} and x in (${args})`)).to.eql(cqn)
|
||||||
.to.eql(CQL`SELECT from Foo`)
|
expect(SELECT.from(Foo).where(`ID=`, ID, `and x in`, args)).to.eql(cqn)
|
||||||
.to.eql(SELECT(Foo))
|
expect(SELECT.from(Foo).where({ ID, x:args })).to.eql(cqn)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (each === 'SELECT')
|
})
|
||||||
test('SELECT ( Foo ) .from ( Bar )', () => {
|
|
||||||
|
|
||||||
expect({
|
|
||||||
SELECT: { columns:[{ref:['Foo']}], from: { ref: ['Bar'] } },
|
|
||||||
})
|
|
||||||
.to.eql(CQL`SELECT Foo from Bar`)
|
|
||||||
.to.eql(SELECT `Foo` .from `Bar`)
|
|
||||||
.to.eql(SELECT `Foo` .from('Bar'))
|
|
||||||
.to.eql(SELECT('Foo').from('Bar'))
|
|
||||||
.to.eql(SELECT(['Foo']).from('Bar'))
|
|
||||||
.to.eql(SELECT(['Foo']).from('Bar'))
|
|
||||||
.to.eql(SELECT `Bar` .columns `Foo`)
|
|
||||||
.to.eql(SELECT `Bar` .columns ('Foo'))
|
|
||||||
.to.eql(SELECT `Bar` .columns (['Foo']))
|
|
||||||
.to.eql(SELECT.from `Bar` .columns ('Foo'))
|
|
||||||
.to.eql(SELECT.from `Bar` .columns (['Foo']))
|
|
||||||
|
|
||||||
expect({
|
|
||||||
SELECT: { columns:[
|
|
||||||
{ref:['Foo']},
|
|
||||||
{ref:['Boo']},
|
|
||||||
], from: { ref: ['Bar'] } },
|
|
||||||
})
|
|
||||||
.to.eql(CQL`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(['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.from `Bar` .columns ('Foo','Boo'))
|
|
||||||
.to.eql(SELECT.from `Bar` .columns (['Foo','Boo']))
|
|
||||||
|
|
||||||
expect({
|
|
||||||
SELECT: { columns:[
|
|
||||||
{ref:['Foo']},
|
|
||||||
{ref:['Boo']},
|
|
||||||
{ref:['Moo']},
|
|
||||||
], from: { ref: ['Bar'] } },
|
|
||||||
})
|
|
||||||
.to.eql(CQL`SELECT Foo, Boo, Moo from Bar`)
|
|
||||||
.to.eql(SELECT `Foo, Boo, Moo` .from `Bar`)
|
|
||||||
.to.eql(SELECT `Foo, Boo, Moo` .from('Bar'))
|
|
||||||
.to.eql(SELECT('Foo','Boo','Moo').from('Bar'))
|
|
||||||
.to.eql(SELECT(['Foo','Boo','Moo']).from('Bar'))
|
|
||||||
.to.eql(SELECT `Bar` .columns `Foo, Boo, Moo`)
|
|
||||||
.to.eql(SELECT `Bar` .columns ('Foo','Boo','Moo'))
|
|
||||||
.to.eql(SELECT `Bar` .columns (['Foo','Boo','Moo']))
|
|
||||||
.to.eql(SELECT.from `Bar` .columns ('Foo','Boo','Moo'))
|
|
||||||
.to.eql(SELECT.from `Bar` .columns (['Foo','Boo','Moo']))
|
|
||||||
|
|
||||||
|
|
||||||
expect({
|
describe(`SELECT...`, () => {
|
||||||
SELECT: { one:true, columns:[{ref:['Foo']}], from: { ref: ['Bar'] } },
|
|
||||||
})
|
|
||||||
// .to.eql(CQL`SELECT one Foo from Bar`)
|
|
||||||
.to.eql(SELECT.one `Foo` .from `Bar`)
|
|
||||||
.to.eql(SELECT.one `Foo` .from('Bar'))
|
|
||||||
.to.eql(SELECT.one('Foo').from('Bar'))
|
|
||||||
.to.eql(SELECT.one(['Foo']).from('Bar'))
|
|
||||||
.to.eql(SELECT.one(['Foo']).from('Bar'))
|
|
||||||
.to.eql(SELECT.one('Bar',['Foo']))
|
|
||||||
.to.eql(SELECT.one `Bar` .columns `Foo`)
|
|
||||||
.to.eql(SELECT.one('Bar').columns('Foo'))
|
|
||||||
.to.eql(SELECT.one('Bar').columns(['Foo']))
|
|
||||||
.to.eql(SELECT.one.from('Bar',['Foo']))
|
|
||||||
.to.eql(SELECT.one.from('Bar').columns('Foo'))
|
|
||||||
.to.eql(SELECT.one.from('Bar').columns(['Foo']))
|
|
||||||
|
|
||||||
expect({
|
|
||||||
SELECT: { one:true, columns:[
|
|
||||||
{ref:['Foo']},
|
|
||||||
{ref:['Boo']},
|
|
||||||
], from: { ref: ['Bar'] } },
|
|
||||||
})
|
|
||||||
// .to.eql(CQL`SELECT Foo, Boo from Bar`)
|
|
||||||
.to.eql(SELECT.one `Foo, Boo` .from `Bar`)
|
|
||||||
.to.eql(SELECT.one `Foo, Boo` .from('Bar'))
|
|
||||||
.to.eql(SELECT.one('Foo','Boo').from('Bar'))
|
|
||||||
.to.eql(SELECT.one(['Foo','Boo']).from('Bar'))
|
|
||||||
.to.eql(SELECT.one('Bar',['Foo','Boo']))
|
|
||||||
.to.eql(SELECT.one `Bar` .columns `Foo, Boo`)
|
|
||||||
.to.eql(SELECT.one('Bar').columns('Foo','Boo'))
|
|
||||||
.to.eql(SELECT.one('Bar').columns(['Foo','Boo']))
|
|
||||||
.to.eql(SELECT.one.from('Bar',['Foo','Boo']))
|
|
||||||
.to.eql(SELECT.one.from('Bar').columns('Foo','Boo'))
|
|
||||||
.to.eql(SELECT.one.from('Bar').columns(['Foo','Boo']))
|
|
||||||
|
|
||||||
expect({
|
|
||||||
SELECT: { one:true, columns:[
|
|
||||||
{ref:['Foo']},
|
|
||||||
{ref:['Boo']},
|
|
||||||
{ref:['Moo']},
|
|
||||||
], from: { ref: ['Bar'] } },
|
|
||||||
})
|
|
||||||
// .to.eql(CQL`SELECT Foo, Boo, Moo from Bar`)
|
|
||||||
.to.eql(SELECT.one `Foo, Boo, Moo` .from `Bar`)
|
|
||||||
.to.eql(SELECT.one `Foo, Boo, Moo` .from('Bar'))
|
|
||||||
.to.eql(SELECT.one('Foo','Boo','Moo').from('Bar'))
|
|
||||||
.to.eql(SELECT.one(['Foo','Boo','Moo']).from('Bar'))
|
|
||||||
.to.eql(SELECT.one('Bar',['Foo','Boo','Moo']))
|
|
||||||
.to.eql(SELECT.one `Bar` .columns `Foo, Boo, Moo`)
|
|
||||||
.to.eql(SELECT.one('Bar').columns('Foo','Boo','Moo'))
|
|
||||||
.to.eql(SELECT.one('Bar').columns(['Foo','Boo','Moo']))
|
|
||||||
.to.eql(SELECT.one.from('Bar',['Foo','Boo','Moo']))
|
|
||||||
.to.eql(SELECT.one.from('Bar').columns('Foo','Boo','Moo'))
|
|
||||||
.to.eql(SELECT.one.from('Bar').columns(['Foo','Boo','Moo']))
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
if (each === 'SELECT')
|
|
||||||
test('from ( Foo )', () => {
|
test('from ( Foo )', () => {
|
||||||
expect({
|
expect({
|
||||||
SELECT: { from: {ref: [{ id:'Foo', where: [{val:11}] }] }}
|
SELECT: { from: { ref: ['Foo'] } },
|
||||||
})
|
})
|
||||||
.to.eql(srv.read`Foo[${11}]`)
|
.to.eql(CQL`SELECT from Foo`)
|
||||||
.to.eql(SELECT`Foo[${11}]`)
|
.to.eql(SELECT.from(Foo))
|
||||||
|
})
|
||||||
|
|
||||||
expect((cqn = SELECT`from Foo[ID=11]`))
|
test('from ( ..., <key>)', () => {
|
||||||
.to.eql(SELECT`from Foo[ID=${11}]`)
|
// Compiler
|
||||||
.to.eql(SELECT.from `Foo[ID=11]`)
|
expect(CQL`SELECT from Foo[11]`).to.eql({
|
||||||
.to.eql(SELECT.from `Foo[ID=${11}]`)
|
SELECT: {
|
||||||
.to.eql(SELECT`Foo[ID=11]`)
|
// REVISIT: add one:true?
|
||||||
expect.plain(cqn)
|
from: { ref: [{ id: 'Foo', where: [{ val: 11 }] }] },
|
||||||
.to.eql(CQL`SELECT from Foo[ID=11]`)
|
},
|
||||||
.to.eql(srv.read`Foo[ID=11]`)
|
|
||||||
.to.eql({
|
|
||||||
SELECT: { from: {
|
|
||||||
ref: [{ id: 'Foo', where: [{ ref: ['ID'] }, '=', { val: 11 }] }],
|
|
||||||
}},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (cdr) expect.plain (cqn)
|
expect(CQL`SELECT from Foo[ID=11]`).to.eql({
|
||||||
.to.eql(SELECT`Foo[ID=${11}]`)
|
SELECT: {
|
||||||
.to.eql(srv.read`Foo[ID=${11}]`)
|
// REVISIT: add one:true
|
||||||
|
from: {
|
||||||
// Following implicitly resolve to SELECT.one
|
ref: [{ id: 'Foo', where: [{ ref: ['ID'] }, '=', { val: 11 }] }],
|
||||||
expect(cqn = SELECT.from(Foo,11))
|
|
||||||
.to.eql(SELECT.from(Foo,{ID:11}))
|
|
||||||
.to.eql(SELECT.from(Foo).byKey(11))
|
|
||||||
.to.eql(SELECT.from(Foo).byKey({ID:11}))
|
|
||||||
if (cds.version >= '5.6.0') {
|
|
||||||
expect.one(cqn)
|
|
||||||
.to.eql({
|
|
||||||
SELECT: {
|
|
||||||
one: true,
|
|
||||||
from: { ref: [{ id: 'Foo', where: [{ ref: ['ID'] }, '=', { val: 11 }] }] },
|
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
} else {
|
})
|
||||||
expect.one(cqn)
|
|
||||||
|
// Runtime ds.ql
|
||||||
|
expect(SELECT.from(Foo, 11))
|
||||||
|
.to.eql(SELECT.from(Foo, { ID: 11 }))
|
||||||
|
.to.eql(SELECT.from(Foo).byKey(11))
|
||||||
|
.to.eql(SELECT.from(Foo).byKey({ ID: 11 }))
|
||||||
|
.to.eql(SELECT.one.from(Foo).where({ ID: 11 }))
|
||||||
.to.eql({
|
.to.eql({
|
||||||
|
// REVISIT: should produce CQN as the ones above?
|
||||||
SELECT: {
|
SELECT: {
|
||||||
one: true,
|
one: true,
|
||||||
from: { ref: ['Foo'] },
|
from: { ref: ['Foo'] },
|
||||||
where: [{ ref: ['ID'] }, '=', { val: 11 }],
|
where: [{ ref: ['ID'] }, '=', { val: 11 }],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
})
|
expect(CQL`SELECT from Foo[11]{a}`).to.eql({
|
||||||
|
|
||||||
test('from Foo {...}', () => {
|
|
||||||
|
|
||||||
expect(cqn = SELECT `*,a,b as c` .from `Foo`)
|
|
||||||
.to.eql(SELECT `*,a,b as c`. from(Foo))
|
|
||||||
.to.eql(SELECT('*','a',{b:'c'}).from`Foo`)
|
|
||||||
.to.eql(SELECT('*','a',{b:'c'}).from(Foo))
|
|
||||||
.to.eql(SELECT(['*','a',{b:'c'}]).from(Foo))
|
|
||||||
.to.eql(SELECT.columns('*','a',{b:'c'}).from(Foo))
|
|
||||||
.to.eql(SELECT.columns(['*','a',{b:'c'}]).from(Foo))
|
|
||||||
.to.eql(SELECT.columns((foo) => { foo`.*`, foo.a, foo.b`as c` }).from(Foo))
|
|
||||||
.to.eql(SELECT.columns((foo) => { foo('*'), foo.a, foo.b.as('c') }).from(Foo))
|
|
||||||
.to.eql(SELECT.from(Foo).columns('*','a',{b:'c'}))
|
|
||||||
.to.eql(SELECT.from(Foo).columns(['*','a',{b:'c'}]))
|
|
||||||
.to.eql(SELECT.from(Foo).columns((foo) => { foo`.*`, foo.a, foo.b`as c` }))
|
|
||||||
.to.eql(SELECT.from(Foo).columns((foo) => { foo('*'), foo.a, foo.b.as('c') }))
|
|
||||||
.to.eql(SELECT.from(Foo,['*','a',{b:'c'}]))
|
|
||||||
.to.eql(SELECT.from(Foo, (foo) => { foo`.*`, foo.a, foo.b`as c` }))
|
|
||||||
.to.eql(SELECT.from(Foo, (foo) => { foo('*'), foo.a, foo.b.as('c') }))
|
|
||||||
|
|
||||||
expect.plain(cqn)
|
|
||||||
.to.eql({
|
|
||||||
SELECT: {
|
SELECT: {
|
||||||
from: { ref: ['Foo'] },
|
// REVISIT: add one:true?
|
||||||
columns: [ STAR, { ref: ['a'] }, { ref: ['b'], as: 'c' }],
|
from: { ref: [{ id: 'Foo', where: [{ val: 11 }] }] },
|
||||||
|
columns: [{ ref: ['a'] }],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
cdr && expect.plain(cqn)
|
expect(SELECT.from(Foo, 11, ['a']))
|
||||||
.to.eql(CQL`SELECT *,a,b as c from Foo`)
|
.to.eql(SELECT.from(Foo, 11, (foo) => foo.a))
|
||||||
.to.eql(CQL`SELECT from Foo {*,a,b as c}`)
|
|
||||||
|
|
||||||
// Test combination with key as second argument to .from
|
|
||||||
expect(cqn = SELECT.from(Foo, 11, ['a']))
|
|
||||||
.to.eql(SELECT.from(Foo, 11, foo => foo.a))
|
|
||||||
|
|
||||||
if (cds.version >= '5.6.0') {
|
|
||||||
expect.one(cqn)
|
|
||||||
.to.eql({
|
|
||||||
SELECT: {
|
|
||||||
one: true,
|
|
||||||
from: { ref: [{ id: 'Foo', where: [{ ref: ['ID'] }, '=', { val: 11 }]}] },
|
|
||||||
columns: [{ ref: ['a'] }]
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
expect.one(cqn)
|
|
||||||
.to.eql({
|
.to.eql({
|
||||||
|
// REVISIT: should produce CQN as the ones above?
|
||||||
SELECT: {
|
SELECT: {
|
||||||
one: true,
|
one: true,
|
||||||
from: { ref: ['Foo'] },
|
from: { ref: ['Foo'] },
|
||||||
@@ -269,56 +98,110 @@ describe('cds.ql → cqn', () => {
|
|||||||
where: [{ ref: ['ID'] }, '=', { val: 11 }],
|
where: [{ ref: ['ID'] }, '=', { val: 11 }],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('with nested expands', () => {
|
test('from ( ..., => {...})', () => {
|
||||||
// SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } }
|
// single *, prefix and postfix, as array and function
|
||||||
expect(cqn =
|
let parsed, fluid
|
||||||
SELECT.from (Foo, foo => {
|
expect((parsed = CQL`SELECT * from Foo`)).to.eql(CQL`SELECT from Foo{*}`)
|
||||||
foo`*`, foo.x, foo.car`*`, foo.boo (b => {
|
//> .to.eql... FIXME: see skipped 'should handle * correctly' below
|
||||||
b`*`, b.moo.zoo(
|
expect((fluid = SELECT('*').from(Foo)))
|
||||||
x => x.y.z
|
.to.eql(SELECT.from(Foo, ['*']))
|
||||||
)
|
.to.eql(SELECT.from(Foo, (foo) => foo('*')))
|
||||||
})
|
.to.eql(SELECT.from(Foo).columns('*'))
|
||||||
|
.to.eql(SELECT.from(Foo).columns((foo) => foo('*')))
|
||||||
|
.to.eql({
|
||||||
|
SELECT: { from: { ref: ['Foo'] }, columns: [cdr ? '*' : { ref: ['*'] }] },
|
||||||
})
|
})
|
||||||
).to.eql(
|
|
||||||
SELECT.from (Foo, foo => {
|
if (cdr) expect(parsed).to.eql(fluid)
|
||||||
foo('*'), foo.x, foo.car('*'), foo.boo (b => {
|
|
||||||
b('*'), b.moo.zoo(
|
// single column, prefix and postfix, as array and function
|
||||||
x => x.y.z
|
expect(CQL`SELECT a from Foo`)
|
||||||
)
|
expect(CQL`SELECT from Foo {a}`)
|
||||||
})
|
.to.eql(SELECT.from(Foo, ['a']))
|
||||||
|
.to.eql(SELECT.from(Foo, (foo) => foo.a))
|
||||||
|
.to.eql({
|
||||||
|
SELECT: { from: { ref: ['Foo'] }, columns: [{ ref: ['a'] }] },
|
||||||
|
})
|
||||||
|
|
||||||
|
// multiple columns, prefix and postfix, as array and function
|
||||||
|
expect(CQL`SELECT a,b as c from Foo`)
|
||||||
|
|
||||||
|
expect (CQL`SELECT from Foo {a,b as c}`).to.eql(cqn = {
|
||||||
|
SELECT: {
|
||||||
|
from: { ref: ['Foo'] },
|
||||||
|
columns: [{ ref: ['a'] }, { ref: ['b'], as: 'c' }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(SELECT.from(Foo, ['a', { b: 'c' }])).to.eql(cqn)
|
||||||
|
expect(
|
||||||
|
SELECT.from(Foo, (foo) => {
|
||||||
|
foo.a, foo.b.as('c')
|
||||||
|
})
|
||||||
|
).to.eql(cqn)
|
||||||
|
expect(SELECT.from(Foo).columns('a', { b: 'c' })).to.eql(cqn)
|
||||||
|
expect(SELECT.from(Foo).columns(['a', { b: 'c' }])).to.eql(cqn)
|
||||||
|
expect(
|
||||||
|
SELECT.from(Foo).columns((foo) => {
|
||||||
|
foo.a, foo.b.as('c')
|
||||||
|
})
|
||||||
|
).to.eql(cqn)
|
||||||
|
|
||||||
|
// multiple columns and *, prefix and postfix, as array and function
|
||||||
|
expect(CQL`SELECT *,a,b from Foo`).to.eql(CQL`SELECT from Foo{*,a,b}`)
|
||||||
|
//> .to.eql... FIXME: see skipped 'should handle * correctly' below
|
||||||
|
expect(SELECT.from(Foo, ['a', 'b', '*']))
|
||||||
|
.to.eql(SELECT.from(Foo).columns('a', 'b', '*'))
|
||||||
|
.to.eql(SELECT.from(Foo).columns(['a', 'b', '*']))
|
||||||
|
.to.eql(
|
||||||
|
SELECT.from(Foo, (foo) => {
|
||||||
|
foo.a, foo.b, foo('*')
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
expect.plain(cqn)
|
|
||||||
.to.eql({
|
.to.eql({
|
||||||
|
SELECT: {
|
||||||
|
from: { ref: ['Foo'] },
|
||||||
|
columns: [{ ref: ['a'] }, { ref: ['b'] }, cdr ? '*' : { ref: ['*'] }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('from ( ..., => _.expand ( x=>{...}))', () => {
|
||||||
|
// SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } }
|
||||||
|
expect(
|
||||||
|
SELECT.from(Foo, (foo) => {
|
||||||
|
foo('*'),
|
||||||
|
foo.x,
|
||||||
|
foo.car('*'),
|
||||||
|
foo.boo((b) => {
|
||||||
|
b('*'), b.moo.zoo((x) => x.y.z)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
).to.eql({
|
||||||
SELECT: {
|
SELECT: {
|
||||||
from: { ref: ['Foo'] },
|
from: { ref: ['Foo'] },
|
||||||
columns: [
|
columns: [
|
||||||
STAR,
|
cdr ? '*' : { ref: ['*'] },
|
||||||
{ ref: ['x'] },
|
{ ref: ['x'] },
|
||||||
{ ref: ['car'], expand: ['*'] },
|
{ ref: ['car'], expand: ['*'] },
|
||||||
{
|
{
|
||||||
ref: ['boo'],
|
ref: ['boo'],
|
||||||
expand: [ '*', { ref: ['moo', 'zoo'], expand: [{ ref: ['y', 'z'] }] }],
|
expand: ['*', { ref: ['moo', 'zoo'], expand: [{ ref: ['y', 'z'] }] }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('from ( ..., => _.inline ( _=>{...}))', () => {
|
||||||
test('with nested inlines', () => {
|
|
||||||
// SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } }
|
// SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } }
|
||||||
expect.plain(
|
expect(
|
||||||
SELECT.from (Foo, foo => {
|
SELECT.from(Foo, (foo) => {
|
||||||
foo.bar `*`,
|
foo.bar('*'),
|
||||||
foo.bar `.*`, //> leading dot indicates inline
|
foo.bar('.*'), //> leading dot indicates inline
|
||||||
foo.boo(_ => _.moo.zoo), //> underscore arg name indicates inline
|
foo.boo((x) => x.moo.zoo),
|
||||||
foo.boo(x => x.moo.zoo)
|
foo.boo((_) => _.moo.zoo) //> underscore arg name indicates inline
|
||||||
})
|
})
|
||||||
).to.eql({
|
).to.eql({
|
||||||
SELECT: {
|
SELECT: {
|
||||||
@@ -326,142 +209,62 @@ describe('cds.ql → cqn', () => {
|
|||||||
columns: [
|
columns: [
|
||||||
{ ref: ['bar'], expand: ['*'] },
|
{ ref: ['bar'], expand: ['*'] },
|
||||||
{ ref: ['bar'], inline: ['*'] },
|
{ ref: ['bar'], inline: ['*'] },
|
||||||
{ ref: ['boo'], inline: [{ ref: ['moo', 'zoo'] }] },
|
|
||||||
{ ref: ['boo'], expand: [{ ref: ['moo', 'zoo'] }] },
|
{ ref: ['boo'], expand: [{ ref: ['moo', 'zoo'] }] },
|
||||||
|
{ ref: ['boo'], inline: [{ ref: ['moo', 'zoo'] }] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
})}
|
test('one / distinct ...', () => {
|
||||||
|
expect(SELECT.distinct.from(Foo).SELECT)
|
||||||
|
// .to.eql(CQL(`SELECT distinct from Foo`).SELECT)
|
||||||
|
.to.eql(SELECT.distinct(Foo).SELECT)
|
||||||
|
.to.eql({ distinct: true, from: { ref: ['Foo'] } })
|
||||||
|
|
||||||
describe ('SELECT where...', ()=>{
|
expect(SELECT.one.from(Foo).SELECT)
|
||||||
|
// .to.eql(CQL(`SELECT one from Foo`).SELECT)
|
||||||
|
.to.eql(SELECT.one(Foo).SELECT)
|
||||||
|
.to.eql({ one: true, from: { ref: ['Foo'] } })
|
||||||
|
|
||||||
|
expect(SELECT.one('a').from(Foo).SELECT)
|
||||||
|
// .to.eql(CQL(`SELECT distinct a from Foo`).SELECT)
|
||||||
|
.to.eql(SELECT.one(['a']).from(Foo).SELECT)
|
||||||
|
.to.eql(SELECT.one(Foo, ['a']).SELECT)
|
||||||
|
.to.eql(SELECT.one(Foo, (foo) => foo.a).SELECT)
|
||||||
|
.to.eql(SELECT.one.from(Foo, (foo) => foo.a).SELECT)
|
||||||
|
.to.eql(SELECT.one.from(Foo, ['a']).SELECT)
|
||||||
|
.to.eql({
|
||||||
|
one: true,
|
||||||
|
from: { ref: ['Foo'] },
|
||||||
|
columns: [{ ref: ['a'] }],
|
||||||
|
})
|
||||||
|
// same for works distinct
|
||||||
|
})
|
||||||
|
|
||||||
it('should correctly handle { ... and:{...} }', () => {
|
it('should correctly handle { ... and:{...} }', () => {
|
||||||
expect(SELECT.from(Foo).where({ x: 1, and: { y: 2, or: { z: 3 } } })).to.eql({
|
expect(SELECT.from(Foo).where({ x: 1, and: { y: 2, or: { z: 3 } } })).to.eql({
|
||||||
SELECT: {
|
SELECT: {
|
||||||
from: { ref: ['Foo'] },
|
from: { ref: ['Foo'] },
|
||||||
where: cdr ? [
|
where: [
|
||||||
{ ref: ['x'] },
|
|
||||||
'=',
|
|
||||||
{ val: 1 },
|
|
||||||
'and',
|
|
||||||
// '(',
|
|
||||||
{xpr:[
|
|
||||||
{ ref: ['y'] },
|
|
||||||
'=',
|
|
||||||
{ val: 2 },
|
|
||||||
'or',
|
|
||||||
{ ref: ['z'] },
|
|
||||||
'=',
|
|
||||||
{ val: 3 },
|
|
||||||
]},
|
|
||||||
// ')',
|
|
||||||
] : [
|
|
||||||
{ ref: ['x'] },
|
{ ref: ['x'] },
|
||||||
'=',
|
'=',
|
||||||
{ val: 1 },
|
{ val: 1 },
|
||||||
'and',
|
'and',
|
||||||
'(',
|
'(',
|
||||||
// {xpr:[
|
{ ref: ['y'] },
|
||||||
{ ref: ['y'] },
|
'=',
|
||||||
'=',
|
{ val: 2 },
|
||||||
{ val: 2 },
|
'or',
|
||||||
'or',
|
{ ref: ['z'] },
|
||||||
{ ref: ['z'] },
|
'=',
|
||||||
'=',
|
{ val: 3 },
|
||||||
{ val: 3 },
|
|
||||||
// ]},
|
|
||||||
')',
|
')',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test ("where x='*'", ()=>{
|
|
||||||
if (cdr)
|
|
||||||
expect (SELECT.from(Foo).where({x:'*'}))
|
|
||||||
.to.eql(SELECT.from(Foo).where("x='*'"))
|
|
||||||
.to.eql(SELECT.from(Foo).where("x=",'*'))
|
|
||||||
.to.eql(SELECT.from(Foo).where`x=${'*'}`)
|
|
||||||
.to.eql(
|
|
||||||
CQL`SELECT from Foo where x='*'`
|
|
||||||
)
|
|
||||||
if (cdr)
|
|
||||||
expect (SELECT.from(Foo).where({x:['*',1]}))
|
|
||||||
.to.eql(SELECT.from(Foo).where("x in ('*',1)"))
|
|
||||||
.to.eql(SELECT.from(Foo).where("x in",['*',1]))
|
|
||||||
.to.eql(SELECT.from(Foo).where`x in ${['*',1]}`)
|
|
||||||
.to.eql(
|
|
||||||
CQL`SELECT from Foo where x in ('*',1)`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test ('where, and, or', ()=>{
|
|
||||||
expect (
|
|
||||||
SELECT.from(Foo).where({x:1,and:{y:2}})
|
|
||||||
).to.eql (
|
|
||||||
CQL`SELECT from Foo where x=1 and y=2`
|
|
||||||
) .to.eql ({ SELECT: {
|
|
||||||
from: {ref:['Foo']},
|
|
||||||
where: [
|
|
||||||
{ref:['x']}, '=', {val:1},
|
|
||||||
'and',
|
|
||||||
{ref:['y']}, '=', {val:2}
|
|
||||||
]
|
|
||||||
}})
|
|
||||||
|
|
||||||
expect (
|
|
||||||
SELECT.from(Foo).where({x:1,or:{y:2}})
|
|
||||||
).to.eql (
|
|
||||||
CQL`SELECT from Foo where x=1 or y=2`
|
|
||||||
).to.eql ({ SELECT: {
|
|
||||||
from: {ref:['Foo']},
|
|
||||||
where: [
|
|
||||||
{ref:['x']}, '=', {val:1},
|
|
||||||
'or',
|
|
||||||
{ref:['y']}, '=', {val:2}
|
|
||||||
]
|
|
||||||
}})
|
|
||||||
|
|
||||||
expect (
|
|
||||||
SELECT.from(Foo).where({x:1,and:{y:2}}).or({z:3})
|
|
||||||
).to.eql (
|
|
||||||
CQL`SELECT from Foo where x=1 and y=2 or z=3`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (cdr) expect (
|
|
||||||
SELECT.from(Foo).where({x:1}).and({y:2,or:{z:3}})
|
|
||||||
).to.eql (
|
|
||||||
CQL`SELECT from Foo where x=1 and ( y=2 or z=3 )`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (cdr) expect (
|
|
||||||
SELECT.from(Foo).where({1:1}).and({x:1,or:{x:2}}).and({y:2,or:{z:3}})
|
|
||||||
).to.eql (
|
|
||||||
CQL`SELECT from Foo where 1=1 and ( x=1 or x=2 ) and ( y=2 or z=3 )`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (cdr) expect (
|
|
||||||
SELECT.from(Foo).where({x:1,or:{x:2}}).and({y:2,or:{z:3}})
|
|
||||||
).to.eql (
|
|
||||||
CQL`SELECT from Foo where ( x=1 or x=2 ) and ( y=2 or z=3 )`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('where ({x:[undefined]})', () => {
|
|
||||||
if (cdr) expect (
|
|
||||||
SELECT.from(Foo).where({x:[undefined]})
|
|
||||||
).to.eql ({ SELECT: {
|
|
||||||
from: {ref:['Foo']},
|
|
||||||
where: [
|
|
||||||
{ref:['x']},
|
|
||||||
'in',
|
|
||||||
{ list: [ {val:undefined} ] }
|
|
||||||
]
|
|
||||||
}})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('where ( ... cql | {x:y} )', () => {
|
test('where ( ... cql | {x:y} )', () => {
|
||||||
const args = [`foo`, "'bar'", 3]
|
const args = [`foo`, "'bar'", 3]
|
||||||
const ID = 11
|
const ID = 11
|
||||||
@@ -477,17 +280,18 @@ describe('cds.ql → cqn', () => {
|
|||||||
).to.eql({
|
).to.eql({
|
||||||
SELECT: {
|
SELECT: {
|
||||||
from: { ref: ['Foo'] },
|
from: { ref: ['Foo'] },
|
||||||
where: cdr ? [
|
where: cds.version >= '5.3.0'
|
||||||
{ ref: ['ID'] },
|
? [
|
||||||
'=',
|
// '(', //> this one is not required
|
||||||
{ val: ID },
|
{ ref: ['ID'] },
|
||||||
'and',
|
'=',
|
||||||
{ ref: ['args'] },
|
{ val: ID },
|
||||||
'in',
|
'and',
|
||||||
{ list: args.map(val => ({ val })) },
|
{ ref: ['args'] },
|
||||||
'and',
|
'in',
|
||||||
{
|
{ list: args.map(val => ({ val })) },
|
||||||
xpr: [
|
'and',
|
||||||
|
'(', //> this one is missing, and that's changing the logic -> that's a BUG
|
||||||
{ ref: ['x'] },
|
{ ref: ['x'] },
|
||||||
'like',
|
'like',
|
||||||
{ val: '%x%' },
|
{ val: '%x%' },
|
||||||
@@ -495,28 +299,29 @@ describe('cds.ql → cqn', () => {
|
|||||||
{ ref: ['y'] },
|
{ ref: ['y'] },
|
||||||
'>=',
|
'>=',
|
||||||
{ val: 9 },
|
{ val: 9 },
|
||||||
|
')',
|
||||||
]
|
]
|
||||||
},
|
: [
|
||||||
] : [
|
// '(', //> this one is not required
|
||||||
{ ref: ['ID'] },
|
{ ref: ['ID'] },
|
||||||
'=',
|
'=',
|
||||||
{ val: ID },
|
{ val: ID },
|
||||||
'and',
|
'and',
|
||||||
{ ref: ['args'] },
|
{ ref: ['args'] },
|
||||||
'in',
|
'in',
|
||||||
{ list: args.map(val => ({ val })) },
|
{ val: args },
|
||||||
'and',
|
'and',
|
||||||
'(',
|
'(', //> this one is missing, and that's changing the logic -> that's a BUG
|
||||||
{ ref: ['x'] },
|
{ ref: ['x'] },
|
||||||
'like',
|
'like',
|
||||||
{ val: '%x%' },
|
{ val: '%x%' },
|
||||||
'or',
|
'or',
|
||||||
{ ref: ['y'] },
|
{ ref: ['y'] },
|
||||||
'>=',
|
'>=',
|
||||||
{ val: 9 },
|
{ val: 9 },
|
||||||
')',
|
')',
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// using CQL fragments -> uses cds.parse.expr
|
// using CQL fragments -> uses cds.parse.expr
|
||||||
@@ -601,32 +406,12 @@ describe('cds.ql → cqn', () => {
|
|||||||
).to.eql(cqn)
|
).to.eql(cqn)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('w/ plain SQL', () => {
|
it('w/ plain SQL', () => {
|
||||||
expect(SELECT.from(Books) + 'WHERE ...').to.eql(
|
expect(SELECT.from(Books) + 'WHERE ...').to.eql(
|
||||||
'SELECT * FROM capire_bookshop_Books WHERE ...'
|
'SELECT * FROM capire_bookshop_Books WHERE ...'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should consistently handle *', () => {
|
|
||||||
if (!cdr) return
|
|
||||||
expect({
|
|
||||||
SELECT: { from: { ref: ['Foo'] }, columns: ['*'] },
|
|
||||||
})
|
|
||||||
.to.eql(CQL`SELECT * from Foo`)
|
|
||||||
.to.eql(CQL`SELECT from Foo{*}`)
|
|
||||||
.to.eql(SELECT('*').from(Foo))
|
|
||||||
.to.eql(SELECT.from(Foo,['*']))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should consistently handle lists', () => {
|
|
||||||
if (!cdr) return
|
|
||||||
const ID = 11, args = [{ref:['foo']}, "bar", 3]
|
|
||||||
const cqn = CQL`SELECT from Foo where ID=11 and x in (foo,'bar',3)`
|
|
||||||
expect(SELECT.from(Foo).where`ID=${ID} and x in ${args}`).to.eql(cqn)
|
|
||||||
expect(SELECT.from(Foo).where(`ID=`, ID, `and x in`, args)).to.eql(cqn)
|
|
||||||
expect(SELECT.from(Foo).where({ ID, x:args })).to.eql(cqn)
|
|
||||||
})
|
|
||||||
|
|
||||||
//
|
//
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -681,31 +466,21 @@ describe('cds.ql → cqn', () => {
|
|||||||
|
|
||||||
describe(`UPDATE...`, () => {
|
describe(`UPDATE...`, () => {
|
||||||
test('entity (..., <key>)', () => {
|
test('entity (..., <key>)', () => {
|
||||||
const cqnWhere = {
|
|
||||||
UPDATE: {
|
|
||||||
entity: 'capire.bookshop.Books',
|
|
||||||
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
expect(UPDATE(Books).where({ ID: 4711 }))
|
|
||||||
.to.eql(UPDATE(Books).where(`ID=`, 4711))
|
|
||||||
.to.eql(cqnWhere)
|
|
||||||
|
|
||||||
const cqnKey = (cds.version >= '5.6.0') ?
|
|
||||||
{
|
|
||||||
UPDATE: {
|
|
||||||
entity: { ref: [{ id: 'capire.bookshop.Books', where: [{ ref: ['ID'] }, '=', { val: 4711 }] }] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: cqnWhere
|
|
||||||
expect(UPDATE(Books, 4711))
|
expect(UPDATE(Books, 4711))
|
||||||
.to.eql(UPDATE(Books, { ID: 4711 }))
|
.to.eql(UPDATE(Books, { ID: 4711 }))
|
||||||
.to.eql(UPDATE(Books).byKey(4711))
|
.to.eql(UPDATE(Books).byKey(4711))
|
||||||
.to.eql(UPDATE(Books).byKey({ ID: 4711 }))
|
.to.eql(UPDATE(Books).byKey({ ID: 4711 }))
|
||||||
|
.to.eql(UPDATE(Books).where({ ID: 4711 }))
|
||||||
|
.to.eql(UPDATE(Books).where(`ID=`, 4711))
|
||||||
.to.eql(UPDATE.entity(Books, 4711))
|
.to.eql(UPDATE.entity(Books, 4711))
|
||||||
.to.eql(UPDATE.entity(Books, { ID: 4711 }))
|
.to.eql(UPDATE.entity(Books, { ID: 4711 }))
|
||||||
// etc...
|
// etc...
|
||||||
.to.eql(cqnKey)
|
.to.eql({
|
||||||
|
UPDATE: {
|
||||||
|
entity: 'capire.bookshop.Books',
|
||||||
|
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -756,29 +531,20 @@ describe('cds.ql → cqn', () => {
|
|||||||
|
|
||||||
describe(`DELETE...`, () => {
|
describe(`DELETE...`, () => {
|
||||||
test('from (..., <key>)', () => {
|
test('from (..., <key>)', () => {
|
||||||
const cqnWhere = {
|
|
||||||
DELETE: {
|
|
||||||
from: 'capire.bookshop.Books',
|
|
||||||
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
expect(DELETE.from(Books).where({ ID: 4711 }))
|
|
||||||
.to.eql(DELETE.from(Books).where(`ID=`, 4711))
|
|
||||||
.to.eql(cqnWhere)
|
|
||||||
const cqnKey = (cds.version >= '5.6.0') ?
|
|
||||||
{
|
|
||||||
DELETE: {
|
|
||||||
from: { ref: [{ id: 'capire.bookshop.Books', where: [{ ref: ['ID'] }, '=', { val: 4711 }]}] }
|
|
||||||
},
|
|
||||||
} : cqnWhere
|
|
||||||
|
|
||||||
expect(DELETE(Books, 4711))
|
expect(DELETE(Books, 4711))
|
||||||
.to.eql(DELETE(Books, { ID: 4711 }))
|
.to.eql(DELETE(Books, { ID: 4711 }))
|
||||||
.to.eql(DELETE.from(Books, 4711))
|
.to.eql(DELETE.from(Books, 4711))
|
||||||
.to.eql(DELETE.from(Books, { ID: 4711 }))
|
.to.eql(DELETE.from(Books, { ID: 4711 }))
|
||||||
.to.eql(DELETE.from(Books).byKey(4711))
|
.to.eql(DELETE.from(Books).byKey(4711))
|
||||||
.to.eql(DELETE.from(Books).byKey({ ID: 4711 }))
|
.to.eql(DELETE.from(Books).byKey({ ID: 4711 }))
|
||||||
.to.eql(cqnKey)
|
.to.eql(DELETE.from(Books).where({ ID: 4711 }))
|
||||||
|
.to.eql(DELETE.from(Books).where(`ID=`, 4711))
|
||||||
|
.to.eql({
|
||||||
|
DELETE: {
|
||||||
|
from: 'capire.bookshop.Books',
|
||||||
|
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('/w plain SQL', () => {
|
test('/w plain SQL', () => {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
const cds = require('@sap/cds/lib')
|
const { expect } = require('../test') .run (
|
||||||
const { expect } = cds.test (
|
|
||||||
'serve', 'AdminService', '--from', '@capire/bookshop,@capire/common', '--in-memory'
|
'serve', 'AdminService', '--from', '@capire/bookshop,@capire/common', '--in-memory'
|
||||||
)
|
)
|
||||||
|
const cds = require('@sap/cds/lib')
|
||||||
|
|
||||||
describe('Consuming Services locally', () => {
|
describe('Consuming Services locally', () => {
|
||||||
//
|
//
|
||||||
it('bootstrapped the database successfully', ()=>{
|
it('bootrapped the database successfully', ()=>{
|
||||||
const { AdminService } = cds.services
|
const { AdminService } = cds.services
|
||||||
const { Authors } = AdminService.entities
|
const { Authors } = AdminService.entities
|
||||||
expect(AdminService).not.to.be.undefined
|
expect(AdminService).not.to.be.undefined
|
||||||
@@ -15,17 +15,17 @@ describe('Consuming Services locally', () => {
|
|||||||
it('supports targets as strings or reflected defs', async () => {
|
it('supports targets as strings or reflected defs', async () => {
|
||||||
const AdminService = await cds.connect.to('AdminService')
|
const AdminService = await cds.connect.to('AdminService')
|
||||||
const { Authors } = AdminService.entities
|
const { Authors } = AdminService.entities
|
||||||
expect (await SELECT.from(Authors))
|
const _ = expect (await AdminService.read(Authors))
|
||||||
.to.eql(await SELECT.from('Authors'))
|
|
||||||
.to.eql(await AdminService.read(Authors))
|
|
||||||
.to.eql(await AdminService.read('Authors'))
|
.to.eql(await AdminService.read('Authors'))
|
||||||
.to.eql(await AdminService.run(SELECT.from(Authors)))
|
.to.eql(await AdminService.run(SELECT.from(Authors)))
|
||||||
.to.eql(await AdminService.run(SELECT.from('Authors')))
|
// temporary workaround
|
||||||
|
if (cds.version >= '4.2.0')
|
||||||
|
_.to.eql(await AdminService.run(SELECT.from('Authors')))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('allows reading from local services using cds.ql', async () => {
|
it('allows reading from local services using cds.ql', async () => {
|
||||||
const AdminService = await cds.connect.to('AdminService')
|
const AdminService = await cds.connect.to('AdminService')
|
||||||
const authors = await AdminService.read (`Authors`, a => {
|
const query = SELECT.from('Authors', (a) => {
|
||||||
a.name,
|
a.name,
|
||||||
a.books((b) => {
|
a.books((b) => {
|
||||||
b.title,
|
b.title,
|
||||||
@@ -34,6 +34,10 @@ describe('Consuming Services locally', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}).where(`name like`, 'E%')
|
}).where(`name like`, 'E%')
|
||||||
|
// temporary workaround
|
||||||
|
if (cds.version < '4.2.0')
|
||||||
|
query.SELECT.from.ref[0] = 'AdminService.Authors'
|
||||||
|
const authors = await AdminService.run(query)
|
||||||
expect(authors).to.containSubset([
|
expect(authors).to.containSubset([
|
||||||
{
|
{
|
||||||
name: 'Emily Brontë',
|
name: 'Emily Brontë',
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
|
const { GET, POST, expect } = require('../test') .run ('bookshop')
|
||||||
const cds = require('@sap/cds/lib')
|
const cds = require('@sap/cds/lib')
|
||||||
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('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, amount: 5 })
|
||||||
await POST `/browse/submitOrder ${{ book: 201, quantity: 5 }}`
|
await POST('/browse/submitOrder', { book: 201, amount: 5 })
|
||||||
await expect(POST `/browse/submitOrder ${{ book: 201, quantity: 5 }}`).to.be.rejectedWith(/409 - 5 exceeds stock for book #201/)
|
await expect(POST('/browse/submitOrder', { book: 201, amount: 5 })).to.be.rejectedWith(/409 - 5 exceeds stock for book #201/)
|
||||||
const { data } = await GET`/admin/Books/201/stock/$value`
|
const { data } = await GET`/admin/Books/201/stock/$value`
|
||||||
expect(data).to.equal(2)
|
expect(data).to.equal(2)
|
||||||
})
|
})
|
||||||
|
|||||||
6
test/events.cds
Normal file
6
test/events.cds
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
service Sue {
|
||||||
|
@cds.persistence.skip
|
||||||
|
entity Foo { key ID:Integer; title:String; status:String(1); }
|
||||||
|
entity Bar { key ID:Integer; foo: Association to Foo }
|
||||||
|
event Foo.changed : projection on Foo { ID, status }
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
const cds = require('@sap/cds/lib')
|
const { GET, expect } = require('../test') .run ('serve','hello/world.cds')
|
||||||
const { GET, expect } = cds.test (__dirname+'/../hello')
|
|
||||||
|
|
||||||
describe('Hello world!', () => {
|
describe('Hello world!', () => {
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
const {expect} = require('../test')
|
||||||
const cds = require('@sap/cds/lib')
|
const cds = require('@sap/cds/lib')
|
||||||
const {expect} = cds.test
|
|
||||||
|
const { parse:cdr } = cds.ql
|
||||||
|
|
||||||
// should become cds.compile(...) when cds5 is released
|
// should become cds.compile(...) when cds5 is released
|
||||||
const model = cds.compile.to.csn (`
|
const model = cds.compile.to.csn (`
|
||||||
@@ -74,9 +76,11 @@ describe('Hierarchical Data', ()=>{
|
|||||||
const expected = [
|
const expected = [
|
||||||
{ ID:100, name:'Some Cats...' },
|
{ ID:100, name:'Some Cats...' },
|
||||||
{ ID:101, name:'Cat' },
|
{ ID:101, name:'Cat' },
|
||||||
|
{ ID:104, name:'Aristocat' }, // REVISIT: Should be deleted as well?
|
||||||
{ ID:108, name:'Catweazle' }
|
{ ID:108, name:'Catweazle' }
|
||||||
]
|
]
|
||||||
expect ( await SELECT`ID,name`.from(Cats) ).to.eql (expected)
|
if (cdr) expect ( await SELECT.from(Cats) ).to.containSubset (expected)
|
||||||
|
else expect ( await SELECT.from(Cats) ).to.eql (expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
2
test/index.js
Normal file
2
test/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
const cds = require('@sap/cds')
|
||||||
|
module.exports = cds.test.in(__dirname,'..')
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
const { GET, expect } = require('../test') .run ('serve', 'test/localized-data.cds', '--in-memory')
|
||||||
const cds = require('@sap/cds/lib')
|
const cds = require('@sap/cds/lib')
|
||||||
const { GET, expect } = cds.test.run ('serve', __dirname+'/localized-data.cds', '--in-memory')
|
|
||||||
if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch
|
if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch
|
||||||
else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases
|
else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
const { expect } = require('../test')
|
||||||
const cds = require('@sap/cds/lib')
|
const cds = require('@sap/cds/lib')
|
||||||
const { expect } = cds.test
|
|
||||||
const _model = '@capire/reviews'
|
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
|
||||||
@@ -60,11 +60,11 @@ describe('Messaging', ()=>{
|
|||||||
expect(M).equals(N)
|
expect(M).equals(N)
|
||||||
expect(received.length).equals(N)
|
expect(received.length).equals(N)
|
||||||
expect(received.map(m=>m.data)).to.deep.equal([
|
expect(received.map(m=>m.data)).to.deep.equal([
|
||||||
{ count: 1, subject: '201', rating: 1 },
|
{ subject: '201', rating: 1 },
|
||||||
{ count: 2, subject: '201', rating: 1.5 },
|
{ subject: '201', rating: 1.5 },
|
||||||
{ count: 3, subject: '201', rating: 2 },
|
{ subject: '201', rating: 2 },
|
||||||
{ count: 4, subject: '201', rating: 2.5 },
|
{ subject: '201', rating: 2.5 },
|
||||||
{ count: 5, subject: '201', rating: 3 },
|
{ subject: '201', rating: 3 },
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user