Compare commits

...

69 Commits

Author SHA1 Message Date
nkaputnik
e08cdf7831 typo 2021-12-10 10:41:34 +01:00
nkaputnik
1ec44aaecf Fixed dependency 2021-12-10 10:40:17 +01:00
nkaputnik
a6ea2fb130 Added changes for simple field extensibility (UIFlex) 2021-12-07 11:10:12 +01:00
Daniel
1fde0c928b Fixed submit order -> returned stock was wrong 2021-11-29 15:38:37 +01:00
Christian Georgi
fa854a2411 Add hint on db-ext datasource to code tour
Signed-off-by: Christian Georgi <christian.georgi@sap.com>
2021-11-29 15:33:57 +01:00
Daniel
40b6acee6a Fixing erroneous db.model config 2021-11-29 15:33:57 +01:00
Daniel
a37f1b8e0c Reformatted package.json 2021-11-29 15:33:57 +01:00
Christian Georgi
042ddec41c Adjust code tour 2021-11-29 15:33:57 +01:00
Daniel
8d8aa6c2c9 Fixing copy errors
Signed-off-by: Daniel <daniel.hutzel@sap.com>
2021-11-29 15:33:57 +01:00
Daniel
756ed48ce3 Fiori apps with manage authors
Signed-off-by: Daniel <daniel.hutzel@sap.com>
2021-11-29 15:33:57 +01:00
Daniel
19b680ab3d Moving lifetime + age to where it is used
Signed-off-by: Daniel <daniel.hutzel@sap.com>
2021-11-29 15:33:57 +01:00
Daniel
33380c0792 Restore simple bookshop - no workarounds
Signed-off-by: Daniel <daniel.hutzel@sap.com>
2021-11-29 15:33:57 +01:00
Christian Georgi
0d5b65d93f Merge pull request #291 from gregorwolf/navigation
Demo for Semantic Navigation using the local Fiori Launchpad
2021-11-22 13:08:57 +01:00
Christian Georgi
dcf5ec6068 Merge branch 'main' into navigation 2021-11-18 16:19:57 +01:00
Christian Georgi
fb41dd100b Merge pull request #293 from SAP-samples/bookstore
In Fiori app, use bookstore consistently
2021-11-18 16:12:12 +01:00
Christian Georgi
769aa2b4f5 Adjust samples tour to bookstore 2021-11-18 15:34:59 +01:00
Christian Georgi
f6c5e620c0 In Fiori app, use bookstore consistently
This also fixes validation errors from annotation LSP, which missed some
of the bookstore elements

Signed-off-by: Christian Georgi <christian.georgi@sap.com>
2021-11-18 14:14:09 +01:00
Gregor Wolf
7013cb6296 remove name 2021-11-13 00:48:13 +01:00
Gregor Wolf
6411ce4ea3 remove redundant annotation 2021-11-13 00:48:04 +01:00
Gregor Wolf
cd914a8d87 show ID in LineItem 2021-11-13 00:41:48 +01:00
Gregor Wolf
b35e92b5bc TextArrangement : #TextOnly 2021-11-13 00:41:28 +01:00
Gregor Wolf
8350aff111 correct renameTo 2021-11-13 00:36:22 +01:00
Gregor Wolf
9aa604c5a0 remove @UI.HiddenFilter 2021-11-12 23:56:11 +01:00
Gregor Wolf
f5cb53adf8 add inbounds parameter 2021-11-12 23:48:51 +01:00
Gregor Wolf
9fe16edc55 fix manifest issues 2021-11-12 23:48:26 +01:00
Gregor Wolf
0093883f25 add Author ID column 2021-11-12 23:46:41 +01:00
Gregor Wolf
14024a4837 add semantic object for navigation 2021-11-12 22:31:58 +01:00
Gregor Wolf
2e13b37096 activate automatic load 2021-11-12 22:31:42 +01:00
Gregor Wolf
7407b0aae3 format 2021-11-12 22:04:48 +01:00
Gregor Wolf
152d088ff4 rename author to authorName 2021-11-12 21:49:39 +01:00
Gregor Wolf
19c886d6dc fix implicit redirection issue 2021-11-12 21:41:39 +01:00
Gregor Wolf
575cffe70a migrate to fioriSandboxConfig.json 2021-11-12 21:29:21 +01:00
Gregor Wolf
03869e5896 Fiori UI for Authors 2021-11-12 21:28:49 +01:00
Gregor Wolf
597afeb3d8 format, rename author to authorName 2021-11-12 21:28:23 +01:00
Gregor Wolf
d376e6e063 UI annotations for Authors 2021-11-12 21:27:40 +01:00
Gregor Wolf
2a44cfa9dc add Authors, rename author attribute 2021-11-12 21:25:33 +01:00
Gregor Wolf
7964c9aefd adjust formatting 2021-11-12 20:59:45 +01:00
Christian Georgi
ce752097d0 Merge pull request #290 from dinurp/main
Annotations for Admin.Book service
2021-11-10 08:55:47 +01:00
dinurp
e838f380c5 annotation to make language value list a drop down.
type:#Fixed was not rendering the value list as a drop down.
There was not UI validation for values entered. 
The added annotation does both these: UI validation and renders the a drop down list.
2021-11-10 11:18:41 +04:00
dinurp
e89e012eba Update bookshop/srv/admin-service.cds
Co-authored-by: Christian Georgi <chgeo@users.noreply.github.com>
2021-11-10 11:12:18 +04:00
Christian Georgi
bf0f57efe5 Merge branch 'main' into main 2021-11-09 17:55:55 +01:00
Dinu
40ea057f7f Annotation for value list for languages for locale 2021-11-09 15:46:22 +04:00
Dinu
31d2f4c932 Avoid popup for ID in Fiori preview for Books. 2021-11-09 14:01:53 +04:00
Daniel Hutzel
8cc2db7118 Using managed compositions for Order.Items (#273)
* Using managed compositions for Order.Items
Co-authored-by: sjvans <30337871+sjvans@users.noreply.github.com>
2021-11-08 17:41:33 +01:00
Daniel Hutzel
482b71e60b Remove workaround for integrity check (#173)
* Remove workaround for integrity check
2021-11-08 14:23:41 +01:00
Daniel Hutzel
7536451e29 Enhanced cds.ql tests (#228)
* Enhanced cds.ql tests
2021-11-08 13:49:43 +01:00
Daniel
2703e8b999 Removed authorization workaround 2021-11-08 13:19:12 +01:00
Daniel
ac2e4dd6f9 Updating package-lock to v2 2021-11-08 13:17:31 +01:00
PierreFritsch
c718fa400d import {Request} => import type {Request} 2021-11-08 13:14:02 +01:00
Pierre Fritsch
730da6e435 hello TypeScript: Define type of req object
Make it clearer that `req` is a CDS `Request` object.
2021-11-08 13:14:02 +01:00
Daniel
7d9b4f7a5d Fixes for Node16 2021-11-05 17:01:59 +01:00
Daniel
35d3708592 Fixes for Node16 2021-11-05 15:49:05 +01:00
Daniel
eaaf0d29a5 Updated package-lock.json 2021-11-04 10:37:33 +01:00
Daniel
64fe700d1e ... 2021-11-04 10:37:33 +01:00
Daniel
5c3cec973e Using bookstore as composite app 2021-11-04 10:37:33 +01:00
Daniel
680a6ae68f Introduced bookstore composite app 2021-11-04 10:37:33 +01:00
Daniel
366b0f8f9a Using flat fiori app folder 2021-11-04 10:37:33 +01:00
Daniel
b95df77b9a Fixed access to cds.entities 2021-11-04 10:37:33 +01:00
Christian Georgi
7f6b87171a Merge pull request #284 from SAP-samples/registry
NPM registry: more robust,  work on Windows
2021-10-26 22:21:32 +02:00
Christian Georgi
21e74bbbfb Add test for NPM registry 2021-10-26 22:17:25 +02:00
Christian Georgi
102b15c3cd NPM registry: more robust, work on Windows
- Run `npm rm` in correct dir
- Encode args in `npm conf rm` command for Powershell
- Don't cleanup in exit handler, this might not complete.
  Instead, do it on startup.
- Different cleanup command for Windows and Linux
- Explicitly close server, instead of relying on process.exit()
- Make scope configurable
2021-10-26 19:11:08 +02:00
Dr. David A. Kunz
73db2e96bc Merge pull request #283 from SAP-samples/ql-changes-typo-fix
fix typo in ql changes
2021-10-26 11:57:44 +02:00
Dr. David A. Kunz
7bb58ee2d5 Merge branch 'main' into ql-changes-typo-fix 2021-10-26 11:56:15 +02:00
D065023
27e82d16e0 fix typo 2021-10-26 11:55:33 +02:00
Dr. David A. Kunz
ff9bbe6d8d Merge pull request #282 from SAP-samples/ql-changes
Changes in ql for cds version 5.6.0
2021-10-26 11:32:09 +02:00
Dr. David A. Kunz
5f1b7b8cbf Merge branch 'main' into ql-changes 2021-10-26 11:27:27 +02:00
D065023
0253300557 Changes in ql for cds version 5.6.0 2021-10-26 11:22:43 +02:00
Daniel
139d957495 Added reviewed.count to messsages 2021-10-18 18:04:33 +02:00
Daniel
404427237b Updated reviews samples for messaging GA 2021-10-18 18:04:33 +02:00
58 changed files with 11913 additions and 682 deletions

View File

@@ -1,18 +1,24 @@
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] = process.argv const [,,port=PORT,scope='@capire'] = 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 ] = /^capire-(\w+)/.exec(tarball) const [, pkg ] = /^\w+-(\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()
})) }))
@@ -25,12 +31,14 @@ 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 {
const [, capire, pkg ] = /^\/(@capire)\/(\w+)/.exec(url) if (!urlRegex.test(url)) return res.sendStatus(404)
const package = require (`${capire}/${pkg}/package.json`) const [, scpe, pkg ] = urlRegex.exec(url)
const tarball = `capire-${pkg}-${package.version}.tgz` const package = require (`${scpe}/${pkg}/package.json`)
const tarball = `${scpe.slice(1)}-${pkg}-${package.version}.tgz`
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md // https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
res.json({ res.json({
"name": package.name, "name": package.name,
@@ -42,29 +50,30 @@ app.get('/*', (req,res)=>{
"name": package.name, "name": package.name,
"version": package.version, "version": package.version,
"dist": { "dist": {
"tarball": `http://localhost:${port}/-/${tarball}` "tarball": `${server.url}/-/${tarball}`
}, },
} }
}, },
}) })
} catch (e) { } catch (e) {
console.error(e) if (e.code === 'MODULE_NOT_FOUND') return res.sendStatus(404)
res.sendStatus(404) console.error(e); throw e
} }
}) })
app.listen(port, ()=>{ const server = app.listen(port, ()=>{
console.log (`npm set @capire:registry=http://localhost:${port}`) const url = server.url = `http://localhost:${server.address().port}`
console.log (`@capire registry listening on http://localhost:${port}`) console.log (`npm set ${scope}:registry=${url}`)
exec(`npm set @capire:registry=http://localhost:${port}`) exec(`npm set ${scope}:registry=${url}`)
console.log (`${scope} registry listening on ${url}`)
}) })
const _exit = ()=>{ const _exit = ()=>{
console.log ('\nnpm conf rm @capire:registry') server.close()
exec('npm conf rm @capire:registry') exec(`npm conf rm "${scope}:registry"`, ()=> { process.exit() })
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)

View File

@@ -68,26 +68,35 @@
}, },
{ {
"file": "fiori/package.json", "file": "fiori/package.json",
"description": "#### Configuration\n\nThe `cds` section in `package.json` is a place to configure which of the `db/sqlite` and `db/hana` folders are used for which database.\nWe use [Node.js profiles](https://cap.cloud.sap/docs/node.js/cds-env#profiles) to separate the configuration.\nIn the `development` profile, you can see that `db/sqlite` is set as the model, while the `db/hana` folder is configured in the `production` profile.", "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.",
"line": 17, "selection": {
"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": 28, "line": 43,
"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": 31, "line": 46,
"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": 68, "line": 72,
"selection": { "selection": {
"start": { "start": {
"line": 67, "line": 67,
@@ -104,6 +113,5 @@
"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": "main"
} }

View File

@@ -19,7 +19,7 @@
} }
}, },
{ {
"file": "hello/world.cds", "file": "hello/srv/world.cds",
"description": "### Hello World!\n\nThis is a simplistic [Hello World](https://cap.cloud.sap/docs/get-started/hello-world) service using [CDS](https://cap.cloud.sap/docs/cds/) and [cds.services](https://cap.cloud.sap/docs/node.js/api#services-api).", "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": "### Compositions and Serving Documents\n\nA standalone orders management service, demonstrating:\n- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with\n- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)\n", "description": "### Orders - Compositions and Serving Documents\n\nA standalone orders management service, demonstrating:\n- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with\n- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)\n",
"line": 1, "line": 1,
"selection": { "selection": {
"start": { "start": {
@@ -84,7 +84,7 @@
}, },
{ {
"file": "reviews/db/schema.cds", "file": "reviews/db/schema.cds",
"description": "### More Modularity\n\nShows how to implement a modular service to manage product reviews, including:\n- Consuming other services synchronously and asynchronously\n- Serving requests synchronously\n- Emitting events asynchronously\n- Grow as you go, with:\n- Mocking app services\n- Running service meshes\n- Late-cut Micro Services\n- As well as managed data, input validations, and authorization\n", "description": "### Reviews - More Modularity\n\nShows how to implement a modular service to manage product reviews, including:\n- Consuming other services synchronously and asynchronously\n- Serving requests synchronously\n- Emitting events asynchronously\n- Grow as you go, with:\n- Mocking app services\n- Running service meshes\n- Late-cut Micro Services\n- As well as managed data, input validations, and authorization\n",
"line": 1, "line": 1,
"selection": { "selection": {
"start": { "start": {
@@ -99,8 +99,12 @@
"title": "Reviews" "title": "Reviews"
}, },
{ {
"file": "fiori/app/index.cds", "title": "Bookstore",
"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", "description": "### Bookstore - Reuse and UI\n\n- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/reuse-and-compose) these packages:\n - [@capire/bookshop](bookshop)\n - [@capire/reviews](reviews)\n - [@capire/orders](orders)\n - [@capire/common](common)\n- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well\n- [The Vue.js app](reviews/app/vue) imported from reviews is served as well\n- [The Fiori app](orders/app) imported from orders is served as well\n- [OpenAPI export + Swagger UI](https://cap.cloud.sap/docs/advanced/openapi)"
},
{
"file": "fiori/app/services.cds",
"description": "### Annotations for SAP Fiori Elements\n\n- [Adds an SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookstore, thereby introducing to:\n- [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files\n- Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)\n- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)\n- Serving SAP Fiori apps locally\n",
"line": 1, "line": 1,
"selection": { "selection": {
"start": { "start": {
@@ -117,14 +121,13 @@
{ {
"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": 15, "line": 16,
"character": 1 "character": 1
} }
}, },
@@ -133,4 +136,4 @@
], ],
"isPrimary": true, "isPrimary": true,
"description": "Overview of CAP Samples for Node.js" "description": "Overview of CAP Samples for Node.js"
} }

View File

@@ -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) }} {{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }} ({{ book.numberOfReviews }})
</td> </td>
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td> <td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
</tr> </tr>

View File

@@ -1,7 +1,7 @@
using { Currency, managed, sap } from '@sap/cds/common'; using { Currency, managed, sap, extensible } from '@sap/cds/common';
namespace sap.capire.bookshop; namespace sap.capire.bookshop;
entity Books : managed { entity Books : managed, extensible {
key ID : Integer; key ID : Integer;
title : localized String(111); title : localized String(111);
descr : localized String(1111); descr : localized String(1111);
@@ -13,7 +13,7 @@ entity Books : managed {
image : LargeBinary @Core.MediaType : 'image/png'; image : LargeBinary @Core.MediaType : 'image/png';
} }
entity Authors : managed { entity Authors : managed, extensible {
key ID : Integer; key ID : Integer;
name : String(111); name : String(111);
dateOfBirth : Date; dateOfBirth : Date;

View File

@@ -1 +1,2 @@
exports.CatalogService = require('./srv/cat-service') const { CatalogService } = require('./srv/cat-service')
module.exports = { CatalogService }

View File

@@ -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,quantity} = 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 (stock >= quantity) { if (quantity > stock) return req.reject (409,`${quantity} exceeds stock for book #${book}`)
await UPDATE (Books,book) .with (`stock -=`, quantity) await UPDATE (Books,book) .with ({ stock: stock -= quantity })
await this.emit ('OrderedBook', { book, quantity, buyer:req.user.id }) await this.emit ('OrderedBook', { book, quantity, buyer:req.user.id })
return { stock } return { stock }
}
else return req.error (409,`${quantity} exceeds stock for book #${book}`)
}) })
// Add some discount for overstocked books // Add some discount for overstocked books

View File

@@ -32,6 +32,19 @@ 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

2
bookstore/index.cds Normal file
View File

@@ -0,0 +1,2 @@
namespace sap.capire.bookshop; //> important for reflection
using from './srv/mashup';

33
bookstore/package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"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 }
}
}

21
bookstore/server.js Normal file
View File

@@ -0,0 +1,21 @@
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

View File

@@ -1,6 +1,7 @@
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Mashing up imported models... // Enhancing bookshop with Reviews and Orders provided through
// respective reuse packages and services
// //
using { sap.capire.bookshop.Books } from '@capire/bookshop'; using { sap.capire.bookshop.Books } from '@capire/bookshop';
@@ -8,18 +9,24 @@ using { sap.capire.bookshop.Books } from '@capire/bookshop';
// //
// Extend Books with access to Reviews and average ratings // Extend Books with access to Reviews and average ratings
// //
using { ReviewsService.Reviews } from '@capire/reviews'; using { ReviewsService.Reviews } from '@capire/reviews';
extend Books with { extend Books with {
reviews : Composition of many Reviews on reviews.subject = $self.ID; reviews : Composition of many Reviews on reviews.subject = $self.ID;
rating : Decimal; rating : Decimal;
numberOfReviews : Integer;
} }
// //
// Extend Orders with Books as Products // Extend Orders with Books as Products
// //
using { sap.capire.orders.Orders } from '@capire/orders';
using { sap.capire.orders.Orders_Items } from '@capire/orders'; extend Orders with {
extend Orders_Items with { extend Items with {
book : Association to Books on product.ID = book.ID book : Association to Books on product.ID = book.ID
}
} }
// Add orders fiori app (in case of embedded orders service)
using from '@capire/orders/app/fiori';

View File

@@ -1,6 +1,6 @@
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Mashing up provided and required services... // Mashing up bookshop services with required services...
// //
module.exports = async()=>{ // called by server.js module.exports = async()=>{ // called by server.js
@@ -20,7 +20,7 @@ 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.tx(req).read ('Reviews',columns).limit(limit).where({subject:String(id)}) return ReviewsService.read ('Reviews',columns).limit(limit).where({subject:String(id)})
})) }))
// //
@@ -37,13 +37,12 @@ module.exports = async()=>{ // called by server.js
}) })
// //
// Update Books' average ratings when ReviewsService signals updatd reviews // Update Books' average ratings when ReviewsService signals updated 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, rating } = msg.data const { subject, count, rating } = msg.data
return UPDATE(Books,subject).with({rating}) return UPDATE(Books,subject).with({ numberOfReviews:count, rating })
// ^ Note: the framework will execute this and take care for db.tx
}) })
// //

View File

@@ -0,0 +1,10 @@
// -----------------------------------------------------------------------
// Adding Swagger UI - see https://cap.cloud.sap/docs/advanced/openapi
const cds = require ('@sap/cds')
try {
const cds_swagger = require ('cds-swagger-ui-express')
cds.once ('bootstrap', app => app.use (cds_swagger()) )
} catch (err) {
if (err.code !== 'MODULE_NOT_FOUND') throw err
}

View File

@@ -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

View File

@@ -1,2 +0,0 @@
# cds.requires.messaging.kind = file-based-messaging
PORT = 4004

View File

@@ -0,0 +1,47 @@
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; }

View File

@@ -0,0 +1,7 @@
sap.ui.define(["sap/fe/core/AppComponent"], function (AppComponent) {
"use strict";
return AppComponent.extend("authors.Component", {
metadata: { manifest: "json" },
});
});
/* eslint no-undef:0 */

View File

@@ -0,0 +1,11 @@
# This is the resource bundle of itelo
# __ldi.translation.uuid=c3431418-9caf-11e8-98d0-529269fb1459
# JCI app descriptor contains lower case TITLE
appTitle=Bookshop Authors
# JCI app descriptor contains lower case DESCRIPTION
appSubTitle=Bookshop Authors
# JCI app descriptor contains lower case DESCRIPTION
appDescription=Bookshop Authors

View File

@@ -0,0 +1,145 @@
{
"_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"
}
}

View File

@@ -1,4 +1,4 @@
using { AdminService } from '../../db/schema'; using { AdminService } from '@capire/bookstore';
using from '../common'; // to help UI linter get the complete annotations using from '../common'; // to help UI linter get the complete annotations
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@@ -40,27 +40,6 @@ 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}'},
]
},
}
);
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
@@ -85,10 +64,14 @@ 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',type:#fixed} locale @ValueList:{entity:'Languages'};
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 through AdminService as a target for ValueList
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; @readonly entity Languages as projection on sap.common.Languages;
} }
// Workaround for Fiori popup for asking user to enter a new UUID on Create
annotate AdminService.Books with { ID @Core.Computed; }

View File

@@ -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("admin.Component", { return AppComponent.extend("books.Component", {
metadata: { manifest: "json" } metadata: { manifest: "json" }
}); });
}); });

View File

@@ -1,7 +1,7 @@
{ {
"_version": "1.8.0", "_version": "1.8.0",
"sap.app": { "sap.app": {
"id": "admin", "id": "books",
"type": "application", "type": "application",
"title": "Manage Books", "title": "Manage Books",
"description": "Sample Application", "description": "Sample Application",
@@ -22,6 +22,10 @@
} }
}, },
"sap.ui5": { "sap.ui5": {
"flexEnabled": true,
"config": {
"experimentalCAPScenario": true
},
"dependencies": { "dependencies": {
"libs": { "libs": {
"sap.fe.templates": {} "sap.fe.templates": {}
@@ -73,6 +77,7 @@
"options": { "options": {
"settings" : { "settings" : {
"entitySet" : "Books", "entitySet" : "Books",
"initialLoad": true,
"navigation" : { "navigation" : {
"Books" : { "Books" : {
"detail" : { "detail" : {

View File

@@ -0,0 +1,142 @@
{
"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"
}
}
}
}
}
}
}
}

View File

@@ -1,3 +0,0 @@
<head>
<meta http-equiv="refresh" content="0;url=bookshop/index.html">
</head>

View File

@@ -1,50 +1,60 @@
using CatalogService from '@capire/bookshop'; using CatalogService from '@capire/bookstore';
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Books Object Page // Books Object Page
// //
annotate CatalogService.Books with @( annotate CatalogService.Books with @(UI : {
UI: { HeaderInfo : {
HeaderInfo: { TypeName : 'Book',
TypeName: 'Book', TypeNamePlural : 'Books',
TypeNamePlural: 'Books', Description : {Value : author}
Description: {Value: author} },
}, HeaderFacets : [{
HeaderFacets: [ $Type : 'UI.ReferenceFacet',
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Description}', Target: '@UI.FieldGroup#Descr'}, Label : '{i18n>Description}',
], Target : '@UI.FieldGroup#Descr'
Facets: [ }, ],
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Price'}, Facets : [{
], $Type : 'UI.ReferenceFacet',
FieldGroup#Descr: { Label : '{i18n>Details}',
Data: [ Target : '@UI.FieldGroup#Price'
{Value: descr}, }, ],
] FieldGroup #Descr : {Data : [{Value : descr}, ]},
}, FieldGroup #Price : {Data : [
FieldGroup#Price: { {Value : price},
Data: [ {
{Value: price}, Value : currency.symbol,
{Value: currency.symbol, Label: '{i18n>Currency}'}, Label : '{i18n>Currency}'
] },
}, ]},
} });
);
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Books Object Page // Books Object Page
// //
annotate CatalogService.Books with @( annotate CatalogService.Books with @(UI : {
UI: { SelectionFields : [
SelectionFields: [ ID, price, currency_code ], ID,
LineItem: [ price,
{Value: title}, currency_code
{Value: author, Label:'{i18n>Author}'}, ],
{Value: genre.name}, LineItem : [
{Value: price}, {
{Value: currency.symbol, Label:' '}, Value : ID,
] Label : '{i18n>Title}'
}, },
); {
Value : author,
Label : '{i18n>Author}'
},
{Value : genre.name},
{Value : price},
{
Value : currency.symbol,
Label : ' '
},
]
}, );

View File

@@ -1,11 +1,14 @@
{ {
"_version": "1.8.0", "_version": "1.28.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/",
@@ -15,14 +18,43 @@
} }
} }
}, },
"-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": {}
} }
@@ -68,6 +100,7 @@
"options": { "options": {
"settings": { "settings": {
"entitySet": "Books", "entitySet": "Books",
"initialLoad": true,
"navigation": { "navigation": {
"Books": { "Books": {
"detail": { "detail": {
@@ -97,7 +130,12 @@
}, },
"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": [],

View File

@@ -1,8 +1,8 @@
/* /*
Common Annotations shared by all apps Common Annotations shared by all apps
*/ */
using { sap.capire.bookshop as my } from '@capire/bookshop'; using { sap.capire.bookshop as my } from '@capire/bookstore';
using { sap.common } from '@capire/common'; using { sap.common } from '@capire/common';
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@@ -10,39 +10,52 @@ using { sap.common } from '@capire/common';
// Books Lists // Books Lists
// //
annotate my.Books with @( annotate my.Books with @(
Common.SemanticKey: [title], Common.SemanticKey : [ID],
UI: { UI : {
Identification: [{Value:title}], Identification : [{Value : title}],
SelectionFields: [ ID, author_ID, price, currency_code ], SelectionFields : [
LineItem: [ ID,
{Value: ID}, author_ID,
{Value: title}, price,
{Value: author.name, Label:'{i18n>Author}'}, currency_code
{Value: genre.name}, ],
{Value: stock}, LineItem : [
{Value: price}, {
{Value: currency.symbol, Label:' '}, Value : ID,
] Label : '{i18n>Title}'
} },
{
Value : author.ID,
Label : '{i18n>Author}'
},
{Value : genre.name},
{Value : stock},
{Value : price},
{
Value : currency.symbol,
Label : ' '
},
]
}
) { ) {
author @ValueList.entity:'Authors'; ID @Common: {
SemanticObject : 'Books',
Text: title,
TextArrangement : #TextOnly
};
author @ValueList.entity : 'Authors';
}; };
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Books Details // Books Details
// //
annotate my.Books with @( annotate my.Books with @(UI : {HeaderInfo : {
UI: { TypeName : '{i18n>Book}',
HeaderInfo: { TypeNamePlural : '{i18n>Books}',
TypeName: '{i18n>Book}', Title : {Value : title},
TypeNamePlural: '{i18n>Books}', Description : {Value : author.name}
Title: {Value: title}, }, });
Description: {Value: author.name}
},
}
);
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@@ -50,13 +63,19 @@ annotate my.Books with @(
// Books Elements // Books Elements
// //
annotate my.Books with { annotate my.Books with {
ID @title:'{i18n>ID}' @UI.HiddenFilter; ID @title : '{i18n>ID}';
title @title:'{i18n>Title}'; title @title : '{i18n>Title}';
genre @title:'{i18n>Genre}' @Common: { Text: genre.name, TextArrangement: #TextOnly }; genre @title : '{i18n>Genre}' @Common : {
author @title:'{i18n>Author}' @Common: { Text: author.name, TextArrangement: #TextOnly }; Text : genre.name,
price @title:'{i18n>Price}' @Measures.ISOCurrency: currency_code; TextArrangement : #TextOnly
stock @title:'{i18n>Stock}'; };
descr @UI.MultiLineText; 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;
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@@ -64,42 +83,45 @@ 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 @( annotate my.Genres with @(UI : {
UI: { Identification : [{Value : name}],
Identification: [{Value:name}], HeaderInfo : {
HeaderInfo: { TypeName : '{i18n>Genre}',
TypeName: '{i18n>Genre}', TypeNamePlural : '{i18n>Genres}',
TypeNamePlural: '{i18n>Genres}', Title : {Value : name},
Title: {Value: name}, Description : {Value : ID}
Description: {Value: ID} },
}, Facets : [{
Facets: [ $Type : 'UI.ReferenceFacet',
{$Type: 'UI.ReferenceFacet', Label: '{i18n>SubGenres}', Target: 'children/@UI.LineItem'}, Label : '{i18n>SubGenres}',
], 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}';
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@@ -107,38 +129,42 @@ annotate my.Genres with {
// Authors List // Authors List
// //
annotate my.Authors with @( annotate my.Authors with @(
Common.SemanticKey: [name], Common.SemanticKey : [ID],
UI: { UI : {
Identification: [{Value:name}], Identification : [{Value : name}],
SelectionFields: [ name ], SelectionFields : [name],
LineItem:[ LineItem : [
{Value: ID}, {Value : ID},
{Value: name}, {Value : dateOfBirth},
{Value: dateOfBirth}, {Value : dateOfDeath},
{Value: dateOfDeath}, {Value : placeOfBirth},
{Value: placeOfBirth}, {Value : placeOfDeath},
{Value: placeOfDeath}, ],
], }
} ) {
); ID @Common: {
SemanticObject : 'Authors',
Text: name,
TextArrangement : #TextOnly,
};
};
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Author Details // Author Details
// //
annotate my.Authors with @( annotate my.Authors with @(UI : {
UI: { HeaderInfo : {
HeaderInfo: { TypeName : '{i18n>Author}',
TypeName: '{i18n>Author}', TypeNamePlural : '{i18n>Authors}',
TypeNamePlural: '{i18n>Authors}', Title : {Value : name},
Title: {Value: name}, Description : {Value : dateOfBirth}
Description: {Value: dateOfBirth} },
}, Facets : [{
Facets: [ $Type : 'UI.ReferenceFacet',
{$Type: 'UI.ReferenceFacet', Target: 'books/@UI.LineItem'}, Target : 'books/@UI.LineItem'
], }, ],
} });
);
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@@ -146,12 +172,12 @@ annotate my.Authors with @(
// Authors Elements // Authors Elements
// //
annotate my.Authors with { annotate my.Authors with {
ID @title:'{i18n>ID}' @UI.HiddenFilter; ID @title : '{i18n>ID}';
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}';
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@@ -159,99 +185,105 @@ 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: [ name, descr ], SelectionFields : [
LineItem:[ name,
{Value: code}, descr
{Value: name}, ],
], LineItem : [
} {Value : code},
{Value : name},
],
}
); );
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Language Details // Language Details
// //
annotate common.Languages with @( annotate common.Languages with @(UI : {
UI: { HeaderInfo : {
HeaderInfo: { TypeName : '{i18n>Language}',
TypeName: '{i18n>Language}', TypeNamePlural : '{i18n>Languages}',
TypeNamePlural: '{i18n>Languages}', Title : {Value : name},
Title: {Value: name}, Description : {Value : descr}
Description: {Value: descr} },
}, Facets : [{
Facets: [ $Type : 'UI.ReferenceFacet',
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'}, Label : '{i18n>Details}',
], Target : '@UI.FieldGroup#Details'
FieldGroup#Details: { }, ],
Data: [ FieldGroup #Details : {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: [ name, descr ], SelectionFields : [
LineItem:[ name,
{Value: descr}, descr
{Value: symbol}, ],
{Value: code}, LineItem : [
], {Value : descr},
} {Value : symbol},
{Value : code},
],
}
); );
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
// Currency Details // Currency Details
// //
annotate common.Currencies with @( annotate common.Currencies with @(UI : {
UI: { HeaderInfo : {
HeaderInfo: { TypeName : '{i18n>Currency}',
TypeName: '{i18n>Currency}', TypeNamePlural : '{i18n>Currencies}',
TypeNamePlural: '{i18n>Currencies}', Title : {Value : descr},
Title: {Value: descr}, Description : {Value : code}
Description: {Value: code} },
}, Facets : [
Facets: [ {
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'}, $Type : 'UI.ReferenceFacet',
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Extended}', Target: '@UI.FieldGroup#Extended'}, Label : '{i18n>Details}',
], Target : '@UI.FieldGroup#Details'
FieldGroup#Details: { },
Data: [ {
{Value: name}, $Type : 'UI.ReferenceFacet',
{Value: symbol}, Label : '{i18n>Extended}',
{Value: code}, Target : '@UI.FieldGroup#Extended'
{Value: descr} },
] ],
}, FieldGroup #Details : {Data : [
FieldGroup#Extended: { {Value : name},
Data: [ {Value : symbol},
{Value: numcode}, {Value : code},
{Value: minor}, {Value : descr}
{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}';
} }

View File

@@ -10,32 +10,21 @@
<script> <script>
window["sap-ushell-config"] = { window["sap-ushell-config"] = {
defaultRenderer: "fiori2", defaultRenderer: "fiori2",
applications: { applications: {},
"browse-books": { bootstrapPlugins: {
title: "Browse Books", RuntimeAuthoringPlugin: {
description: "w/ SAP Fiori Elements", component: "sap.ushell.plugins.rta",
additionalInformation: "SAPUI5.Component=bookshop", config: {
applicationType : "URL", validateAppVersion: false,
url: "/browse/webapp", },
navigationMode: "embedded" },
}, PersonalizePlugin: {
"manage-books": { component: "sap.ushell.plugins.rta-personalize",
title: "Manage Books", config: {
description: "w/ SAP Fiori Elements", validateAppVersion: false,
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>
@@ -47,8 +36,13 @@
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"));
</script> sap.ui
.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>

View File

@@ -1,3 +0,0 @@
<head>
<meta http-equiv="refresh" content="0;url=reviews/index.html">
</head>

View File

@@ -2,11 +2,8 @@
This model controls what gets served to Fiori frontends... This model controls what gets served to Fiori frontends...
*/ */
using from './admin/fiori-service'; using from './admin-authors/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';

View File

@@ -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 '../schema'; using { AdminService } from '@capire/bookshop';
extend projection AdminService.Authors with { extend projection AdminService.Authors with {
YEARS_BETWEEN(dateOfBirth, dateOfDeath) as age: Integer, YEARS_BETWEEN(dateOfBirth, dateOfDeath) as age: Integer,

View File

@@ -1,8 +0,0 @@
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;
}

View File

@@ -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 '../schema'; using { AdminService } from '@capire/bookshop';
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,

View File

@@ -2,10 +2,7 @@
"name": "@capire/fiori", "name": "@capire/fiori",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@capire/bookshop": "*", "@capire/bookstore": "*",
"@capire/reviews": "*",
"@capire/orders": "*",
"@capire/common": "*",
"@sap/cds": "^5", "@sap/cds": "^5",
"express": "^4.17.1", "express": "^4.17.1",
"passport": "^0.4.1" "passport": "^0.4.1"
@@ -15,10 +12,10 @@
"watch": "cds watch" "watch": "cds watch"
}, },
"cds": { "cds": {
"hana": {
"deploy-format": "hdbtable"
},
"requires": { "requires": {
"extensibility": {
"kind": "uiflex"
},
"auth": { "auth": {
"strategy": "dummy" "strategy": "dummy"
}, },
@@ -30,15 +27,31 @@
"kind": "odata", "kind": "odata",
"model": "@capire/orders" "model": "@capire/orders"
}, },
"messaging": {
"[production]": {
"kind": "enterprise-messaging"
},
"[development]": {
"kind": "file-based-messaging"
},
"[hybrid!]": {
"kind": "enterprise-messaging-shared"
}
},
"db": { "db": {
"kind": "sql", "kind": "sql"
},
"db-ext": {
"[development]": { "[development]": {
"model": "db/sqlite" "model": "db/sqlite"
}, },
"[production]": { "[production]": {
"model": "db/hana" "model": "db/hana"
} }
},
"hana": {
"deploy-format": "hdbtable"
} }
} }
} }
} }

View File

@@ -1,25 +1 @@
const cds = require ('@sap/cds') module.exports = require('@capire/bookstore/server.js')
module.exports = cds.server
cds.once('bootstrap',(app)=>{
app.use ('/orders/webapp', _from('@capire/orders/app/orders/webapp/manifest.json'))
app.use ('/bookshop', _from('@capire/bookshop/app/vue/index.html'))
app.use ('/reviews', _from('@capire/reviews/app/vue/index.html'))
})
cds.once('served', require('./srv/mashup'))
// Swagger UI - see https://cap.cloud.sap/docs/advanced/openapi
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
}
// -----------------------------------------------------------------------
// Helper for serving static content from npm-installed packages
const {static} = require('express')
const {dirname} = require('path')
const _from = target => static (dirname (require.resolve(target)))

View File

@@ -7,8 +7,8 @@
"start:ts": "cds-ts serve srv/world.cds" "start:ts": "cds-ts serve srv/world.cds"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^26.0.23", "@types/jest": "^27.0.2",
"@types/node": "^15.12.0", "@types/node": "^16.11.6",
"ts-jest": "^27.0.2", "ts-jest": "^27.0.2",
"typescript": "^4.3.5" "typescript": "^4.3.5"
}, },

View File

@@ -1,5 +1,7 @@
import type { Request } from "@sap/cds/apis/services"
module.exports = class say { module.exports = class say {
hello(req: any) { hello(req: Request) {
return `Hello ${req.data.to} from a TypeScript file!` return `Hello ${req.data.to} from a TypeScript file!`
} }
} }

View File

@@ -10,7 +10,7 @@
using { OrdersService } from '../../srv/orders-service'; using { OrdersService } from '../srv/orders-service';
@odata.draft.enabled @odata.draft.enabled
@@ -68,7 +68,7 @@ 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'},

View File

@@ -1,5 +0,0 @@
/*
This model controls what gets served to Fiori frontends...
*/
using from './orders/fiori-service';

View File

@@ -3,21 +3,22 @@ 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 Orders_Items on Items.up_ = $self; Items : Composition of many {
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
quantity : 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';

View File

@@ -2,6 +2,7 @@
"name": "@capire/orders", "name": "@capire/orders",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@capire/common": "*",
"@sap/cds": "^5" "@sap/cds": "^5"
} }
} }

View File

@@ -3,7 +3,7 @@ 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

10924
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@
"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",

View File

@@ -1,2 +1,2 @@
cds.requires.messaging.kind = file-based-messaging # cds.requires.messaging.kind = file-based-messaging
PORT = 4005 PORT = 4005

View File

@@ -1,5 +1,5 @@
subject;rating;title;text subject;rating;reviewer;title;text
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;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;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. 201;4;bob;Fascinating;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Id diam maecenas ultricies mi eget mauris pharetra et. Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque. Pulvinar mattis nunc sed blandit libero. Facilisis magna etiam tempor orci eu. Nec sagittis aliquam malesuada bibendum arcu. Eu consequat ac felis donec. Ultricies tristique nulla aliquet enim tortor at auctor urna nunc. Tortor posuere ac ut consequat semper viverra nam libero. Amet nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Scelerisque purus semper eget duis at tellus. Elementum tempus egestas sed sed risus pretium. Arcu dictum varius duis at. Amet luctus venenatis lectus magna fringilla urna. Eget velit aliquet sagittis id consectetur purus ut faucibus. Vitae auctor eu augue ut lectus. Fermentum iaculis eu non diam phasellus vestibulum.
207;2;What is this?;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero justo laoreet sit amet cursus sit amet dictum. Nunc faucibus a pellentesque sit. Dis parturient montes nascetur ridiculus mus mauris vitae ultricies. Enim nunc faucibus a pellentesque. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien. Cras ornare arcu dui vivamus. Facilisi etiam dignissim diam quis enim lobortis. Et molestie ac feugiat sed. Urna neque viverra justo nec ultrices dui. Ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis. Dignissim sodales ut eu sem. Feugiat in fermentum posuere urna nec. At augue eget arcu dictum varius. 207;2;bob;What is this?;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero justo laoreet sit amet cursus sit amet dictum. Nunc faucibus a pellentesque sit. Dis parturient montes nascetur ridiculus mus mauris vitae ultricies. Enim nunc faucibus a pellentesque. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien. Cras ornare arcu dui vivamus. Facilisi etiam dignissim diam quis enim lobortis. Et molestie ac feugiat sed. Urna neque viverra justo nec ultrices dui. Ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis. Dignissim sodales ut eu sem. Feugiat in fermentum posuere urna nec. At augue eget arcu dictum varius.
251;3;It's dark...;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Suscipit tellus mauris a diam. Velit aliquet sagittis id consectetur purus ut. Viverra adipiscing at in tellus integer. Vitae elementum curabitur vitae nunc. Mattis ullamcorper velit sed ullamcorper morbi. Diam quis enim lobortis scelerisque. Auctor neque vitae tempus quam pellentesque nec nam aliquam. Semper auctor neque vitae tempus. Quis eleifend quam adipiscing vitae proin. Neque convallis a cras semper auctor neque vitae. Imperdiet massa tincidunt nunc pulvinar sapien et ligula. Sit amet consectetur adipiscing elit ut aliquam purus. Pretium quam vulputate dignissim suspendisse. 251;3;bob;It's dark...;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Suscipit tellus mauris a diam. Velit aliquet sagittis id consectetur purus ut. Viverra adipiscing at in tellus integer. Vitae elementum curabitur vitae nunc. Mattis ullamcorper velit sed ullamcorper morbi. Diam quis enim lobortis scelerisque. Auctor neque vitae tempus quam pellentesque nec nam aliquam. Semper auctor neque vitae tempus. Quis eleifend quam adipiscing vitae proin. Neque convallis a cras semper auctor neque vitae. Imperdiet massa tincidunt nunc pulvinar sapien et ligula. Sit amet consectetur adipiscing elit ut aliquam purus. Pretium quam vulputate dignissim suspendisse.
1 subject rating reviewer title text
2 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.
3 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.
4 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.
5 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.

View File

@@ -10,15 +10,14 @@
"@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": {
"db": { "messaging": {
"kind": "sql" "[development]": { "kind": "file-based-messaging" },
} "[hybrid]": { "kind": "enterprise-messaging-shared" },
"[production]": { "kind": "enterprise-messaging" }
},
"db": { "kind": "sql" }
} }
} }
} }

View File

@@ -8,10 +8,11 @@ service ReviewsService {
action unlike (review: type of Reviews:ID); action unlike (review: type of Reviews:ID);
// Async API // Async API
event reviewed : { event reviewed : {
subject: type of Reviews:subject; subject : type of Reviews:subject;
rating: Decimal(2,1) count : Integer;
} rating : Decimal;
}
// Input validation // Input validation
annotate Reviews with { annotate Reviews with {
@@ -27,14 +28,7 @@ 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' },
]; ];

View File

@@ -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 {rating} = await cds.tx(req) .run ( const { count, rating } = await cds.tx(req) .run (
SELECT.one (['round(avg(rating),2) as rating']) .from (Reviews) .where ({subject}) SELECT.one `round(avg(rating),2) as rating, count(*) as count` .from (Reviews) .where ({subject})
) )
global.it || console.log ('< emitting:', 'reviewed', { subject, rating }) global.it || console.log ('< emitting:', 'reviewed', { subject, count, rating })
await this.emit ('reviewed', { subject, rating }) await this.emit ('reviewed', { subject, count, rating })
}) })
// Increment counter for reviews considered helpful // Increment counter for reviews considered helpful

View File

@@ -51,21 +51,28 @@ Each sub directory essentially is an individual npm package arranged in an [all-
- As well as managed data, input validations, and authorization - As well as managed data, input validations, and authorization
## [@capire/fiori](fiori) ## [@capire/bookstore](bookstore)
- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages: - A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:
- [@capire/bookshop](bookshop) - [@capire/bookshop](bookshop)
- [@capire/reviews](reviews) - [@capire/reviews](reviews)
- [@capire/orders](orders) - [@capire/orders](orders)
- [@capire/common](common) - [@capire/common](common)
- [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 - [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) - [OpenAPI export + Swagger UI](https://cap.cloud.sap/docs/advanced/openapi)
## [@capire/fiori](fiori)
- [Adds an SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookstore, 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
<br> <br>
# All-in-one Monorepo # All-in-one Monorepo

View File

@@ -42,24 +42,132 @@ describe('cds.ql → cqn', () => {
}) })
}) })
if (each === 'SELECT')
test('from Foo [<key>]', () => { test('SELECT ( Foo )', () => {
expect({
expect(cqn = SELECT`from Foo[11]`) SELECT: { from: { ref: ['Foo'] } },
.to.eql(SELECT`from Foo[${11}]`)
.to.eql(SELECT.from `Foo[11]`)
.to.eql(SELECT.from `Foo[${11}]`)
.to.eql(SELECT`Foo[11]`)
expect.plain(cqn)
.to.eql(CQL`SELECT from Foo[11]`)
.to.eql(srv.read`Foo[11]`)
.to.eql({
SELECT: { from: {
ref: [{ id: 'Foo', where: [{ val: 11 }] }]
}},
}) })
.to.eql(CQL`SELECT from Foo`)
.to.eql(SELECT(Foo))
})
if (cdr) expect.plain (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({
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 )', () => {
expect({
SELECT: { from: {ref: [{ id:'Foo', where: [{val:11}] }] }}
})
.to.eql(srv.read`Foo[${11}]`) .to.eql(srv.read`Foo[${11}]`)
.to.eql(SELECT`Foo[${11}]`) .to.eql(SELECT`Foo[${11}]`)
@@ -86,14 +194,24 @@ describe('cds.ql → cqn', () => {
.to.eql(SELECT.from(Foo,{ID:11})) .to.eql(SELECT.from(Foo,{ID:11}))
.to.eql(SELECT.from(Foo).byKey(11)) .to.eql(SELECT.from(Foo).byKey(11))
.to.eql(SELECT.from(Foo).byKey({ID:11})) .to.eql(SELECT.from(Foo).byKey({ID:11}))
expect.one(cqn) if (cds.version >= '5.6.0') {
.to.eql({ expect.one(cqn)
SELECT: { .to.eql({
one: true, SELECT: {
from: { ref: ['Foo'] }, one: true,
where: [{ ref: ['ID'] }, '=', { val: 11 }], from: { ref: [{ id: 'Foo', where: [{ ref: ['ID'] }, '=', { val: 11 }] }] },
}, },
}) })
} else {
expect.one(cqn)
.to.eql({
SELECT: {
one: true,
from: { ref: ['Foo'] },
where: [{ ref: ['ID'] }, '=', { val: 11 }],
},
})
}
}) })
@@ -131,15 +249,27 @@ describe('cds.ql → cqn', () => {
// Test combination with key as second argument to .from // Test combination with key as second argument to .from
expect(cqn = SELECT.from(Foo, 11, ['a'])) expect(cqn = SELECT.from(Foo, 11, ['a']))
.to.eql(SELECT.from(Foo, 11, foo => foo.a)) .to.eql(SELECT.from(Foo, 11, foo => foo.a))
expect.one(cqn)
.to.eql({ if (cds.version >= '5.6.0') {
SELECT: { expect.one(cqn)
one: true, .to.eql({
from: { ref: ['Foo'] }, SELECT: {
columns: [{ ref: ['a'] }], one: true,
where: [{ ref: ['ID'] }, '=', { val: 11 }], from: { ref: [{ id: 'Foo', where: [{ ref: ['ID'] }, '=', { val: 11 }]}] },
}, columns: [{ ref: ['a'] }]
}) },
})
} else {
expect.one(cqn)
.to.eql({
SELECT: {
one: true,
from: { ref: ['Foo'] },
columns: [{ ref: ['a'] }],
where: [{ ref: ['ID'] }, '=', { val: 11 }],
},
})
}
}) })
@@ -551,21 +681,31 @@ describe('cds.ql → cqn', () => {
describe(`UPDATE...`, () => { describe(`UPDATE...`, () => {
test('entity (..., <key>)', () => { test('entity (..., <key>)', () => {
expect(UPDATE(Books, 4711)) const cqnWhere = {
.to.eql(UPDATE(Books, { ID: 4711 }))
.to.eql(UPDATE(Books).byKey(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, { ID: 4711 }))
// etc...
.to.eql({
UPDATE: { UPDATE: {
entity: 'capire.bookshop.Books', entity: 'capire.bookshop.Books',
where: [{ ref: ['ID'] }, '=', { val: 4711 }], 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))
.to.eql(UPDATE(Books, { ID: 4711 }))
.to.eql(UPDATE(Books).byKey(4711))
.to.eql(UPDATE(Books).byKey({ ID: 4711 }))
.to.eql(UPDATE.entity(Books, 4711))
.to.eql(UPDATE.entity(Books, { ID: 4711 }))
// etc...
.to.eql(cqnKey)
}) })
/* /*
@@ -616,20 +756,29 @@ 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(DELETE.from(Books).where({ ID: 4711 })) .to.eql(cqnKey)
.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', () => {

View File

@@ -1,8 +1,6 @@
const cds = require('@sap/cds/lib') const cds = require('@sap/cds/lib')
const {expect} = cds.test 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 (`
entity Categories { entity Categories {
@@ -78,9 +76,7 @@ describe('Hierarchical Data', ()=>{
{ ID:101, name:'Cat' }, { ID:101, name:'Cat' },
{ ID:108, name:'Catweazle' } { ID:108, name:'Catweazle' }
] ]
return 'skipped as this will be fixed in a newer cds version' 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)
}) })
}) })

View File

@@ -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([
{ subject: '201', rating: 1 }, { count: 1, subject: '201', rating: 1 },
{ subject: '201', rating: 1.5 }, { count: 2, subject: '201', rating: 1.5 },
{ subject: '201', rating: 2 }, { count: 3, subject: '201', rating: 2 },
{ subject: '201', rating: 2.5 }, { count: 4, subject: '201', rating: 2.5 },
{ subject: '201', rating: 3 }, { count: 5, subject: '201', rating: 3 },
]) ])
}) })
}) })

54
test/registry.test.js Normal file
View File

@@ -0,0 +1,54 @@
const { fork } = require('child_process')
const { resolve } = require('path')
const Axios = require('axios')
const verbose = process.env.CDS_TEST_VERBOSE
// ||true
describe('Local NPM registry', () => {
let registry
let axios
const cwd = resolve(__dirname, '..')
beforeAll(async ()=> {
const env = Object.assign(process.env, {PORT:'0'})
const res = await exec (resolve(cwd, '.registry/server.js'), {cwd, stdio: 'pipe', env})
registry = res.cp
axios = Axios.default.create ({ baseURL: res.url, validateStatus: (status)=>status<500 })
})
afterAll(() => { registry.kill() })
for (const mod of ['bookshop','fiori','orders','reviews']) {
it(`should serve ${mod}`, async () => {
const resp = await axios.get(`/@capire/${mod}`)
expect(resp.data).toMatchObject({name: `@capire/${mod}`, versions:{}})
const versions = Object.values(resp.data.versions)
await axios.get(versions[0].dist.tarball)
})
}
it(`should return 404 for unknown packages`, async () => {
let resp = await axios.get(`/@capire/foo`)
expect(resp.status).toEqual(404)
resp = await axios.get(`/foo`)
expect(resp.status).toEqual(404)
})
})
function exec (script, opts) {
return new Promise((resolve, reject)=> {
const cp = fork (script, [], opts)
.on('error', err => reject(new Error(err)))
cp.stdout.on('data', chunk => {
if (verbose) console.log(chunk.toString())
if (chunk.toString().match(/listening.*(http:.*:\d+)/i)) {
resolve({cp, url:RegExp.$1})
}
})
cp.stderr.on('data', chunk => {
if (verbose) console.error(chunk.toString())
})
})
}