Compare commits
1 Commits
fk-constra
...
adding-que
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae0cfc8c1e |
@@ -21,7 +21,6 @@
|
|||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
"require-atomic-updates": "off",
|
"require-atomic-updates": "off"
|
||||||
"require-await":"warn"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: This channel is CLOSED.
|
name: Question, feedback or bug?
|
||||||
about: Use our community at https://answers.sap.com/tags/9f13aee1-834c-4105-8e43-ee442775e5ce
|
about: Use our community!
|
||||||
title: ''
|
title: ''
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|||||||
2
.github/workflows/node.js.yml
vendored
2
.github/workflows/node.js.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [12.x, 14.x]
|
node-version: [10.x, 12.16.2]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|||||||
1
.mocharc.yml
Normal file
1
.mocharc.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
parallel: true
|
||||||
1
.registry/.gitignore
vendored
1
.registry/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
*.tgz
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
const { exec } = require ('child_process')
|
|
||||||
const express = require ('express')
|
|
||||||
const fs = require ('fs')
|
|
||||||
const app = express()
|
|
||||||
|
|
||||||
const { PORT=4444 } = process.env
|
|
||||||
const [,,port=PORT] = process.argv
|
|
||||||
process.chdir(__dirname)
|
|
||||||
|
|
||||||
app.use('/-/:tarball', (req,res,next) => {
|
|
||||||
console.debug ('GET', req.params)
|
|
||||||
try {
|
|
||||||
const { tarball } = req.params
|
|
||||||
const [, pkg ] = /^capire-(\w+)/.exec(tarball)
|
|
||||||
fs.lstat(tarball,(err => {
|
|
||||||
if (err) exec(`npm pack ../${pkg}`,next)
|
|
||||||
else next()
|
|
||||||
}))
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
res.sendStatus(500)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.use('/-', express.static(__dirname))
|
|
||||||
|
|
||||||
app.get('/*', (req,res)=>{
|
|
||||||
const url = decodeURIComponent(req.url)
|
|
||||||
console.debug ('GET',url)
|
|
||||||
try {
|
|
||||||
const [, capire, pkg ] = /^\/(@capire)\/(\w+)/.exec(url)
|
|
||||||
const package = require (`${capire}/${pkg}/package.json`)
|
|
||||||
const tarball = `capire-${pkg}-${package.version}.tgz`
|
|
||||||
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
|
|
||||||
res.json({
|
|
||||||
"name": package.name,
|
|
||||||
"dist-tags": {
|
|
||||||
"latest": package.version
|
|
||||||
},
|
|
||||||
"versions": {
|
|
||||||
[package.version]: {
|
|
||||||
"name": package.name,
|
|
||||||
"version": package.version,
|
|
||||||
"dist": {
|
|
||||||
"tarball": `http://localhost:${port}/-/${tarball}`
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
res.sendStatus(404)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.listen(port, ()=>{
|
|
||||||
console.log (`npm set @capire:registry=http://localhost:${port}`)
|
|
||||||
console.log (`@capire registry listening on http://localhost:${port}`)
|
|
||||||
exec(`npm set @capire:registry=http://localhost:${port}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
const _exit = ()=>{
|
|
||||||
console.log ('\nnpm conf rm @capire:registry')
|
|
||||||
exec('npm conf rm @capire:registry')
|
|
||||||
exec('rm *.tgz')
|
|
||||||
process.exit()
|
|
||||||
}
|
|
||||||
process.on ('SIGTERM',_exit)
|
|
||||||
process.on ('SIGHUP',_exit)
|
|
||||||
process.on ('SIGINT',_exit)
|
|
||||||
process.on ('SIGUSR2',_exit)
|
|
||||||
29
.reuse/dep5
29
.reuse/dep5
@@ -1,29 +0,0 @@
|
|||||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
|
||||||
Upstream-Name: cloud-cap-samples
|
|
||||||
Upstream-Contact: <Christian Georgi (christian.georgi@sap.com)>
|
|
||||||
Source: https://github.com/SAP-samples/cloud-cap-samples
|
|
||||||
Disclaimer: The code in this project may include calls to APIs (“API Calls”) of
|
|
||||||
SAP or third-party products or services developed outside of this project
|
|
||||||
(“External Products”).
|
|
||||||
“APIs” means application programming interfaces, as well as their respective
|
|
||||||
specifications and implementing code that allows software to communicate with
|
|
||||||
other software.
|
|
||||||
API Calls to External Products are not licensed under the open source license
|
|
||||||
that governs this project. The use of such API Calls and related External
|
|
||||||
Products are subject to applicable additional agreements with the relevant
|
|
||||||
provider of the External Products. In no event shall the open source license
|
|
||||||
that governs this project grant any rights in or to any External Products,or
|
|
||||||
alter, expand or supersede any terms of the applicable additional agreements.
|
|
||||||
If you have a valid license agreement with SAP for the use of a particular SAP
|
|
||||||
External Product, then you may make use of any API Calls included in this
|
|
||||||
project’s code for that SAP External Product, subject to the terms of such
|
|
||||||
license agreement. If you do not have a valid license agreement for the use of
|
|
||||||
a particular SAP External Product, then you may only make use of any API Calls
|
|
||||||
in this project for that SAP External Product for your internal, non-productive
|
|
||||||
and non-commercial test and evaluation of such API Calls. Nothing herein grants
|
|
||||||
you any rights to use or access any SAP External Product, or provide any third
|
|
||||||
parties the right to use of access any SAP External Product, through API Calls.
|
|
||||||
|
|
||||||
Files: *
|
|
||||||
Copyright: 2019-2020 SAP SE or an SAP affiliate company and cap-cloud-samples
|
|
||||||
License: Apache-2.0
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://aka.ms/codetour-schema",
|
|
||||||
"title": "Database Functions",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"title": "Introduction",
|
|
||||||
"description": "### Database Functions in CDS Models\n\nIn this tour, you'll learn how to add database-specific functions to CDS models in your application."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "bookshop/db/schema.cds",
|
|
||||||
"description": "#### Basic Schema\n\nWe want to add two fields to the `Authors` entity, one for the author's age and one for the span of years that she or he lived.\n\nThese two fields can be computed out of the existing `dateOfBirth` and `dateOfDeath` fields.",
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 19,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 21,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Base fields in Author"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "bookshop/srv/admin-service.cds",
|
|
||||||
"description": "This is how the `Authors` entity gets exposed in an OData or REST service.\n\nIn the next step, you'll see how we extend this projection.",
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 4,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 5,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Authors service"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "fiori/db/sqlite/index.cds",
|
|
||||||
"description": "#### SQLite Implementation\n\nHere's the first implementation for SQLite. It computes the two fields `age` and `lifetime` through SQLite's [strftime](https://sqlite.org/lang_datefunc.html) function.\n\nThrough the [`extend projection`](https://cap.cloud.sap/docs/cds/cdl#extend-view) clause you can add additional fields to projection entities. These are deployed as database views, which is why we can integrate the database functions in the first place.\n",
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 7,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 11,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "SQLite implementation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "fiori/db/hana/index.cds",
|
|
||||||
"description": "#### SAP HANA Implementation\n\nThis is the second implementation for SAP HANA. It computes the same two fields `age` and `lifetime` through the [YEARS_BETWEEN](https://help.sap.com/viewer/7c78579ce9b14a669c1f3295b0d8ca16/Cloud/en-US/7c0d2c161ea34def86de3f5eadd6a0af.html) and [YEAR](https://help.sap.com/viewer/7c78579ce9b14a669c1f3295b0d8ca16/Cloud/en-US/20f5fac6751910148dabd3c6821f907d.html) functions of SAP HANA.\n\n#### File Layout and Code Structure\n\nNote the path of the `.cds` file we are in: it's in a subfolder of `db`, so that it's _not_ automatically picked up when we start the application. The same is true for the SQLite implementation: it's in a separate `db/sqlite/` folder as well. In the next step, you'll see how these files are loaded.\n\nAlso, we choose to implement all of that as an extension of the original bookshop here in the _fiori_ package. See the first [CAP Samples] code tour for more details on the different packages of this repository.",
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 7,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 11,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "SAP HANA implementation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "fiori/package.json",
|
|
||||||
"description": "#### Configuration\n\nThe `cds` section in `package.json` is a place to configure which of the `db/sqlite` and `db/hana` folders are used for which database.\nWe use [Node.js profiles](https://cap.cloud.sap/docs/node.js/cds-env#profiles) to separate the configuration.\nIn the `development` profile, you can see that `db/sqlite` is set as the model, while the `db/hana` folder is configured in the `production` profile.",
|
|
||||||
"line": 17,
|
|
||||||
"title": "Configuration"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "fiori/package.json",
|
|
||||||
"description": "#### Run with SQLite\n\nTo run with `development` and an in-memory SQLite database, you don't need to do anything special, because it's activated by default. Just run:\n\n>> cds watch fiori\n\nThen open [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) to see the two new fields.\n",
|
|
||||||
"line": 28,
|
|
||||||
"title": "Run with SQLite"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "fiori/package.json",
|
|
||||||
"description": "#### Deploy the CDS Model to SAP HANA\n\nTo 'activate' SAP HANA through the `production` profile, you can use the global `--production` flag:\n\n>> cd fiori; cds deploy --to hana --production\n\n[Learn more about SAP HANA deployment](https://cap.cloud.sap/docs/guides/databases#get-hana)\n\n#### Run the Application\n\n>> cd fiori; cds watch --production\n\nThe service on [http://localhost:4004/admin/Authors](http://localhost:4004/admin/Authors) is the same as before, but this time the `Authors` entity is backed by a database view with an SAP HANA function.\n\n#### More\n\nIf you don't see data, you can add some in the next step.",
|
|
||||||
"line": 31,
|
|
||||||
"title": "Run with SAP HANA"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "fiori/test/requests.http",
|
|
||||||
"description": "### Add More Data\n\nOptionally you can add some `Authors` data by clicking on the _Send Request_ link (provided by the [REST client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) extension).",
|
|
||||||
"line": 68,
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 67,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 73,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Add Data"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Wrap-up",
|
|
||||||
"description": "### Summary\n\nThat's it! You have seen: \n- How to integrate database-specific functions in a CDS model.\n- How to switch between the two implementations for SQLite and SAP HANA."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"ref": "master"
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://aka.ms/codetour-schema",
|
|
||||||
"title": "CAP Samples",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"title": "Welcome",
|
|
||||||
"file": "README.md",
|
|
||||||
"description": "### Welcome to CAP Samples!\n\nThis tour leads you through a collection of samples for the [SAP Cloud Application Programming Model (CAP)](https://cap.cloud.sap).\nYou will learn which features of the programming model are demonstrated in which sample.\n\nLet's start!",
|
|
||||||
"line": 2,
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 1,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 3,
|
|
||||||
"character": 108
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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).",
|
|
||||||
"line": 2,
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 1,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 4,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Hello World!"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "bookshop/db/schema.cds",
|
|
||||||
"description": "### A Bookshop!\n\nIntroduces:\n- [Project Setup](https://cap.cloud.sap/docs/get-started/) and [Layouts](https://cap.cloud.sap/docs/get-started/projects)\n- [Domain Modeling](https://cap.cloud.sap/docs/guides/domain-models)\n- [Defining Services](https://cap.cloud.sap/docs/guides/providing-services)\n- [Generic Providers](https://cap.cloud.sap/docs/guides/generic-providers)\n- [Adding Custom Logic](https://cap.cloud.sap/docs/guides/service-impl)\n- [Using Databases](https://cap.cloud.sap/docs/guides/databases)\n",
|
|
||||||
"line": 1,
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 1,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 32,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Bookshop"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "common/index.cds",
|
|
||||||
"description": "### Extend and Reuse\n\nShowcases how to extend [@sap/cds/common](https://cap.cloud.sap/docs/cds/common) thereby covering:\n- Building [extension packages](https://cap.cloud.sap/docs/guides/domain-models#aspects-extensibility)\n- Providing [reuse packages](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content)\n- [Verticalization](https://cap.cloud.sap/docs/cds/common#adapting-to-your-needs)\n- Using [Aspects](https://cap.cloud.sap/docs/cds/cdl#aspects)\n- Used in the [fiori app sample](#fiori)\n",
|
|
||||||
"line": 1,
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 1,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 46,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Common"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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",
|
|
||||||
"line": 1,
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 1,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 27,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Orders"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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",
|
|
||||||
"line": 1,
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 1,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 39,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Reviews"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "fiori/app/index.cds",
|
|
||||||
"description": "### Annotations for SAP Fiori Elements\n\nA [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:\n - [@capire/bookshop](bookshop)\n - [@capire/reviews](reviews)\n - [@capire/orders](orders)\n - [@capire/common](common)\n\n[Adds a SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookshop, thereby introducing to:\n - [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files\n - Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)\n - Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)\n - Serving SAP Fiori apps locally\n\n[The Vue.js app](bookshop/app/vue) imported from bookshop is served as well.\n",
|
|
||||||
"line": 1,
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 1,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 13,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Fiori"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"file": "package.json",
|
|
||||||
"description": "### All-in-one Monorepo\n\nEach sample sub directory essentially is a standard npm package, some with standard npm dependencies to other samples. The root folder's [package.json](package.json) has local links to the sub folders, such that an `npm install` populates a local `node_modules` folder acts like a local npm registry to the individual sample packages.\n",
|
|
||||||
"line": 8,
|
|
||||||
"selection": {
|
|
||||||
"start": {
|
|
||||||
"line": 8,
|
|
||||||
"character": 1
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 15,
|
|
||||||
"character": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"title": "Packages"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isPrimary": true,
|
|
||||||
"description": "Overview of CAP Samples for Node.js"
|
|
||||||
}
|
|
||||||
5
.vscode/extensions.json
vendored
5
.vscode/extensions.json
vendored
@@ -4,15 +4,14 @@
|
|||||||
|
|
||||||
// List of extensions which should be recommended for users of this workspace.
|
// List of extensions which should be recommended for users of this workspace.
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"sapse.vscode-cds",
|
"SAPSE.vscode-cds",
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"mechatroner.rainbow-csv",
|
"mechatroner.rainbow-csv",
|
||||||
"humao.rest-client",
|
"humao.rest-client",
|
||||||
"alexcvzz.vscode-sqlite",
|
"alexcvzz.vscode-sqlite",
|
||||||
"hbenl.vscode-mocha-test-adapter",
|
"hbenl.vscode-mocha-test-adapter",
|
||||||
"sdras.night-owl",
|
"sdras.night-owl"
|
||||||
"vsls-contrib.codetour"
|
|
||||||
],
|
],
|
||||||
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
||||||
"unwantedRecommendations": [
|
"unwantedRecommendations": [
|
||||||
|
|||||||
28
.vscode/launch.json
vendored
28
.vscode/launch.json
vendored
@@ -6,29 +6,17 @@
|
|||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "bookshop",
|
"name": "bookshop",
|
||||||
"command": "npx cds watch bookshop",
|
"command": "cds watch bookshop",
|
||||||
"type": "node-terminal",
|
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"skipFiles": [
|
"type": "node-terminal",
|
||||||
"<node_internals>/**",
|
"skipFiles": ["<node_internals>/**"]
|
||||||
"**/node_modules/**",
|
|
||||||
"**/cds/lib/lazy.js",
|
|
||||||
"**/cds/lib/req/cls.js",
|
|
||||||
"**/odata-v4/okra/**"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Fiori App",
|
"name": "Fiori app",
|
||||||
"command": "npx cds watch fiori",
|
"command": "cds watch fiori",
|
||||||
"type": "node-terminal",
|
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"skipFiles": [
|
"type": "node-terminal",
|
||||||
"<node_internals>/**",
|
"skipFiles": ["<node_internals>/**"]
|
||||||
"**/node_modules/**",
|
|
||||||
"**/cds/lib/lazy.js",
|
|
||||||
"**/cds/lib/req/cls.js",
|
|
||||||
"**/odata-v4/okra/**"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"inputs": [
|
"inputs": [
|
||||||
@@ -36,7 +24,7 @@
|
|||||||
"type": "pickString",
|
"type": "pickString",
|
||||||
"id": "sample",
|
"id": "sample",
|
||||||
"description": "Which sample do you want to start?",
|
"description": "Which sample do you want to start?",
|
||||||
"options": [ "bookshop", "fiori", "reviews", "reviews" ],
|
"options": ["bookshop", "fiori", "reviews", "reviews/test/bookshop"],
|
||||||
"default": "bookshop"
|
"default": "bookshop"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
15
.vscode/settings.json
vendored
15
.vscode/settings.json
vendored
@@ -1,17 +1,6 @@
|
|||||||
{
|
{
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
".reuse/**": true,
|
|
||||||
"**/.gitignore": true,
|
"**/.gitignore": true,
|
||||||
"**/.vscode": true,
|
"**/.vscode": true
|
||||||
"LICENSES/**": true
|
}
|
||||||
},
|
|
||||||
"debug.javascript.terminalOptions": {
|
|
||||||
"skipFiles": [
|
|
||||||
"<node_internals>/**",
|
|
||||||
"**/node_modules/**",
|
|
||||||
"**/cds/lib/lazy.js",
|
|
||||||
"**/cds/lib/req/cls.js",
|
|
||||||
"**/odata-v4/okra/**"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,3 +199,19 @@
|
|||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------------------------------------------------
|
||||||
|
APIs
|
||||||
|
|
||||||
|
This project may include APIs to SAP or third-party products or services.
|
||||||
|
The use of these APIs, products and services may be subject to additional
|
||||||
|
agreements. In no event shall the application of the Apache Software
|
||||||
|
License, v.2 to this project grant any rights in or to these APIs,
|
||||||
|
products or services that would alter, expand, be inconsistent with,
|
||||||
|
or supersede any terms of these additional agreements. “API” means
|
||||||
|
application programming interfaces, as well as their respective
|
||||||
|
specifications and implementing code that allows other software
|
||||||
|
products to communicate with or call on SAP or third party products
|
||||||
|
or services (for example, SAP Enterprise Services, BAPIs, Idocs, RFCs
|
||||||
|
and ABAP calls or other user exits) and may be made available through
|
||||||
|
SAP or third party products, SDKs, documentation or other media.
|
||||||
40
README.md
40
README.md
@@ -3,21 +3,17 @@
|
|||||||
Find here a collection of samples for the [SAP Cloud Application Programming Model](https://cap.cloud.sap) organized in a simplistic [monorepo setup](samples.md#all-in-one-monorepo). → See [**Overview** of contained samples](samples.md)
|
Find here a collection of samples for the [SAP Cloud Application Programming Model](https://cap.cloud.sap) organized in a simplistic [monorepo setup](samples.md#all-in-one-monorepo). → See [**Overview** of contained samples](samples.md)
|
||||||
|
|
||||||

|

|
||||||
[](https://api.reuse.software/info/github.com/SAP-samples/cloud-cap-samples)
|
|
||||||
|
|
||||||
### Preliminaries
|
### Preliminaries
|
||||||
|
|
||||||
1. Install [**@sap/cds-dk**](https://cap.cloud.sap/docs/get-started/) globally:
|
1. [Install @sap/cds-dk](https://cap.cloud.sap/docs/get-started/) as documented in [capire](https://cap.cloud.sap)
|
||||||
|
2. _Optional:_ [Use Visual Studio Code](https://cap.cloud.sap/docs/get-started/in-vscode)
|
||||||
|
|
||||||
```sh
|
|
||||||
npm i -g @sap/cds-dk
|
|
||||||
```
|
|
||||||
|
|
||||||
2. _Optional:_ [Use Visual Studio Code](https://cap.cloud.sap/docs/get-started/tools#vscode)
|
|
||||||
|
|
||||||
### Download
|
### Download
|
||||||
|
|
||||||
If you've [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, otherwise [download as ZIP file](archive/master.zip).
|
Clone this repo as shown below, if you have [git](https://git-scm.com/downloads) installed,
|
||||||
|
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
|
||||||
@@ -42,45 +38,21 @@ cds watch bookshop
|
|||||||
|
|
||||||
After that open this link in your browser: [http://localhost:4004](http://localhost:4004)
|
After that open this link in your browser: [http://localhost:4004](http://localhost:4004)
|
||||||
|
|
||||||
When asked to log in, type `alice` as user and leave the password field blank, which is the [default user](https://cap.cloud.sap/docs/node.js/authentication#mocked).
|
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
Run the provided tests with [_jest_](http://jestjs.io) or [_mocha_](http://mochajs.org), for example:
|
Run the provided tests with [_jest_](http://jestjs.io) or [_mocha_](http://mochajs.org), for example:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npx jest
|
npx jest
|
||||||
```
|
```
|
||||||
> While mocha is a bit smaller and faster, jest runs tests in parallel and isolation, which allows to run all tests.
|
> While mocha is a bit smaller and faster, jest runs tests in parallel and isolation, which allows to run all tests.
|
||||||
|
|
||||||
|
|
||||||
### Serve `npm`
|
|
||||||
|
|
||||||
We've included a simple npm registry mock, which allows you to do an `npm install @capire/<package>` locally. Use it as follows:
|
|
||||||
|
|
||||||
1. Start the @capire registry:
|
|
||||||
```sh
|
|
||||||
npm run registry
|
|
||||||
```
|
|
||||||
> While running this will have `@capire:registry=http://localhost:4444` set with npmrc.
|
|
||||||
|
|
||||||
2. Install one of the @capire packages wherever you like, for example:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm add @capire/common @capire/bookshop
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Code Tours
|
|
||||||
|
|
||||||
Take one of the [guided tours](.tours) in VS Code through our CAP samples and learn which CAP features are showcased by the different parts of the repository. Just install the [CodeTour extension](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.codetour) for VS Code. We'll add more code tours in the future. Stay tuned!
|
|
||||||
|
|
||||||
## Get Support
|
## Get Support
|
||||||
|
|
||||||
Check out the documentation at [https://cap.cloud.sap](https://cap.cloud.sap). <br>
|
Check out the documentation at [https://cap.cloud.sap](https://cap.cloud.sap). <br>
|
||||||
In case you've a question, find a bug, or otherwise need support, use our [community](https://answers.sap.com/tags/9f13aee1-834c-4105-8e43-ee442775e5ce) to get more visibility.
|
In case you have a question, find a bug, or otherwise need support, please use our [community](https://answers.sap.com/tags/9f13aee1-834c-4105-8e43-ee442775e5ce).
|
||||||
|
|
||||||
|
|
||||||
## 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) 2020 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](/LICENSE) file.
|
||||||
|
|||||||
1
bookshop/.env
Normal file
1
bookshop/.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
cds.features.snapi = y
|
||||||
@@ -9,36 +9,33 @@ const books = new Vue ({
|
|||||||
|
|
||||||
data: {
|
data: {
|
||||||
list: [],
|
list: [],
|
||||||
book: undefined,
|
book: { descr:'( click on a row to see details... )' },
|
||||||
order: { amount:1, succeeded:'', failed:'' }
|
order: { amount:1, succeeded:'', failed:'' }
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
search: ({target:{value:v}}) => books.fetch(v && '&$search='+v),
|
search: ({target:{value:v}}) => books.fetch (v && '$search='+v),
|
||||||
|
|
||||||
async fetch (etc='') {
|
async fetch (_filter='') {
|
||||||
const {data} = await GET(`/ListOfBooks?$expand=genre,currency${etc}`)
|
const columns = 'ID,title,author,price,stock', details = 'genre,currency'
|
||||||
|
const {data} = await GET(`/Books?$select=${columns}&$expand=${details}&${_filter}`)
|
||||||
books.list = data.value
|
books.list = data.value
|
||||||
},
|
},
|
||||||
|
|
||||||
async inspect (eve) {
|
async inspect () {
|
||||||
const book = books.book = books.list [eve.currentTarget.rowIndex-1]
|
const book = books.book = books.list [event.currentTarget.rowIndex-1]
|
||||||
const res = await GET(`/Books/${book.ID}?$select=descr,stock,image`)
|
book.descr || await GET(`/Books/${book.ID}/descr/$value`) .then (({data}) => book.descr = data)
|
||||||
Object.assign (book, res.data)
|
|
||||||
books.order = { amount:1 }
|
books.order = { amount:1 }
|
||||||
setTimeout (()=> $('form > input').focus(), 111)
|
setTimeout (()=> $('form > input').focus(), 111)
|
||||||
},
|
},
|
||||||
|
|
||||||
async submitOrder () {
|
submitOrder () { event.preventDefault()
|
||||||
const {book,order} = books, amount = parseInt (order.amount) || 1 // REVISIT: Okra should be less strict
|
const {book,order} = books, amount = parseInt (order.amount) || 1
|
||||||
try {
|
POST(`/submitOrder`, { amount, book: book.ID })
|
||||||
const res = await POST(`/submitOrder`, { amount, book: book.ID })
|
.then (()=> books.order = { amount, succeeded: `Successfully orderd ${amount} item(s).` })
|
||||||
book.stock = res.data.stock
|
.catch (e=> books.order = { amount, failed: e.response.data.error.message })
|
||||||
books.order = { amount, succeeded: `Successfully orderd ${amount} item(s).` }
|
GET(`/Books/${book.ID}/stock/$value`).then (res => book.stock = res.data)
|
||||||
} catch (e) {
|
|
||||||
books.order = { amount, failed: e.response.data.error.message }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,56 +7,50 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
|
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
|
||||||
<style>
|
<style>
|
||||||
.hovering tr:hover td { color:cyan; background: #123; cursor: pointer; }
|
#books tr:hover { background: #f2f2f2; cursor: pointer; }
|
||||||
.rating-stars { color:teal }
|
form { float:right; display:flex; flex-direction: row-reverse }
|
||||||
.succeeded { color:teal }
|
form #amount { width: 5em }
|
||||||
.failed { color:red }
|
.is-success { color: #0d920d }
|
||||||
|
.has-error { color: #df1010 }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="small-container", style="margin-top: 70px;">
|
<body class="small-container", style="margin-top: 70px;">
|
||||||
<div id='app'>
|
<div id='app'>
|
||||||
|
|
||||||
<h1> {{ document.title }} </h1>
|
<h1> Capire Books </h1>
|
||||||
|
|
||||||
<input type="text" placeholder="Search..." @input="search">
|
<input type="text" placeholder="Search..." @input="search">
|
||||||
|
|
||||||
<table id='books' class="hovering">
|
<table id='books'>
|
||||||
<thead>
|
<thead>
|
||||||
<th> Book </th>
|
<th> Book </th>
|
||||||
<th> Author </th>
|
<th> Author </th>
|
||||||
<th> Genre </th>
|
<th> Genre </th>
|
||||||
<th> Rating </th>
|
|
||||||
<th> Price </th>
|
<th> Price </th>
|
||||||
</thead>
|
</thead>
|
||||||
<tr v-for="book in list" v-bind:id="book.ID" v-on:click="inspect">
|
<tr v-for="book in list" v-bind:id="book.ID" v-on:click="inspect">
|
||||||
<td>{{ book.title }}</td>
|
<td>{{ book.title }}</td>
|
||||||
<td>{{ book.author }}</td>
|
<td>{{ book.author }}</td>
|
||||||
<td>{{ book.genre.name }}</td>
|
<td>{{ book.genre.name }}</td>
|
||||||
<td class="rating-stars">
|
<td>{{ book.currency.symbol }} {{ book.price }}</td>
|
||||||
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}
|
|
||||||
</td>
|
|
||||||
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div v-if="book">
|
<div v-if="book.title">
|
||||||
<img v-bind:src="book.image" alt=""/>
|
|
||||||
<label style="text-align:right">
|
<label style="text-align:right">
|
||||||
<span class="succeeded"> {{ order.succeeded }} </span>
|
<span class="is-success"> {{ order.succeeded }} </span>
|
||||||
<span class="failed"> {{ order.failed }} </span>
|
<span class="has-error"> {{ order.failed }} </span>
|
||||||
{{ 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="submitOrder">
|
||||||
<input type="number" v-model="order.amount" v-bind:class="{ failed: order.failed }" style="width:5em">
|
<input type="number" id="amount" v-model="order.amount" v-bind:class="{ 'has-error': order.failed }">
|
||||||
<input type="submit" value="Order:" class="muted-button">
|
<input type="submit" value="Order:" class="muted-button">
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h4> {{ book.title }} </h4>
|
<h4> {{ book.title }} </h4>
|
||||||
<p> {{ book.descr }} </p>
|
<p> {{ book.descr }} </p>
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
( click on a row to see details... )
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
ID;name
|
ID;name;dateOfBirth;placeOfBirth;dateOfDeath;placeOfDeath
|
||||||
101;Emily Brontë
|
101;Emily Brontë;1818-07-30;Thornton, Yorkshire;1848-12-19;Haworth, Yorkshire
|
||||||
107;Charlotte Brontë
|
107;Charlotte Brontë;1818-04-21;Thornton, Yorkshire;1855-03-31;Haworth, Yorkshire
|
||||||
150;Edgar Allen Poe
|
150;Edgar Allen Poe;1809-01-19;Boston, Massachusetts;1849-10-07;Baltimore, Maryland
|
||||||
170;Richard Carpenter
|
170;Richard Carpenter;1929-08-14;King’s Lynn, Norfolk;2012-02-26;Hertfordshire, England
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
ID;title;author_ID;genre_ID
|
ID;title;descr;author_ID;stock;price;currency_code;genre_ID
|
||||||
201;Wuthering Heights;101;11
|
201;Wuthering Heights;"Wuthering Heights, Emily Brontë's only novel, was published in 1847 under the pseudonym ""Ellis Bell"". It was written between October 1845 and June 1846. Wuthering Heights and Anne Brontë's Agnes Grey were accepted by publisher Thomas Newby before the success of their sister Charlotte's novel Jane Eyre. After Emily's death, Charlotte edited the manuscript of Wuthering Heights and arranged for the edited version to be published as a posthumous second edition in 1850.";101;12;11.11;GBP;11
|
||||||
207;Jane Eyre;107;11
|
207;Jane Eyre;"Jane Eyre /ɛər/ (originally published as Jane Eyre: An Autobiography) is a novel by English writer Charlotte Brontë, published under the pen name ""Currer Bell"", on 16 October 1847, by Smith, Elder & Co. of London. The first American edition was published the following year by Harper & Brothers of New York. Primarily a bildungsroman, Jane Eyre follows the experiences of its eponymous heroine, including her growth to adulthood and her love for Mr. Rochester, the brooding master of Thornfield Hall. The novel revolutionised prose fiction in that the focus on Jane's moral and spiritual development is told through an intimate, first-person narrative, where actions and events are coloured by a psychological intensity. The book contains elements of social criticism, with a strong sense of Christian morality at its core and is considered by many to be ahead of its time because of Jane's individualistic character and how the novel approaches the topics of class, sexuality, religion and feminism.";107;11;12.34;GBP;11
|
||||||
251;The Raven;150;16
|
251;The Raven;"""The Raven"" is a narrative poem by American writer Edgar Allan Poe. First published in January 1845, the poem is often noted for its musicality, stylized language, and supernatural atmosphere. It tells of a talking raven's mysterious visit to a distraught lover, tracing the man's slow fall into madness. The lover, often identified as being a student, is lamenting the loss of his love, Lenore. Sitting on a bust of Pallas, the raven seems to further distress the protagonist with its constant repetition of the word ""Nevermore"". The poem makes use of folk, mythological, religious, and classical references.";150;333;13.13;USD;16
|
||||||
252;Eleonora;150;16
|
252;Eleonora;"""Eleonora"" is a short story by Edgar Allan Poe, first published in 1842 in Philadelphia in the literary annual The Gift. It is often regarded as somewhat autobiographical and has a relatively ""happy"" ending.";150;555;14;USD;16
|
||||||
271;Catweazle;170;13
|
271;Catweazle;Catweazle is a British fantasy television series, starring Geoffrey Bayldon in the title role, and created by Richard Carpenter for London Weekend Television. The first series, produced and directed by Quentin Lawrence, was screened in the UK on ITV in 1970. The second series, directed by David Reid and David Lane, was shown in 1971. Each series had thirteen episodes, most but not all written by Carpenter, who also published two books based on the scripts.;170;22;15;EUR;13
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
ID;locale;title
|
ID;locale;title;descr
|
||||||
201;de;Sturmhöhe
|
201;de;Sturmhöhe;Sturmhöhe (Originaltitel: Wuthering Heights) ist der einzige Roman der englischen Schriftstellerin Emily Brontë (1818–1848). Der 1847 unter dem Pseudonym Ellis Bell veröffentlichte Roman wurde vom viktorianischen Publikum weitgehend abgelehnt, heute gilt er als ein Klassiker der britischen Romanliteratur des 19. Jahrhunderts.
|
||||||
201;fr;Les Hauts de Hurlevent
|
201;fr;Les Hauts de Hurlevent;Les Hauts de Hurlevent (titre original : Wuthering Heights), parfois orthographié Les Hauts de Hurle-Vent, est l'unique roman d'Emily Brontë, publié pour la première fois en 1847 sous le pseudonyme d’Ellis Bell. Loin d'être un récit moralisateur, Emily Brontë achève néanmoins le roman dans une atmosphère sereine, suggérant le triomphe de la paix et du Bien sur la vengeance et le Mal.
|
||||||
207;de;Jane Eyre
|
207;de;Jane Eyre;Jane Eyre. Eine Autobiographie (Originaltitel: Jane Eyre. An Autobiography), erstmals erschienen im Jahr 1847 unter dem Pseudonym Currer Bell, ist der erste veröffentlichte Roman der britischen Autorin Charlotte Brontë und ein Klassiker der viktorianischen Romanliteratur des 19. Jahrhunderts. Der Roman erzählt in Form einer Ich-Erzählung die Lebensgeschichte von Jane Eyre (ausgesprochen /ˌdʒeɪn ˈɛə/), die nach einer schweren Kindheit eine Stelle als Gouvernante annimmt und sich in ihren Arbeitgeber verliebt, jedoch immer wieder um ihre Freiheit und Selbstbestimmung kämpfen muss. Als klein, dünn, blass, stets schlicht dunkel gekleidet und mit strengem Mittelscheitel beschrieben, gilt die Heldin des Romans Jane Eyre nicht zuletzt aufgrund der Kino- und Fernsehversionen der melodramatischen Romanvorlage als die bekannteste englische Gouvernante der Literaturgeschichte
|
||||||
252;de;Eleonora
|
252;de;Eleonora;“Eleonora” ist eine Erzählung von Edgar Allan Poe. Sie wurde 1841 erstveröffentlicht. In ihr geht es um das Paradox der Treue in der Treulosigkeit.
|
||||||
|
@@ -14,8 +14,3 @@ ID;parent_ID;name
|
|||||||
22;21;Autobiography
|
22;21;Autobiography
|
||||||
23;20;Essay
|
23;20;Essay
|
||||||
24;20;Speech
|
24;20;Speech
|
||||||
30;;Others
|
|
||||||
31;30;Other-A
|
|
||||||
32;30;Other-A
|
|
||||||
33;30;Other-A
|
|
||||||
34;30;Other-A
|
|
||||||
|
|||||||
|
@@ -1,19 +1,28 @@
|
|||||||
using { Currency, managed, sap } from '@sap/cds/common';
|
using { Currency, managed, sap } from '@sap/cds/common';
|
||||||
namespace sap.capire.bookshop;
|
namespace sap.capire.bookshop;
|
||||||
|
|
||||||
entity Books {
|
entity Books : managed {
|
||||||
key ID : Integer;
|
key ID : Integer;
|
||||||
title : localized String(111);
|
title : localized String(111);
|
||||||
|
descr : localized String(1111);
|
||||||
author : Association to Authors;
|
author : Association to Authors;
|
||||||
genre : Association to Genres;
|
genre : Association to Genres;
|
||||||
|
stock : Integer;
|
||||||
|
price : Decimal(9,2);
|
||||||
|
currency : Currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
entity Authors {
|
entity Authors : managed {
|
||||||
key ID : Integer;
|
key ID : Integer;
|
||||||
name : String(111);
|
name : String(111);
|
||||||
|
dateOfBirth : Date;
|
||||||
|
dateOfDeath : Date;
|
||||||
|
placeOfBirth : String;
|
||||||
|
placeOfDeath : String;
|
||||||
books : Association to many Books on books.author = $self;
|
books : Association to many Books on books.author = $self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Hierarchically organized Code List for Genres */
|
||||||
entity Genres : sap.common.CodeList {
|
entity Genres : sap.common.CodeList {
|
||||||
key ID : Integer;
|
key ID : Integer;
|
||||||
parent : Association to Genres;
|
parent : Association to Genres;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
namespace sap.capire.bookshop; //> important for reflection
|
namespace sap.capire.bookshop; //> important for reflection
|
||||||
using from './db/schema';
|
using from './db/schema';
|
||||||
|
using from './srv/cat-service';
|
||||||
using from './srv/admin-service';
|
using from './srv/admin-service';
|
||||||
|
|||||||
1
bookshop/index.js
Normal file
1
bookshop/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
exports.CatalogService = require('./srv/cat-service')
|
||||||
@@ -3,42 +3,19 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "A simple self-contained bookshop service.",
|
"description": "A simple self-contained bookshop service.",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sap/cds": "^5.0.4",
|
"@capire/common": "../common",
|
||||||
"express": "^4.17.1",
|
"@sap/cds": ">=3.33.1",
|
||||||
"passport": "0.4.1",
|
"express": "^4.17.1"
|
||||||
"@sap/hana-client": "^2.7.21"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"genres": "cds serve test/genres.cds",
|
||||||
|
"start": "cds run",
|
||||||
"watch": "cds watch"
|
"watch": "cds watch"
|
||||||
},
|
},
|
||||||
"cds": {
|
"cds": {
|
||||||
"requires": {
|
"requires": {
|
||||||
"db": {
|
"db": {
|
||||||
"kind": "sql",
|
"kind": "sql"
|
||||||
"xxx-model": "*",
|
|
||||||
"xxx-credentials": {
|
|
||||||
"database": "localSqlite.db"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"[production]": {
|
|
||||||
"db": {
|
|
||||||
"kind": "hana",
|
|
||||||
"model": [
|
|
||||||
"db",
|
|
||||||
"srv"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"features": {
|
|
||||||
"assert_integrity": false
|
|
||||||
},
|
|
||||||
"hana": {
|
|
||||||
"deploy-format": "hdbtable"
|
|
||||||
},
|
|
||||||
"cdsc": {
|
|
||||||
"beta": {
|
|
||||||
"foreignKeyConstraints": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,126 +1,31 @@
|
|||||||
# Foreign key constraints on DB
|
# Bookshop Getting Started Sample
|
||||||
|
|
||||||
As model we use a simplified version of the bookshop scenario, we work with nodejs runtime.
|
This stand-alone sample introduces the essential tasks in the development of CAP-based services as also covered in the [Getting Started guide in capire](https://cap.cloud.sap/docs/get-started/in-a-nutshell).
|
||||||
|
|
||||||
|
## Hypothetical Use Cases
|
||||||
|
|
||||||
## Preparation
|
1. Build a service that allows to browse _Books_ and _Authors_.
|
||||||
|
2. Books have assigned _Genres_ which are organized hierarchically.
|
||||||
|
3. All users may browse books without login.
|
||||||
|
4. All entries are maintained by Administrators.
|
||||||
|
5. End users may order books (the actual order mgmt being out of scope)
|
||||||
|
|
||||||
As we want to see the effects of the database constaints, we first need to switch off the integrity checks done by the node runtime. In the `package.json`:
|
## Running the Sample
|
||||||
```json
|
|
||||||
"cds": {
|
```sh
|
||||||
...,
|
npm run watch
|
||||||
"features": {
|
|
||||||
"assert_integrity": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Then switch on constraint generation. In the `package.json`:
|
## Content & Best Practices
|
||||||
```json
|
|
||||||
"cds": {
|
|
||||||
...,
|
|
||||||
"cdsc": {
|
|
||||||
"beta": {
|
|
||||||
"foreignKeyConstraints": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## SQLite
|
| Links to capire | Sample files / folders |
|
||||||
|
| --------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
||||||
In SQLite, FK constraints can only be specified during the creation of a table. They cannot be altered for an existing table. So here we at least don't have issues with existing data.
|
| [Project Setup and Layouts](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content) | [`./`](./) |
|
||||||
|
| [Defining Domain Models](https://cap.cloud.sap/docs/guides/domain-models) | [`./db/schema.cds`](./db/schema.cds) |
|
||||||
For SQLite, constraint checks need to be enabled by application at runtime for each db connection with the command
|
| [Defining Services](https://cap.cloud.sap/docs/guides/providing-services) | [`./srv/*.cds`](./srv) |
|
||||||
```sql
|
| [Single-purposed Services](https://cap.cloud.sap/docs/guides/providing-services#single-purposed-services) | [`./srv/*.cds`](./srv) |
|
||||||
PRAGMA foreign_keys = ON;
|
| [Generic Providers](https://cap.cloud.sap/docs/guides/providing-services) | http://localhost:4004 |
|
||||||
```
|
| Using Databases | [`./db/data/*.csv`](./db/data) |
|
||||||
We do this via a custom handler in `admin-service.js`.
|
| [Adding Custom Logic](https://cap.cloud.sap/docs/guides/service-impl) | [`./srv/*.js`](./srv) |
|
||||||
|
| Adding Tests | [`./test`](./test) |
|
||||||
Now run the requests in `requests.http` (use `http://localhost:4004` as server URL). They behave as expected.
|
| [Sharing for Reuse](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content) | [`./index.cds`](./index.cds) |
|
||||||
|
|
||||||
But the error messages are not providing any details. They say "constraint violation", but they don't say which table/constraint is violated:
|
|
||||||
```
|
|
||||||
HTTP/1.1 500 Internal Server Error
|
|
||||||
...
|
|
||||||
{
|
|
||||||
"error": {
|
|
||||||
"code": "SQLITE_CONSTRAINT",
|
|
||||||
"message": "SQLITE_CONSTRAINT: FOREIGN KEY constraint failed in: \nCOMMIT"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The runtime constraint checks do provide the name of the restricting association (but not the name of the entity where the association lives).
|
|
||||||
|
|
||||||
## HANA
|
|
||||||
|
|
||||||
Preparation:
|
|
||||||
* logon to cf account (e.g. trial)
|
|
||||||
* create HANA service instance: `cf create-service hanatrial hdi-shared bookshop-db`
|
|
||||||
|
|
||||||
First make the deplyoment without constraints (switch off constraint generation in `package.json`).
|
|
||||||
|
|
||||||
* Run `cds build --production`. No constraints are generated.
|
|
||||||
* Deploy db model: `cf push -f gen/db`
|
|
||||||
* Deploy nodejs app: `cf push -f gen/srv --random-route`. Take note of the app URL.
|
|
||||||
|
|
||||||
Run the requests in `request.http` (use the URL from the previous step as server URL, prepend with `https://`).
|
|
||||||
Make sure to introduce some data that violates the constraints. No errors are reported - as expected.
|
|
||||||
|
|
||||||
Now switch on constraint generation in `package.json`.
|
|
||||||
|
|
||||||
* Run `cds build --production`. The constraints are placed in separate `hdbconstraint` files in `gen\db\src\gen`. They are generated with `VALIDATED ON`.
|
|
||||||
* Re-deploy db model: `cf push -f gen/db`. It fails, as the existing data violates the constraints.
|
|
||||||
* Check logs: `cf logs bookshop-db-deployer --recent`.
|
|
||||||
|
|
||||||
```
|
|
||||||
Error: com.sap.hana.di.constraint: Could not create the database object [8250005]
|
|
||||||
at "src/gen/sap.capire.bookshop.Books_author.hdbconstraint" (1:1)
|
|
||||||
Error: com.sap.hana.di.constraint: Database error 7: : feature not supported: referential constraint violated by existing value [8201003]
|
|
||||||
at "src/gen/sap.capire.bookshop.Books_author.hdbconstraint" (1:1)
|
|
||||||
```
|
|
||||||
You can see which constraint is violated, but there is no info about the offensive entries.
|
|
||||||
|
|
||||||
Now use the requests from `request.http` to fix the entries that have been broken above.
|
|
||||||
|
|
||||||
* Re-deploy db model: `cf push -f gen/db`. It should succeed.
|
|
||||||
|
|
||||||
Again run the requests from `request.http`. This time all changes that would lead to a
|
|
||||||
constraint violation fail.
|
|
||||||
|
|
||||||
Inserting a book with non-existing author ID or changing a book's author ID to an invalid value yields
|
|
||||||
the message
|
|
||||||
```
|
|
||||||
HTTP/1.1 500 Internal Server Error
|
|
||||||
...
|
|
||||||
{
|
|
||||||
"error": {
|
|
||||||
"code": "500",
|
|
||||||
"message": "Internal Server Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
When trying to delete a genre that is still referenced in a book we get
|
|
||||||
```
|
|
||||||
HTTP/1.1 462 status code 462
|
|
||||||
...
|
|
||||||
{
|
|
||||||
"error": {
|
|
||||||
"code": "462",
|
|
||||||
"message": "failed on update or delete by foreign key constraint violation: TrexColumnUpdate failed on table '21F87859727E470E94917B690908A09D:SAP_CAPIRE_BOOKSHOP_GENRES' with error: DELETE on 21F87859727E470E94917B690908A09D:SAP_CAPIRE_BOOKSHOP_GENRES(ID) failed, because 1 rows with corresponding foreign keys still exist in 21F87859727E470E94917B690908A09D:SAP_CAPIRE_BOOKSHOP_BOOKS(GENRE_ID), rc=1536"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: when using `cdsc` to generate the constraints
|
|
||||||
```
|
|
||||||
cdsc Q --beta foreignKeyConstraints -d hana -s hdi -o dbstuff db\schema.cds srv\admin-service.cds
|
|
||||||
```
|
|
||||||
there will be constraints for e.g. `sap_common_Currencies_texts`. This cannot be deplyoed, as the corresponding table is removed from `cds` via the tree-shaking mechanism. These constraints need to be removed manually before deployment.
|
|
||||||
|
|
||||||
## Todo
|
|
||||||
|
|
||||||
* Provide a way to control parameters `VALIDATED` and `ENFORCED` of the constraints (only interesting for HANA, as in SQLite changing constraints after table generation is not possible).
|
|
||||||
* Provide some tool to identify existing entries that violate constraints. We can generate a list of `SELECT` statements that find these entries, but how do we run it and show the results to the user?
|
|
||||||
* Can runtimes improve the error messages?
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using { sap.capire.bookshop as my } from '../db/schema';
|
using { sap.capire.bookshop as my } from '../db/schema';
|
||||||
service AdminService {
|
service AdminService @(requires_:'admin') {
|
||||||
entity Books as projection on my.Books;
|
entity Books as projection on my.Books;
|
||||||
entity Authors as projection on my.Authors;
|
entity Authors as projection on my.Authors;
|
||||||
entity Genres as projection on my.Genres;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
// sqlite: FK constraint checks need to be enabled by application at runtime
|
|
||||||
// for each db connection
|
|
||||||
const cds = require('@sap/cds')
|
|
||||||
module.exports = cds.service.impl (async function(){
|
|
||||||
|
|
||||||
const db = await cds.connect.to('db')
|
|
||||||
|
|
||||||
if (db.kind === 'sqlite')
|
|
||||||
{
|
|
||||||
db.before('BEGIN', function (req) {
|
|
||||||
//console.log('--- PRAGMA foreign_keys = ON ---')
|
|
||||||
return this.dbc.run('PRAGMA foreign_keys = ON;')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
10
bookshop/srv/cat-service.cds
Normal file
10
bookshop/srv/cat-service.cds
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using { sap.capire.bookshop as my } from '../db/schema';
|
||||||
|
service CatalogService @(path:'/browse') {
|
||||||
|
|
||||||
|
@readonly entity Books as SELECT from my.Books {*,
|
||||||
|
author.name as author
|
||||||
|
} excluding { createdBy, modifiedBy };
|
||||||
|
|
||||||
|
@requires_: 'authenticated-user'
|
||||||
|
action submitOrder (book : Books.ID, amount: Integer);
|
||||||
|
}
|
||||||
20
bookshop/srv/cat-service.js
Normal file
20
bookshop/srv/cat-service.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const cds = require('@sap/cds')
|
||||||
|
module.exports = async function (){
|
||||||
|
|
||||||
|
const db = await cds.connect.to('db') // connect to database service
|
||||||
|
const { Books } = db.entities // get reflected definitions
|
||||||
|
|
||||||
|
// Reduce stock of ordered books if available stock suffices
|
||||||
|
this.on ('submitOrder', async req => {
|
||||||
|
const {book,amount} = req.data
|
||||||
|
const n = await UPDATE (Books, book)
|
||||||
|
.with ({ stock: {'-=': amount }})
|
||||||
|
.where ({ stock: {'>=': amount }})
|
||||||
|
n > 0 || req.error (409,`${amount} exceeds stock for book #${book}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add some discount for overstocked books
|
||||||
|
this.after ('READ','Books', each => {
|
||||||
|
if (each.stock > 111) each.title += ` -- 11% discount!`
|
||||||
|
})
|
||||||
|
}
|
||||||
4
bookshop/test/genres.cds
Normal file
4
bookshop/test/genres.cds
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
using { sap.capire.bookshop as my } from '../db/schema';
|
||||||
|
service TestService {
|
||||||
|
entity Genres as projection on my.Genres;
|
||||||
|
}
|
||||||
38
bookshop/test/genres.http
Normal file
38
bookshop/test/genres.http
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#################################################
|
||||||
|
#
|
||||||
|
# Genres
|
||||||
|
#
|
||||||
|
|
||||||
|
GET http://localhost:4004/test/Genres?
|
||||||
|
###
|
||||||
|
|
||||||
|
GET http://localhost:4004/test/Genres?
|
||||||
|
&$filter=parent_ID eq null&$select=name
|
||||||
|
&$expand=children($select=name)
|
||||||
|
###
|
||||||
|
|
||||||
|
POST http://localhost:4004/test/Genres?
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{ "ID":100, "name":"Some Sample Genres...", "children":[
|
||||||
|
{ "ID":101, "name":"Cat", "children":[
|
||||||
|
{ "ID":102, "name":"Kitty", "children":[
|
||||||
|
{ "ID":103, "name":"Kitty Cat", "children":[
|
||||||
|
{ "ID":104, "name":"Aristocat" } ]},
|
||||||
|
{ "ID":105, "name":"Kitty Bat" } ]},
|
||||||
|
{ "ID":106, "name":"Catwoman", "children":[
|
||||||
|
{ "ID":107, "name":"Catalina" } ]} ]},
|
||||||
|
{ "ID":108, "name":"Catweazle" }
|
||||||
|
]}
|
||||||
|
###
|
||||||
|
|
||||||
|
GET http://localhost:4004/test/Genres(100)?
|
||||||
|
# &$expand=children
|
||||||
|
# &$expand=children($expand=children($expand=children($expand=children)))
|
||||||
|
###
|
||||||
|
|
||||||
|
DELETE http://localhost:4004/test/Genres(103)
|
||||||
|
###
|
||||||
|
|
||||||
|
DELETE http://localhost:4004/test/Genres(100)
|
||||||
|
###
|
||||||
@@ -1,68 +1,51 @@
|
|||||||
# running locally
|
|
||||||
@server = http://localhost:4004
|
@server = http://localhost:4004
|
||||||
|
@me = Authorization: Basic {{$processEnv USER}}:
|
||||||
# running on cf
|
|
||||||
#@server = https://bookshop-srv-....hana.ondemand.com
|
|
||||||
|
|
||||||
### get all books
|
|
||||||
GET {{server}}/admin/Books
|
|
||||||
|
|
||||||
### get all authors
|
|
||||||
GET {{server}}/admin/Authors
|
|
||||||
|
|
||||||
### get all genres
|
|
||||||
GET {{server}}/admin/Genres
|
|
||||||
|
|
||||||
|
|
||||||
### create book with wrong author ID -> should fail
|
### ------------------------------------------------------------------------
|
||||||
POST {{server}}/admin/Books
|
# Get service info
|
||||||
|
GET {{server}}/browse
|
||||||
|
{{me}}
|
||||||
|
|
||||||
|
|
||||||
|
### ------------------------------------------------------------------------
|
||||||
|
# Get $metadata document
|
||||||
|
GET {{server}}/browse/$metadata
|
||||||
|
{{me}}
|
||||||
|
|
||||||
|
|
||||||
|
### ------------------------------------------------------------------------
|
||||||
|
# Browse Books as any user
|
||||||
|
GET {{server}}/browse/Books?
|
||||||
|
# &$select=title,stock
|
||||||
|
# &$expand=currency
|
||||||
|
# &sap-language=de
|
||||||
|
{{me}}
|
||||||
|
|
||||||
|
|
||||||
|
### ------------------------------------------------------------------------
|
||||||
|
# Fetch Authors as admin
|
||||||
|
GET {{server}}/admin/Authors?
|
||||||
|
# &$select=name,dateOfBirth,placeOfBirth
|
||||||
|
# &$expand=books($select=title;$expand=currency)
|
||||||
|
# &$filter=ID eq 101
|
||||||
|
# &sap-language=de
|
||||||
|
Authorization: Basic alice:
|
||||||
|
|
||||||
|
|
||||||
|
### ------------------------------------------------------------------------
|
||||||
|
# Submit Order as authenticated user
|
||||||
|
# (send that three times to get out-of-stock message)
|
||||||
|
POST {{server}}/browse/submitOrder
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
{{me}}
|
||||||
|
|
||||||
{
|
{ "book":201, "amount":5 }
|
||||||
"ID": 701,
|
|
||||||
"title": "XXX",
|
|
||||||
"author": { "ID": 9999 }
|
|
||||||
}
|
|
||||||
|
|
||||||
### create book with correct author ID -> should work
|
|
||||||
POST {{server}}/admin/Books
|
|
||||||
Content-Type: application/json;IEEE754Compatible=true
|
|
||||||
|
|
||||||
{
|
|
||||||
"ID": 702,
|
|
||||||
"title": "Poems : Pocket Poets",
|
|
||||||
"author": { "ID": 101 }
|
|
||||||
}
|
|
||||||
|
|
||||||
### delete book
|
|
||||||
DELETE {{server}}/admin/Books(ID=701)
|
|
||||||
|
|
||||||
### change book to wrong author ID -> should fail
|
|
||||||
|
|
||||||
PATCH {{server}}/admin/Books(ID=207)
|
|
||||||
Content-Type:application/json
|
|
||||||
|
|
||||||
{"author_ID": 9999}
|
|
||||||
|
|
||||||
### change book to correct author ID -> should work
|
|
||||||
|
|
||||||
PATCH {{server}}/admin/Books(ID=207)
|
|
||||||
Content-Type:application/json
|
|
||||||
|
|
||||||
{"author_ID": 170}
|
|
||||||
|
|
||||||
|
|
||||||
|
### ------------------------------------------------------------------------
|
||||||
### delete "leaf" genre that is used in a book -> should not work
|
# Browse Genres
|
||||||
DELETE {{server}}/admin/Genres(ID=13)
|
GET {{server}}/browse/Genres?
|
||||||
|
# &$filter=parent_ID eq null&$select=name
|
||||||
### delete "leaf" genre that is not used in a book -> should work
|
# &$expand=children($select=name)
|
||||||
DELETE {{server}}/admin/Genres(ID=14)
|
{{me}}
|
||||||
|
|
||||||
|
|
||||||
### delete "header" genre where leaves are used in a book -> should not work
|
|
||||||
DELETE {{server}}/admin/Genres(ID=10)
|
|
||||||
|
|
||||||
|
|
||||||
### delete "header" genre where leaves are not used in a book -> should work and delete all leaves
|
|
||||||
DELETE {{server}}/admin/Genres(ID=30)
|
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "@capire/common",
|
"name": "@capire/common",
|
||||||
"description": "Provides a pre-built extension package for std @sap/cds/common",
|
"version": "1.0.0"
|
||||||
"version": "1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@sap/cds": "*"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
# cds.requires.messaging.kind = file-based-messaging
|
cds.features.snapi = y
|
||||||
PORT = 4004
|
|
||||||
@@ -11,7 +11,6 @@ DateOfBirth = Date of Birth
|
|||||||
DateOfDeath = Date of Death
|
DateOfDeath = Date of Death
|
||||||
PlaceOfBirth = Place of Birth
|
PlaceOfBirth = Place of Birth
|
||||||
PlaceOfDeath = Place of Death
|
PlaceOfDeath = Place of Death
|
||||||
Age = Age
|
|
||||||
Authors = Authors
|
Authors = Authors
|
||||||
Order = Order
|
Order = Order
|
||||||
Orders = Orders
|
Orders = Orders
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ Authors = Autoren
|
|||||||
Author = Autor
|
Author = Autor
|
||||||
AuthorID = ID des Autors
|
AuthorID = ID des Autors
|
||||||
AuthorName = Name des Autors
|
AuthorName = Name des Autors
|
||||||
Age = Alter
|
|
||||||
Name = Name
|
Name = Name
|
||||||
Stock = Bestand
|
Stock = Bestand
|
||||||
Order = Bestellung
|
Order = Bestellung
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using { AdminService } from '../../db/schema';
|
using AdminService from '@capire/bookshop';
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
@@ -39,27 +39,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}'},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
<head>
|
|
||||||
<meta http-equiv="refresh" content="0;url=bookshop/index.html">
|
|
||||||
</head>
|
|
||||||
@@ -7,8 +7,6 @@ using CatalogService from '@capire/bookshop';
|
|||||||
annotate CatalogService.Books with @(
|
annotate CatalogService.Books with @(
|
||||||
UI: {
|
UI: {
|
||||||
HeaderInfo: {
|
HeaderInfo: {
|
||||||
TypeName: 'Book',
|
|
||||||
TypeNamePlural: 'Books',
|
|
||||||
Description: {Value: author}
|
Description: {Value: author}
|
||||||
},
|
},
|
||||||
HeaderFacets: [
|
HeaderFacets: [
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
applications: {
|
applications: {
|
||||||
"browse-books": {
|
"browse-books": {
|
||||||
title: "Browse Books",
|
title: "Browse Books",
|
||||||
description: "w/ SAP Fiori Elements",
|
description: "... testing FE v42",
|
||||||
additionalInformation: "SAPUI5.Component=bookshop",
|
additionalInformation: "SAPUI5.Component=bookshop",
|
||||||
applicationType : "URL",
|
applicationType : "URL",
|
||||||
url: "/browse/webapp",
|
url: "/browse/webapp",
|
||||||
@@ -21,15 +21,15 @@
|
|||||||
},
|
},
|
||||||
"manage-books": {
|
"manage-books": {
|
||||||
title: "Manage Books",
|
title: "Manage Books",
|
||||||
description: "w/ SAP Fiori Elements",
|
description: "... testing FE v42",
|
||||||
additionalInformation: "SAPUI5.Component=admin",
|
additionalInformation: "SAPUI5.Component=admin",
|
||||||
applicationType : "URL",
|
applicationType : "URL",
|
||||||
url: "/admin/webapp",
|
url: "/admin/webapp",
|
||||||
navigationMode: "embedded"
|
navigationMode: "embedded"
|
||||||
},
|
},
|
||||||
"manage-orders": {
|
"manage-orders": {
|
||||||
title: "Manage Orders",
|
title: "Order Books",
|
||||||
description: "w/ SAP Fiori Elements",
|
description: "... testing FE v42",
|
||||||
additionalInformation: "SAPUI5.Component=orders",
|
additionalInformation: "SAPUI5.Component=orders",
|
||||||
applicationType : "URL",
|
applicationType : "URL",
|
||||||
url: "/orders/webapp",
|
url: "/orders/webapp",
|
||||||
@@ -39,8 +39,8 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id="sap-ushell-bootstrap" src="https://sapui5.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js"></script>
|
<script 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"
|
<script src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
|
||||||
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout"
|
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout"
|
||||||
data-sap-ui-compatVersion="edge"
|
data-sap-ui-compatVersion="edge"
|
||||||
data-sap-ui-theme="sap_fiori_3"
|
data-sap-ui-theme="sap_fiori_3"
|
||||||
@@ -4,9 +4,7 @@
|
|||||||
|
|
||||||
using from './admin/fiori-service';
|
using from './admin/fiori-service';
|
||||||
using from './browse/fiori-service';
|
using from './browse/fiori-service';
|
||||||
|
using from './orders/fiori-service';
|
||||||
using from './common';
|
using from './common';
|
||||||
|
|
||||||
using from '@capire/common';
|
using from '@capire/common';
|
||||||
|
|
||||||
// only works in case of embedded orders service
|
|
||||||
using from '@capire/orders/app/orders/fiori-service';
|
|
||||||
@@ -1,27 +1,44 @@
|
|||||||
|
using OrdersService from '@capire/orders/srv/orders-service';
|
||||||
|
|
||||||
|
annotate OrdersService.Books with {
|
||||||
|
price @Common.FieldControl: #ReadOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Note: this is designed for the OrdersService being co-located with
|
// Common
|
||||||
// bookshop. It does not work if OrdersService is run as a separate
|
|
||||||
// process, and is not intended to do so.
|
|
||||||
//
|
//
|
||||||
////////////////////////////////////////////////////////////////////////////
|
annotate OrdersService.OrderItems with {
|
||||||
|
book @(
|
||||||
|
Common: {
|
||||||
|
Text: book.title,
|
||||||
using { OrdersService } from '../../srv/orders-service';
|
FieldControl: #Mandatory
|
||||||
|
},
|
||||||
|
ValueList.entity:'Books',
|
||||||
|
);
|
||||||
|
amount @(
|
||||||
|
Common.FieldControl: #Mandatory
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@odata.draft.enabled
|
@odata.draft.enabled
|
||||||
annotate OrdersService.Orders with @(
|
annotate OrdersService.Orders with @(
|
||||||
UI: {
|
UI: {
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Lists of Orders
|
||||||
|
//
|
||||||
SelectionFields: [ createdAt, createdBy ],
|
SelectionFields: [ createdAt, createdBy ],
|
||||||
LineItem: [
|
LineItem: [
|
||||||
{Value: OrderNo, Label:'OrderNo'},
|
{Value: createdBy, Label:'Customer'},
|
||||||
{Value: buyer, Label:'Customer'},
|
|
||||||
{Value: createdAt, Label:'Date'}
|
{Value: createdAt, Label:'Date'}
|
||||||
],
|
],
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Order Details
|
||||||
|
//
|
||||||
HeaderInfo: {
|
HeaderInfo: {
|
||||||
TypeName: 'Order', TypeNamePlural: 'Orders',
|
TypeName: 'Order', TypeNamePlural: 'Orders',
|
||||||
Title: {
|
Title: {
|
||||||
@@ -45,7 +62,7 @@ annotate OrdersService.Orders with @(
|
|||||||
],
|
],
|
||||||
FieldGroup#Details: {
|
FieldGroup#Details: {
|
||||||
Data: [
|
Data: [
|
||||||
{Value: currency.code, Label:'Currency'}
|
{Value: currency_code, Label:'Currency'}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
FieldGroup#Created: {
|
FieldGroup#Created: {
|
||||||
@@ -68,25 +85,36 @@ annotate OrdersService.Orders with @(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
annotate OrdersService.Orders_Items with @(
|
//The enity types name is OrdersService.my_bookshop_OrderItems
|
||||||
|
//The annotations below are not generated in edmx WHY?
|
||||||
|
annotate OrdersService.OrderItems with @(
|
||||||
UI: {
|
UI: {
|
||||||
|
HeaderInfo: {
|
||||||
|
TypeName: 'Order Item', TypeNamePlural: ' ',
|
||||||
|
Title: {
|
||||||
|
Value: book.title
|
||||||
|
},
|
||||||
|
Description: {Value: book.descr}
|
||||||
|
},
|
||||||
|
// There is no filterbar for items so the selctionfileds is not needed
|
||||||
|
SelectionFields: [ book_ID ],
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Lists of OrderItems
|
||||||
|
//
|
||||||
LineItem: [
|
LineItem: [
|
||||||
{Value: product_ID, Label:'Product ID'},
|
{Value: book_ID, Label:'Book'},
|
||||||
{Value: title, Label:'Product Title'},
|
//The following entry is only used to have the assoication followed in the read event
|
||||||
{Value: price, Label:'Unit Price'},
|
{Value: book.price, Label:'Book Price'},
|
||||||
{Value: amount, Label:'Quantity'},
|
{Value: amount, Label:'Quantity'},
|
||||||
],
|
],
|
||||||
Identification: [ //Is the main field group
|
Identification: [ //Is the main field group
|
||||||
|
//{Value: ID, Label:'ID'}, //A guid shouldn't be on the UI
|
||||||
|
{Value: book_ID, Label:'Book'},
|
||||||
{Value: amount, Label:'Amount'},
|
{Value: amount, Label:'Amount'},
|
||||||
{Value: title, Label:'Product'},
|
|
||||||
{Value: price, Label:'Unit Price'},
|
|
||||||
],
|
],
|
||||||
Facets: [
|
Facets: [
|
||||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>OrderItems}', Target: '@UI.Identification'},
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>OrderItems}', Target: '@UI.Identification'},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
) {
|
|
||||||
amount @(
|
|
||||||
Common.FieldControl: #Mandatory
|
|
||||||
);
|
);
|
||||||
};
|
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
"name": "sap.fe.templates.ObjectPage",
|
"name": "sap.fe.templates.ObjectPage",
|
||||||
"options": {
|
"options": {
|
||||||
"settings" : {
|
"settings" : {
|
||||||
"entitySet": "Orders_Items"
|
"entitySet": "OrderItems"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<head>
|
|
||||||
<meta http-equiv="refresh" content="0;url=reviews/index.html">
|
|
||||||
</head>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
//
|
|
||||||
// Add Author.age and .lifetime with a DB-specific function
|
|
||||||
//
|
|
||||||
|
|
||||||
using { AdminService } from '../schema';
|
|
||||||
|
|
||||||
extend projection AdminService.Authors with {
|
|
||||||
YEARS_BETWEEN(dateOfBirth, dateOfDeath) as age: Integer,
|
|
||||||
YEAR(dateOfBirth) || ' – ' || YEAR(dateOfDeath) as lifetime : String
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,3 @@
|
|||||||
using { sap.capire.bookshop } from '@capire/bookshop';
|
// Proxy for importing schema from bookshop sample
|
||||||
|
using from '@capire/bookshop';
|
||||||
// Forward-declare calculated fields to be filled in database-specific ways
|
namespace sap.capire.bookshop;
|
||||||
// TODO find a better way to have 'default' fields that still can be overwritten.
|
|
||||||
extend bookshop.Authors with {
|
|
||||||
virtual age: Integer;
|
|
||||||
virtual lifetime: String;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
//
|
|
||||||
// Add Author.age and .lifetime with a DB-specific function
|
|
||||||
//
|
|
||||||
|
|
||||||
using { AdminService } from '../schema';
|
|
||||||
|
|
||||||
extend projection AdminService.Authors with {
|
|
||||||
strftime('%Y',dateOfDeath)-strftime('%Y',dateOfBirth) as age: Integer,
|
|
||||||
strftime('%Y',dateOfBirth) || ' – ' || strftime('%Y',dateOfDeath) as lifetime : String
|
|
||||||
}
|
|
||||||
@@ -2,42 +2,20 @@
|
|||||||
"name": "@capire/fiori",
|
"name": "@capire/fiori",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capire/bookshop": "*",
|
"@capire/bookshop": "../bookshop",
|
||||||
"@capire/reviews": "*",
|
"@capire/orders": "../orders",
|
||||||
"@capire/orders": "*",
|
"@capire/common": "../common",
|
||||||
"@capire/common": "*",
|
"@sap/cds": ">=3.33.1",
|
||||||
"@sap/cds": ">=4",
|
"express": "^4.17.1"
|
||||||
"express": "^4.17.1",
|
|
||||||
"passport": "^0.4.1"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cds run --in-memory?",
|
"start": "cds run --in-memory?",
|
||||||
"watch": "cds watch"
|
"watch": "cds watch"
|
||||||
},
|
},
|
||||||
"cds": {
|
"cds": {
|
||||||
"hana": {
|
|
||||||
"deploy-format": "hdbtable"
|
|
||||||
},
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"auth": {
|
|
||||||
"strategy": "dummy"
|
|
||||||
},
|
|
||||||
"ReviewsService": {
|
|
||||||
"kind": "odata",
|
|
||||||
"model": "@capire/reviews"
|
|
||||||
},
|
|
||||||
"OrdersService": {
|
|
||||||
"kind": "odata",
|
|
||||||
"model": "@capire/orders"
|
|
||||||
},
|
|
||||||
"db": {
|
"db": {
|
||||||
"kind": "sql",
|
"kind": "sql"
|
||||||
"[development]": {
|
|
||||||
"model": "db/sqlite"
|
|
||||||
},
|
|
||||||
"[production]": {
|
|
||||||
"model": "db/hana"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
const cds = require ('@sap/cds')
|
|
||||||
|
|
||||||
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'))
|
|
||||||
|
|
||||||
module.exports = cds.server
|
|
||||||
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// 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)))
|
|
||||||
3
fiori/srv/admin-service.cds
Normal file
3
fiori/srv/admin-service.cds
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Proxy for importing services from bookshop sample
|
||||||
|
using from '@capire/bookshop';
|
||||||
|
annotate AdminService with @impl:'srv/admin-service.js';
|
||||||
8
fiori/srv/admin-service.js
Normal file
8
fiori/srv/admin-service.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
const cds = require('@sap/cds')
|
||||||
|
|
||||||
|
module.exports = cds.service.impl (async function() {
|
||||||
|
const {Books} = cds.entities
|
||||||
|
const {ID} = await SELECT.one.from(Books).columns('max(ID) as ID')
|
||||||
|
let newID = ID - ID % 100 + 100
|
||||||
|
this.before ('NEW','Books', req => req.data.ID = ++newID)
|
||||||
|
})
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Mashing up imported models...
|
|
||||||
//
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Mashing up provided and required services...
|
|
||||||
//
|
|
||||||
module.exports = async()=>{ // called by server.js
|
|
||||||
|
|
||||||
const cds = require('@sap/cds')
|
|
||||||
const CatalogService = await cds.connect.to ('CatalogService')
|
|
||||||
const ReviewsService = await cds.connect.to ('ReviewsService')
|
|
||||||
const OrdersService = await cds.connect.to ('OrdersService')
|
|
||||||
const db = await cds.connect.to ('db')
|
|
||||||
|
|
||||||
// reflect entity definitions used below...
|
|
||||||
const { Books } = db.entities ('sap.capire.bookshop')
|
|
||||||
|
|
||||||
//
|
|
||||||
// Delegate requests to read reviews to the ReviewsService
|
|
||||||
// Note: prepend is neccessary to intercept generic default handler
|
|
||||||
//
|
|
||||||
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
|
|
||||||
console.debug ('> delegating request to ReviewsService')
|
|
||||||
const [id] = req.params, { columns, limit } = req.query.SELECT
|
|
||||||
return ReviewsService.tx(req).read ('Reviews',columns).limit(limit).where({subject:String(id)})
|
|
||||||
}))
|
|
||||||
|
|
||||||
//
|
|
||||||
// Create an order with the OrdersService when CatalogService signals a new order
|
|
||||||
//
|
|
||||||
CatalogService.on ('OrderedBook', async (msg) => {
|
|
||||||
const { book, amount, buyer } = msg.data
|
|
||||||
const { title, price } = await db.tx(msg).read (Books, book, b => { b.title, b.price })
|
|
||||||
return OrdersService.tx(msg).create ('Orders').entries({
|
|
||||||
OrderNo: 'Order at '+ (new Date).toLocaleString(),
|
|
||||||
Items: [{ product:{ID:`${book}`}, title, price, amount }],
|
|
||||||
buyer, createdBy: buyer
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
//
|
|
||||||
// Update Books' average ratings when ReviewsService signals updatd reviews
|
|
||||||
//
|
|
||||||
ReviewsService.on ('reviewed', (msg) => {
|
|
||||||
console.debug ('> received:', msg.event, msg.data)
|
|
||||||
const { subject, rating } = msg.data
|
|
||||||
return UPDATE(Books,subject).with({rating})
|
|
||||||
// ^ Note: the framework will execute this and take care for db.tx
|
|
||||||
})
|
|
||||||
|
|
||||||
//
|
|
||||||
// Reduce stock of ordered books for orders are created from Orders admin UI
|
|
||||||
//
|
|
||||||
OrdersService.on ('OrderChanged', (msg) => {
|
|
||||||
console.debug ('> received:', msg.event, msg.data)
|
|
||||||
const { product, deltaAmount } = msg.data
|
|
||||||
return UPDATE (Books) .where ('ID =', product)
|
|
||||||
.and ('stock >=', deltaAmount)
|
|
||||||
.set ('stock -=', deltaAmount)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
|
|
||||||
@bookshop = http://localhost:4004
|
|
||||||
@reviews-service = {{bookshop}}/reviews
|
|
||||||
# Uncomment this when running a separate reviews service
|
|
||||||
@reviews-service = http://localhost:4005/reviews
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#################################################
|
|
||||||
#
|
|
||||||
# Reviews Service
|
|
||||||
#
|
|
||||||
|
|
||||||
GET {{reviews-service}}/Reviews
|
|
||||||
|
|
||||||
###
|
|
||||||
|
|
||||||
POST {{reviews-service}}/Reviews
|
|
||||||
Authorization: Basic {{$processEnv USER}}:
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{"subject":"201", "title":"boo", "rating":3 }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#################################################
|
|
||||||
#
|
|
||||||
# Bookshop Services
|
|
||||||
#
|
|
||||||
|
|
||||||
GET {{bookshop}}/browse/Books/201/reviews?
|
|
||||||
&$select=rating,date,title
|
|
||||||
&$top=3
|
|
||||||
|
|
||||||
###
|
|
||||||
|
|
||||||
GET {{bookshop}}/browse/Books(201)?
|
|
||||||
&$select=ID,title,rating
|
|
||||||
&$expand=reviews
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#################################################
|
|
||||||
#
|
|
||||||
# Orders Service, incl. draft choreography
|
|
||||||
#
|
|
||||||
@newOrderID = e939604c-ab83-4d4f-bdb6-95fe30b3773e
|
|
||||||
|
|
||||||
GET {{bookshop}}/orders/Orders
|
|
||||||
|
|
||||||
### Create order, still inactive
|
|
||||||
POST {{bookshop}}/orders/Orders
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{"ID": "{{newOrderID}}"}
|
|
||||||
|
|
||||||
### Get inactive order. We have to specify `IsActiveEntity`.
|
|
||||||
GET {{bookshop}}/orders/Orders(ID={{newOrderID}},IsActiveEntity=false)
|
|
||||||
|
|
||||||
### Activate order using `.../<servicename>.draftActivate`
|
|
||||||
POST {{bookshop}}/orders/Orders(ID={{newOrderID}},IsActiveEntity=false)/OrdersService.draftActivate
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
### Get active order
|
|
||||||
GET {{bookshop}}/orders/Orders(ID={{newOrderID}},IsActiveEntity=true)
|
|
||||||
|
|
||||||
### Create author
|
|
||||||
POST {{bookshop}}/admin/Authors
|
|
||||||
Content-Type: application/json
|
|
||||||
Authorization: Basic alice:
|
|
||||||
|
|
||||||
{
|
|
||||||
"ID": 200,
|
|
||||||
"name": "William Shakespeare",
|
|
||||||
"dateOfBirth": "1564-04-26",
|
|
||||||
"dateOfDeath": "1616-04-23"
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 KiB |
@@ -1,35 +0,0 @@
|
|||||||
### Requires REST Client for VS Code
|
|
||||||
### https://marketplace.visualstudio.com/items?itemName=humao.rest-client
|
|
||||||
###
|
|
||||||
@protocol = http
|
|
||||||
@host = localhost
|
|
||||||
@port = 4004
|
|
||||||
### Read Pictures
|
|
||||||
GET {{protocol}}://{{host}}:{{port}}/media-server/Media
|
|
||||||
Authorization: Basic admin:
|
|
||||||
|
|
||||||
### Create Picture with mediatype
|
|
||||||
POST {{protocol}}://{{host}}:{{port}}/media-server/Media
|
|
||||||
Authorization: Basic admin:
|
|
||||||
Accept: application/json
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"mediaType": "image/png"
|
|
||||||
}
|
|
||||||
|
|
||||||
### Upload Binary PNG
|
|
||||||
PUT {{protocol}}://{{host}}:{{port}}/media-server/Media(1)/content
|
|
||||||
Authorization: Basic admin:
|
|
||||||
Content-Type: image/png
|
|
||||||
|
|
||||||
< ./Test.png
|
|
||||||
|
|
||||||
### Read Binary
|
|
||||||
GET {{protocol}}://{{host}}:{{port}}/media-server/Media(1)/content
|
|
||||||
Authorization: Basic admin:
|
|
||||||
|
|
||||||
### Delete Image
|
|
||||||
DELETE {{protocol}}://{{host}}:{{port}}/media-server/Media(1)
|
|
||||||
Authorization: Basic admin:
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
cds.requires.messaging.kind = file-based-messaging
|
|
||||||
PORT = 4006
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<!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: {
|
|
||||||
"manage-orders": {
|
|
||||||
title: "Manage Orders",
|
|
||||||
description: "... testing FE v42",
|
|
||||||
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>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
/*
|
|
||||||
This model controls what gets served to Fiori frontends...
|
|
||||||
*/
|
|
||||||
|
|
||||||
using from './orders/fiori-service';
|
|
||||||
4
orders/db/data/sap.capire.bookshop-OrderItems.csv
Normal file
4
orders/db/data/sap.capire.bookshop-OrderItems.csv
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
ID;amount;parent_ID;book_ID;netAmount
|
||||||
|
58040e66-1dcd-4ffb-ab10-fdce32028b79;1;7e2f2640-6866-4dcf-8f4d-3027aa831cad;201;11.11
|
||||||
|
64e718c9-ff99-47f1-8ca3-950c850777d4;1;7e2f2640-6866-4dcf-8f4d-3027aa831cad;271;15
|
||||||
|
e9641166-e050-4261-bfee-d1e797e6cb7f;2;64e718c9-ff99-47f1-8ca3-950c850777d4;252;28
|
||||||
|
3
orders/db/data/sap.capire.bookshop-Orders.csv
Normal file
3
orders/db/data/sap.capire.bookshop-Orders.csv
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ID;modifiedAt;createdAt;createdBy;modifiedBy;OrderNo;currency_code
|
||||||
|
7e2f2640-6866-4dcf-8f4d-3027aa831cad;;2019-01-31;john.doe@test.com;;1;EUR
|
||||||
|
64e718c9-ff99-47f1-8ca3-950c850777d4;;2019-01-30;jane.doe@test.com;;2;EUR
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
ID;createdAt;createdBy;buyer;OrderNo;currency_code
|
|
||||||
7e2f2640-6866-4dcf-8f4d-3027aa831cad;2019-01-31;john.doe@test.com;john.doe@test.com;1;EUR
|
|
||||||
64e718c9-ff99-47f1-8ca3-950c850777d4;2019-01-30;jane.doe@test.com;jane.doe@test.com;2;EUR
|
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
ID;up__ID;amount;product_ID;title;price
|
|
||||||
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
|
|
||||||
e9641166-e050-4261-bfee-d1e797e6cb7f;64e718c9-ff99-47f1-8ca3-950c850777d4;2;252;Eleonora;28
|
|
||||||
|
@@ -1,23 +1,16 @@
|
|||||||
using { Currency, User, managed, cuid } from '@sap/cds/common';
|
using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
||||||
namespace sap.capire.orders;
|
using { Currency, managed, cuid } from '@sap/cds/common';
|
||||||
|
namespace sap.capire.bookshop;
|
||||||
|
|
||||||
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 OrderItems on Items.parent = $self;
|
||||||
buyer : User;
|
|
||||||
currency : Currency;
|
currency : Currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
entity Orders_Items {
|
entity OrderItems : cuid {
|
||||||
key ID : UUID;
|
parent : Association to Orders;
|
||||||
up_ : Association to Orders;
|
book : Association to Books;
|
||||||
product : Association to Products @assert.integrity:false; // REVISIT: this is a temporary workaround for a glitch in cds-runtime
|
|
||||||
amount : Integer;
|
amount : Integer;
|
||||||
title : String; //> intentionally replicated as snapshot from product.title
|
netAmount : Decimal(9,2);
|
||||||
price : Double;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** This is a stand-in for arbitrary ordered Products */
|
|
||||||
entity Products @(cds.persistence.skip:'always') {
|
|
||||||
key ID : String;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
/*
|
|
||||||
This model controls what gets exposed
|
|
||||||
*/
|
|
||||||
namespace sap.capire.orders;
|
|
||||||
using from './srv/orders-service';
|
|
||||||
using from './db/schema';
|
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "@capire/orders",
|
"name": "@capire/orders",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0"
|
||||||
"dependencies": {
|
|
||||||
"@sap/cds": ">=4.3.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using { sap.capire.orders as my } from '../db/schema';
|
using { sap.capire.bookshop as my } from '../db/schema';
|
||||||
|
|
||||||
service OrdersService {
|
service OrdersService {
|
||||||
entity Orders as projection on my.Orders;
|
entity Orders as projection on my.Orders;
|
||||||
|
entity Books as projection on my.Books;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,21 @@
|
|||||||
const cds = require('@sap/cds')
|
const cds = require('@sap/cds')
|
||||||
class OrdersService extends cds.ApplicationService {
|
|
||||||
|
|
||||||
/** register custom handlers */
|
module.exports = cds.service.impl(function() {
|
||||||
init(){
|
|
||||||
const { Orders_Items:OrderItems } = this.entities
|
|
||||||
|
|
||||||
this.before ('UPDATE', 'Orders', async function(req) {
|
const { Books } = cds.entities
|
||||||
const { ID, Items } = req.data
|
|
||||||
if (Items) for (let { product_ID, amount } of Items) {
|
// Reduce stock of ordered books if available stock suffices
|
||||||
const { amount:before } = await cds.tx(req).run (
|
this.before ('CREATE', 'Orders', (req) => {
|
||||||
SELECT.one.from (OrderItems, oi => oi.amount) .where ({up__ID:ID, product_ID})
|
const { Items: OrderItems } = req.data
|
||||||
|
return cds.transaction(req) .run (()=> OrderItems.map (order =>
|
||||||
|
UPDATE (Books) .where ('ID =', order.book_ID)
|
||||||
|
.and ('stock >=', order.amount)
|
||||||
|
.set ('stock -=', order.amount)
|
||||||
|
)) .then (all => all.forEach ((affectedRows,i) => {
|
||||||
|
if (affectedRows === 0) req.error (409,
|
||||||
|
`${OrderItems[i].amount} exceeds stock for book #${OrderItems[i].book_ID}`
|
||||||
)
|
)
|
||||||
if (amount != before) await this.orderChanged (product_ID, amount-before)
|
}))
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.before ('DELETE', 'Orders', async function(req) {
|
|
||||||
const { ID } = req.data
|
|
||||||
const Items = await cds.tx(req).run (
|
|
||||||
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.amount)))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return super.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** order changed -> broadcast event */
|
|
||||||
orderChanged (product, deltaAmount) {
|
|
||||||
// Emit events to inform subscribers about changes in orders
|
|
||||||
console.log ('> emitting:', 'OrderChanged', { product, deltaAmount })
|
|
||||||
return this.emit ('OrderChanged', { product, deltaAmount })
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
module.exports = OrdersService
|
|
||||||
|
|||||||
25
package.json
25
package.json
@@ -5,32 +5,25 @@
|
|||||||
"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/bookshop": "./bookshop",
|
"@capire/bookshop": "file:bookshop",
|
||||||
"@capire/common": "./common",
|
"@capire/common": "file:common",
|
||||||
"@capire/fiori": "./fiori",
|
"@capire/fiori": "file:fiori",
|
||||||
"@capire/hello": "./hello",
|
"@capire/orders": "file:orders",
|
||||||
"@capire/media": "./media",
|
"@capire/reviews": "file:reviews",
|
||||||
"@capire/orders": "./orders",
|
"@capire/tests": "file:test"
|
||||||
"@capire/reviews": "./reviews"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^4.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.0.0"
|
"sqlite3": "^4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"cleanup": "rm -rf node_modules && rm -rf */node_modules && rm -rf */*/node_modules",
|
|
||||||
"registry": "node .registry/server.js",
|
|
||||||
"bookshop": "cds watch bookshop",
|
"bookshop": "cds watch bookshop",
|
||||||
"fiori": "cds watch fiori",
|
"fiori": "cds watch fiori",
|
||||||
"media": "cds watch media",
|
|
||||||
"mocha": "npx mocha || echo",
|
"mocha": "npx mocha || echo",
|
||||||
"jest": "npx jest",
|
"jest": "npx jest --verbose",
|
||||||
"test": "npm run jest --silent"
|
"test": "npm run jest -s"
|
||||||
},
|
|
||||||
"mocha": {
|
|
||||||
"parallel": true
|
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"testEnvironment": "node"
|
"testEnvironment": "node"
|
||||||
|
|||||||
1
reviewed/.env
Normal file
1
reviewed/.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
cds.requires.messaging.kind = file-based-messaging
|
||||||
16
reviewed/db/schema.cds
Normal file
16
reviewed/db/schema.cds
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// Extending Books with Reviews
|
||||||
|
//
|
||||||
|
|
||||||
|
using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
||||||
|
using { ReviewsService.Reviews } from '@capire/reviews';
|
||||||
|
|
||||||
|
extend Books with {
|
||||||
|
/** Access to detailed collection of Reviews */
|
||||||
|
reviews : Composition of many Reviews on reviews.subject = $self.ID;
|
||||||
|
/** Average rating */
|
||||||
|
rating : Reviews.rating;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporary workaround for cap/issues#4112:
|
||||||
|
annotate Reviews with @cds.autoexpose;
|
||||||
20
reviewed/package.json
Normal file
20
reviewed/package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "@capire/bookshop-with-reviews",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@capire/bookshop": "../bookshop",
|
||||||
|
"@capire/reviews": "../reviews",
|
||||||
|
"@sap/cds": "^3.33.1",
|
||||||
|
"express": "^4.17.1"
|
||||||
|
},
|
||||||
|
"cds": {
|
||||||
|
"requires": {
|
||||||
|
"db": {
|
||||||
|
"kind": "sql"
|
||||||
|
},
|
||||||
|
"ReviewsService": {
|
||||||
|
"kind": "odata", "model": "@capire/reviews"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
reviewed/server.js
Normal file
55
reviewed/server.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// This is an example of using a project-local server.js to intercept
|
||||||
|
// the default bootstrapping process.
|
||||||
|
//
|
||||||
|
|
||||||
|
const cds = require ('@sap/cds')
|
||||||
|
|
||||||
|
// Mashup services after all are served...
|
||||||
|
cds.once('served', async()=>{
|
||||||
|
|
||||||
|
// react on event messages from reviews service
|
||||||
|
const ReviewsService = await cds.connect.to ('ReviewsService')
|
||||||
|
const db = await cds.connect.to ('db')
|
||||||
|
|
||||||
|
// reflect entities required below...
|
||||||
|
const { Books } = db.entities('sap.capire.bookshop')
|
||||||
|
const { Reviews } = ReviewsService.entities
|
||||||
|
|
||||||
|
ReviewsService.on ('reviewed', (msg) => {
|
||||||
|
console.debug ('> received:', msg.event, msg.data)
|
||||||
|
const { subject, rating } = msg.data
|
||||||
|
const tx = db.tx (msg) // TODO: db.tx(msg) fully implemented?
|
||||||
|
return tx.update (Books,subject) .with ({rating})
|
||||||
|
})
|
||||||
|
|
||||||
|
// delegate requests to read reviews to ReviewsService
|
||||||
|
const CatalogService = await cds.connect.to ('CatalogService')
|
||||||
|
CatalogService.impl (srv => srv.on ('READ', 'Books/reviews', (req) => {
|
||||||
|
console.debug ('> delegating to ReviewsService')
|
||||||
|
const [ id ] = req.params
|
||||||
|
const tx = ReviewsService.tx(req)
|
||||||
|
return tx.read (Reviews) .where ({ subject: String(id) })
|
||||||
|
.columns (req.query.SELECT.columns)
|
||||||
|
}))
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// Other bootstrapping events you could hook in to...
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
cds.on('bootstrap',(app) => {/* ... */})
|
||||||
|
cds.on('loaded', (model) => {/* ... */})
|
||||||
|
cds.on('connect', (srv) => {/* ... */})
|
||||||
|
cds.on('serving', (srv) => {/* ... */})
|
||||||
|
cds.once('listening', ({server,url}) => {/* ... */})
|
||||||
|
|
||||||
|
|
||||||
|
// Delegate bootstrapping to built-in server.js
|
||||||
|
module.exports = cds.server
|
||||||
|
|
||||||
|
// Monkey-patching older releases
|
||||||
|
if (cds.version < '3.33.4') cds.once('listening', ()=> cds.emit('served'))
|
||||||
|
|
||||||
|
// Launch server if started directly from command-line
|
||||||
|
if (!module.parent) cds.server()
|
||||||
40
reviewed/test/requests.http
Normal file
40
reviewed/test/requests.http
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#################################################
|
||||||
|
#
|
||||||
|
# To ReviewsService
|
||||||
|
#
|
||||||
|
# move the right down:
|
||||||
|
@reviews-service = http://localhost:4004/reviews
|
||||||
|
@reviews-service = http://localhost:5005/reviews
|
||||||
|
|
||||||
|
### Get all reviews
|
||||||
|
GET {{reviews-service}}/Reviews
|
||||||
|
|
||||||
|
### Add a new review (with random rating)
|
||||||
|
POST {{reviews-service}}/Reviews
|
||||||
|
Content-Type: application/json;IEEE754Compatible=true
|
||||||
|
|
||||||
|
{"subject":"201", "title":"boo"}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#################################################
|
||||||
|
#
|
||||||
|
# Bookshop Requests involving reviews
|
||||||
|
# (both in-process as well as separate one)
|
||||||
|
#
|
||||||
|
|
||||||
|
@bookshop = http://localhost:4004
|
||||||
|
|
||||||
|
### Request to CatalogService > delegated to ReviewsService
|
||||||
|
GET {{bookshop}}/browse/Books(201)/reviews?
|
||||||
|
&$select=rating,date,reviewer,title
|
||||||
|
|
||||||
|
### Alternative OData URL
|
||||||
|
GET {{bookshop}}/browse/Books/201/reviews?
|
||||||
|
&$select=rating,date,reviewer,title
|
||||||
|
|
||||||
|
###
|
||||||
|
GET {{bookshop}}/browse/Books(201)?
|
||||||
|
&$select=ID,title,rating
|
||||||
|
&$expand=reviews
|
||||||
|
# Note: the $expand only works in case of ReviewsService in same process
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
cds.requires.messaging.kind = file-based-messaging
|
cds.requires.messaging.kind = file-based-messaging
|
||||||
PORT = 4005
|
PORT = 5005
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
/* global Vue axios */ //> from vue.html
|
|
||||||
const $ = sel => document.querySelector(sel)
|
|
||||||
const GET = (url) => axios.get('/reviews'+url)
|
|
||||||
const PUT = (cmd,data) => axios.patch('/reviews'+cmd,data)
|
|
||||||
const POST = (cmd,data) => axios.post('/reviews'+cmd,data)
|
|
||||||
|
|
||||||
const reviews = new Vue ({
|
|
||||||
|
|
||||||
el:'#app',
|
|
||||||
|
|
||||||
data: {
|
|
||||||
list: [],
|
|
||||||
review: undefined,
|
|
||||||
message: {},
|
|
||||||
Ratings: Object.entries({
|
|
||||||
5 : '★★★★★',
|
|
||||||
4 : '★★★★',
|
|
||||||
3 : '★★★',
|
|
||||||
2 : '★★',
|
|
||||||
1 : '★',
|
|
||||||
}).reverse()
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
|
|
||||||
search: ({target:{value:v}}) => reviews.fetch(v && '&$search='+v),
|
|
||||||
|
|
||||||
async fetch (etc='') {
|
|
||||||
const {data} = await GET(`/Reviews?${etc}`)
|
|
||||||
reviews.list = data.value
|
|
||||||
},
|
|
||||||
|
|
||||||
async inspect (eve) {
|
|
||||||
const review = reviews.review = reviews.list [eve.currentTarget.rowIndex-1]
|
|
||||||
const res = await GET(`/Reviews/${review.ID}/text/$value`)
|
|
||||||
review.text = res.data
|
|
||||||
reviews.message = {}
|
|
||||||
},
|
|
||||||
|
|
||||||
newReview () {
|
|
||||||
reviews.review = {}
|
|
||||||
reviews.message = {}
|
|
||||||
setTimeout (()=> $('form > input').focus(), 111)
|
|
||||||
},
|
|
||||||
|
|
||||||
async submitReview () {
|
|
||||||
const review = reviews.review; review.rating = parseInt (review.rating) // REVISIT: Okra should be less strict
|
|
||||||
try {
|
|
||||||
if (!review.ID) {
|
|
||||||
const res = await POST(`/Reviews`,review)
|
|
||||||
reviews.ID = res.data.ID
|
|
||||||
} else {
|
|
||||||
console.trace()
|
|
||||||
await PUT(`/Reviews/${review.ID}`,review)
|
|
||||||
}
|
|
||||||
reviews.message = { succeeded: 'Your review was submitted successfully. Thanks.' }
|
|
||||||
} catch (e) {
|
|
||||||
reviews.message = { failed: e.response.data.error.message }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
filters: {
|
|
||||||
stars: (r) => ('★'.repeat(Math.round(r))+'☆☆☆☆☆').slice(0,5),
|
|
||||||
datetime: (d) => d && new Date(d).toLocaleString(),
|
|
||||||
},
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
// initially fill list of my reviews
|
|
||||||
reviews.fetch()
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title> Capire Reviews </title>
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/primitive-ui/dist/css/main.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
|
|
||||||
<style>
|
|
||||||
.hovering tr:hover td { color:cyan; background: #123; cursor: pointer; }
|
|
||||||
.rating-stars { color:teal }
|
|
||||||
.succeeded { color:teal }
|
|
||||||
.failed { color:red }
|
|
||||||
textarea { line-height: 1.4em;}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="small-container", style="margin-top: 70px;">
|
|
||||||
<div id='app'>
|
|
||||||
|
|
||||||
<h1> {{ document.title }} </h1>
|
|
||||||
|
|
||||||
<input type="text" placeholder="Search..." @input="search">
|
|
||||||
|
|
||||||
<table id='my-reviews' class="hovering">
|
|
||||||
<thead>
|
|
||||||
<th> Subject </th>
|
|
||||||
<th> Rating </th>
|
|
||||||
<th> Title </th>
|
|
||||||
<th> Date </th>
|
|
||||||
</thead>
|
|
||||||
<tr v-for="review in list" v-bind:id="review.ID" v-on:click="inspect">
|
|
||||||
<td>{{ review.subject }}</td>
|
|
||||||
<td class="rating-stars">{{ review.rating | stars }}</td>
|
|
||||||
<td>{{ review.title }}</td>
|
|
||||||
<td>{{ review.date | datetime }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<button v-on:click="newReview()" class="round-button muted-button">Add Review...</button>
|
|
||||||
|
|
||||||
<form v-if="review" @submit.prevent="submitReview">
|
|
||||||
<input id="subject" type="text" v-model="review.subject" style="font-weight:bold; display:inline; width:20%">
|
|
||||||
<input type="text" v-model="review.title" style="font-weight:bold; display:inline; width:60%">
|
|
||||||
<select v-model="review.rating" style="font-weight:bold; display:inline; width:17%; float: right;">
|
|
||||||
<option v-for="option in Ratings" v-bind:value="option[0]"> {{ option[1] }} </option>
|
|
||||||
</select>
|
|
||||||
<textarea v-model="review.text" rows="9"></textarea>
|
|
||||||
<input type="submit" value="Submit" class="round-button muted-button">
|
|
||||||
<span class="succeeded"> {{ message.succeeded }} </span>
|
|
||||||
<span class="failed"> {{ message.failed }} </span>
|
|
||||||
</form>
|
|
||||||
<div v-else style="margin-top: 2em;">
|
|
||||||
( click on a row to see details... )
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
<script src="app.js"></script>
|
|
||||||
</html>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
subject;rating;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;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;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.
|
|
||||||
|
@@ -17,7 +17,7 @@ entity Reviews {
|
|||||||
liked : Integer default 0; // counter for likes as helpful review (count of all _likes belonging to this review)
|
liked : Integer default 0; // counter for likes as helpful review (count of all _likes belonging to this review)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Rating : Integer enum {
|
type Rating : Decimal(3,2) enum {
|
||||||
Best = 5;
|
Best = 5;
|
||||||
Good = 4;
|
Good = 4;
|
||||||
Avg = 3;
|
Avg = 3;
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
using from './srv/reviews-service';
|
using from './srv/reviews-service';
|
||||||
namespace sap.capire.reviews;
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"index.cds"
|
"index.cds"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sap/cds": ">=4",
|
"@sap/cds": ">=3.33.1",
|
||||||
"express": "^4.17.1"
|
"express": "^4.17.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -2,23 +2,20 @@
|
|||||||
|
|
||||||
## Run all-in-one
|
## Run all-in-one
|
||||||
|
|
||||||
Open a terminal window and run the `fiori` app in it:
|
Open a terminal window and run the bookshop in it:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run fiori
|
npm run bookshop
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Run as Separate Services
|
## Run as separate services
|
||||||
|
|
||||||
Open two terminal windows. In the first one start the reviews service stand-alone:
|
|
||||||
|
|
||||||
|
Open two terminal windows, and in the first one start the reviews service stand-alone:
|
||||||
```sh
|
```sh
|
||||||
npm run reviews-service
|
npm run reviews-service
|
||||||
```
|
```
|
||||||
|
|
||||||
In the second one start the `fiori` app:
|
In the the second one start the bookshop:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run fiori
|
npm run bookshop
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -11,34 +11,27 @@ service ReviewsService {
|
|||||||
event reviewed : {
|
event reviewed : {
|
||||||
subject: type of Reviews:subject;
|
subject: type of Reviews:subject;
|
||||||
rating: Decimal(2,1)
|
rating: Decimal(2,1)
|
||||||
}
|
};
|
||||||
|
|
||||||
// Input validation
|
// Input validation
|
||||||
annotate Reviews with {
|
annotate Reviews with {
|
||||||
subject @mandatory;
|
subject @mandatory;
|
||||||
title @mandatory;
|
title @mandatory;
|
||||||
rating @assert.range;
|
rating @mandatory @assert.enum;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Access control restrictions
|
// Access control restrictions
|
||||||
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' },
|
||||||
];
|
];
|
||||||
|
|
||||||
annotate ReviewsService with @restrict:[
|
annotate ReviewsService with @restrict_:[
|
||||||
{ grant:'like', to:'identified-user' },
|
{ grant:'like', to:'identified-user' },
|
||||||
{ grant:'unlike', to:'identified-user', where:'user=$user' },
|
{ grant:'unlike', to:'identified-user', where:'user=$user' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -10,20 +10,23 @@ 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) {
|
// ( Note: req.on.succeeded ensures we only do that if there's no error )
|
||||||
|
this.after (['CREATE','UPDATE','DELETE'], 'Reviews', async(_,req) => {
|
||||||
const {subject} = req.data
|
const {subject} = req.data
|
||||||
const {rating} = await cds.tx(req) .run (
|
const {rating} = await cds.transaction(req) .run (
|
||||||
SELECT.one (['round(avg(rating),2) as rating']) .from (Reviews) .where ({subject})
|
SELECT.one (['round(avg(rating),2) as rating']) .from (Reviews) .where ({subject})
|
||||||
)
|
)
|
||||||
|
req.on ('succeeded', ()=>{
|
||||||
global.it || console.log ('< emitting:', 'reviewed', { subject, rating })
|
global.it || console.log ('< emitting:', 'reviewed', { subject, rating })
|
||||||
await this.emit ('reviewed', { subject, rating })
|
this.emit ('reviewed', { subject, rating })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Increment counter for reviews considered helpful
|
// Increment counter for reviews considered helpful
|
||||||
this.on ('like', (req) => {
|
this.on ('like', (req) => {
|
||||||
if (!req.user) return req.reject(400, 'You must be identified to like a review')
|
if (!req.user) return req.reject(400, 'You must be identified to like a review')
|
||||||
const {review} = req.data, {user} = req
|
const {review} = req.data, {user} = req
|
||||||
const tx = cds.tx(req)
|
const tx = cds.transaction(req)
|
||||||
return tx.run ([
|
return tx.run ([
|
||||||
INSERT.into (Likes) .entries ({review_ID: review, user: user.id}),
|
INSERT.into (Likes) .entries ({review_ID: review, user: user.id}),
|
||||||
UPDATE (Reviews) .set({liked: {'+=': 1}}) .where({ID:review})
|
UPDATE (Reviews) .set({liked: {'+=': 1}}) .where({ID:review})
|
||||||
@@ -34,7 +37,7 @@ module.exports = cds.service.impl (function(){
|
|||||||
this.on ('unlike', async (req) => {
|
this.on ('unlike', async (req) => {
|
||||||
if (!req.user) return req.reject(400, 'You must be identified to remove a former like of yours')
|
if (!req.user) return req.reject(400, 'You must be identified to remove a former like of yours')
|
||||||
const {review} = req.data, {user} = req
|
const {review} = req.data, {user} = req
|
||||||
const tx = cds.tx(req)
|
const tx = cds.transaction(req)
|
||||||
const affectedRows = await tx.run (DELETE.from (Likes) .where ({review_ID: review,user: user.id}))
|
const affectedRows = await tx.run (DELETE.from (Likes) .where ({review_ID: review,user: user.id}))
|
||||||
if (affectedRows === 1) return tx.run (UPDATE (Reviews) .set ({liked: {'-=': 1}}) .where ({ID:review}))
|
if (affectedRows === 1) return tx.run (UPDATE (Reviews) .set ({liked: {'-=': 1}}) .where ({ID:review}))
|
||||||
})
|
})
|
||||||
|
|||||||
40
samples.md
40
samples.md
@@ -1,28 +1,28 @@
|
|||||||
# Overview of Samples
|
# Overview of Samples
|
||||||
|
|
||||||
The following list gives an overview of the samples provided in subdirectories.
|
The list below gives an overview of the samples provided in subdirectories.
|
||||||
Each sub directory essentially is an individual npm package arranged in an [all-in-one monorepo](all-in-one-monorepo) umbrella setup.
|
Each sub directory essentially is a individual npm package arranged in an [all-in-one monorepo](all-in-one-monorepo) umbrella setup.
|
||||||
|
|
||||||
|
|
||||||
## [@capire/hello-world](hello)
|
## [hello](hello)
|
||||||
|
|
||||||
- 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).
|
- 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).
|
||||||
|
|
||||||
|
|
||||||
## [@capire/bookshop](bookshop)
|
## [bookshop](bookshop)
|
||||||
|
|
||||||
- [Getting Started](https://cap.cloud.sap/docs/get-started/in-a-nutshell) with CAP, briefly introducing:
|
- [Getting Started](https://cap.cloud.sap/docs/get-started/in-a-nutshell) with CAP, briefly introducing:
|
||||||
- [Project Setup](https://cap.cloud.sap/docs/get-started/) and [Layouts](https://cap.cloud.sap/docs/get-started/projects)
|
- [Project Setup](https://cap.cloud.sap/docs/get-started/) and [Layouts](https://cap.cloud.sap/docs/get-started/projects)
|
||||||
- [Domain Modeling](https://cap.cloud.sap/docs/guides/domain-models)
|
- [Domain Modelling](https://cap.cloud.sap/docs/guides/domain-models)
|
||||||
- [Defining Services](https://cap.cloud.sap/docs/guides/providing-services)
|
- [Defining Services](https://cap.cloud.sap/docs/guides/providing-services)
|
||||||
- [Generic Providers](https://cap.cloud.sap/docs/guides/generic-providers)
|
- [Generic Providers](https://cap.cloud.sap/docs/guides/generic-providers)
|
||||||
- [Adding Custom Logic](https://cap.cloud.sap/docs/guides/service-impl)
|
- [Adding Custom Logic](https://cap.cloud.sap/docs/guides/service-impl)
|
||||||
- [Using Databases](https://cap.cloud.sap/docs/guides/databases)
|
- [Using Databases](https://cap.cloud.sap/docs/guides/databases)
|
||||||
|
|
||||||
|
|
||||||
## [@capire/common](common)
|
## [common](common)
|
||||||
|
|
||||||
- Showcases how to extend [@sap/cds/common](https://cap.cloud.sap/docs/cds/common) thereby covering:
|
- Showcases how to extend [@sap/cds/common](https://cap.cloud.sap/docs/cds/common) thereby covering...
|
||||||
- Building [extension packages](https://cap.cloud.sap/docs/guides/domain-models#aspects-extensibility)
|
- Building [extension packages](https://cap.cloud.sap/docs/guides/domain-models#aspects-extensibility)
|
||||||
- Providing [reuse packages](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content)
|
- Providing [reuse packages](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content)
|
||||||
- [Verticalization](https://cap.cloud.sap/docs/cds/common#adapting-to-your-needs)
|
- [Verticalization](https://cap.cloud.sap/docs/cds/common#adapting-to-your-needs)
|
||||||
@@ -30,39 +30,35 @@ Each sub directory essentially is an individual npm package arranged in an [all-
|
|||||||
- Used in the [fiori app sample](#fiori)
|
- Used in the [fiori app sample](#fiori)
|
||||||
|
|
||||||
|
|
||||||
## [@capire/orders](orders)
|
## [orders](orders)
|
||||||
|
|
||||||
- A standalone orders management service, demonstrating:
|
- Adds orders to the [bookshop](#bookshop), thereby demonstrating...
|
||||||
- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with
|
- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with
|
||||||
- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)
|
- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)
|
||||||
|
|
||||||
|
|
||||||
## [@capire/reviews](reviews)
|
## [reviews](reviews)
|
||||||
|
|
||||||
- Shows how to implement a modular service to manage product reviews, including:
|
- Shows how to implement a modular service to manage product reviews, including...
|
||||||
- Consuming other services synchronously and asynchronously
|
- Consuming other services synchronously and asynchronously
|
||||||
- Serving requests synchronously
|
- Serving requests synchronously
|
||||||
- Emitting events asynchronously
|
- Emitting events asynchronously
|
||||||
- Grow as you go, with:
|
- Grow as you go, with...
|
||||||
- Mocking app services
|
- Mocking app services
|
||||||
- Running service meshes
|
- Running service meshes
|
||||||
- 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/fiori](fiori)
|
## [fiori](fiori)
|
||||||
|
|
||||||
- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:
|
- [Adds a Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/), introducing to...
|
||||||
- [@capire/bookshop](bookshop)
|
|
||||||
- [@capire/reviews](reviews)
|
|
||||||
- [@capire/orders](orders)
|
|
||||||
- [@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
|
- [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 [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)
|
||||||
- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)
|
- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)
|
||||||
- Serving SAP Fiori apps locally
|
- Serving Fiori apps locally
|
||||||
- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well
|
- Combining most of the other samples through [package reuse](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content)
|
||||||
|
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
|||||||
151
test/_cds_tests.js
Normal file
151
test/_cds_tests.js
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// This is included with cds in newer versions
|
||||||
|
//
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
const { resolve, dirname } = require('path')
|
||||||
|
const cwd = process.cwd()
|
||||||
|
|
||||||
|
// harmonizing jest and mocha
|
||||||
|
const is_mocha = !global.test
|
||||||
|
const is_jest = !!global.test
|
||||||
|
if (is_jest) { // it's jest
|
||||||
|
global.before = (msg,fn) => global.beforeAll(fn||msg)
|
||||||
|
global.after = (msg,fn) => global.afterAll(fn||msg)
|
||||||
|
} else { // it's mocha
|
||||||
|
global.beforeAll = global.before
|
||||||
|
global.afterAll = global.after
|
||||||
|
global.test = global.it
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test kit for jest or mocha testing, which can be used statically
|
||||||
|
* via the getters for chai, expect and assert or through a server
|
||||||
|
* started with .launch(...).
|
||||||
|
*/
|
||||||
|
class CDSTestKit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance serving projects from subfolders under the root
|
||||||
|
* specified by a sequence of path components which are concatenated with
|
||||||
|
* path.resolve().
|
||||||
|
*/
|
||||||
|
for (...paths) {
|
||||||
|
const tk = new CDSTestKit
|
||||||
|
tk.root = resolve (...paths)
|
||||||
|
return tk
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lazily loads and returns an instance of chai */
|
||||||
|
get chai() {
|
||||||
|
const chai = require('chai')
|
||||||
|
chai.use (require('chai-as-promised'))
|
||||||
|
chai.use (require('chai-subset'))
|
||||||
|
Object.defineProperty (CDSTestKit.prototype, 'chai', {value:chai})
|
||||||
|
return chai
|
||||||
|
}
|
||||||
|
get expect(){ return this.chai.expect }
|
||||||
|
get assert(){ return this.chai.assert }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launches a cds server with arbitrary port and returns a subclass which
|
||||||
|
* also acts as an axios lookalike, providing methods to send requests.
|
||||||
|
*/
|
||||||
|
launch (project, ...args) {
|
||||||
|
|
||||||
|
// Setting up test server
|
||||||
|
const console = global.console, logs=[]
|
||||||
|
const axios = require('axios').default
|
||||||
|
const lazy = this, test = {
|
||||||
|
|
||||||
|
GET: (path,...etc) => axios.get (test.url+path,...etc) .catch(_error),
|
||||||
|
PUT: (path,...etc) => axios.put (test.url+path,...etc) .catch(_error),
|
||||||
|
POST: (path,...etc) => axios.post (test.url+path,...etc) .catch(_error),
|
||||||
|
PATCH: (path,...etc) => axios.patch (test.url+path,...etc) .catch(_error),
|
||||||
|
DELETE: (path,...etc) => axios.delete (test.url+path,...etc) .catch(_error),
|
||||||
|
|
||||||
|
get: (path,...etc) => axios.get (test.url+path,...etc) .catch(_error),
|
||||||
|
put: (path,...etc) => axios.put (test.url+path,...etc) .catch(_error),
|
||||||
|
post: (path,...etc) => axios.post (test.url+path,...etc) .catch(_error),
|
||||||
|
patch: (path,...etc) => axios.patch (test.url+path,...etc) .catch(_error),
|
||||||
|
delete: (path,...etc) => axios.delete (test.url+path,...etc) .catch(_error),
|
||||||
|
|
||||||
|
/** Lazily loads and returns an instance of chai */
|
||||||
|
get chai(){ return lazy.chai },
|
||||||
|
get expect(){ return lazy.expect },
|
||||||
|
get assert(){ return lazy.assert },
|
||||||
|
}
|
||||||
|
|
||||||
|
// launch cds server...
|
||||||
|
before (`launching cds server for ${project}...`, done => {
|
||||||
|
|
||||||
|
const cds = require('@sap/cds'), { isdir } = cds.utils
|
||||||
|
|
||||||
|
let cmd = 'run'
|
||||||
|
if (project.startsWith('cds ')) [ cmd, project ] = [ project.slice(4), args.shift() ]
|
||||||
|
if (!args.length) args = ['--in-memory?']
|
||||||
|
|
||||||
|
// Supporting .launch (<package name>)
|
||||||
|
if (cmd === 'run') {
|
||||||
|
if (isdir(project)) ; //> all fine
|
||||||
|
else if (isdir(resolve(this.root,project))) project = resolve(this.root,project)
|
||||||
|
else try { project = dirname (require.resolve(project+'/package.json')) }
|
||||||
|
catch(e) { throw cds.error (`Cannot resolve project folder for '${project}'`) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.env.CDS_TEST_VERBOSE) global.console = { __proto__: global.console, logs,
|
||||||
|
time: ()=>{}, timeEnd: (...args)=> logs.push(args),
|
||||||
|
debug: (...args)=> logs.push(args),
|
||||||
|
log: (...args)=> logs.push(args),
|
||||||
|
warn: (...args)=> logs.push(args),
|
||||||
|
error: (...args)=> logs.push(args),
|
||||||
|
dump(){ for (let each of logs) console.log (...each) },
|
||||||
|
}
|
||||||
|
|
||||||
|
// return done (new Error(11))
|
||||||
|
process.env.PORT = '0'
|
||||||
|
const p = cds.exec (cmd, project, ...args) // TODO w/ @sap/cds@3.33.3: , '--port', '0')
|
||||||
|
if (p && 'catch' in p) p.catch (e => {
|
||||||
|
if (is_mocha) console.error(e)
|
||||||
|
done(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
cds.once('listening', ({ server, url }) => {
|
||||||
|
Object.assign (test,{server,url})
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// shutdown cds server...
|
||||||
|
after (done => {
|
||||||
|
if (global.console !== console) global.console = console
|
||||||
|
if (cwd !== process.cwd()) process.chdir(cwd)
|
||||||
|
test.server ? test.server.close (done) : done()
|
||||||
|
process.emit('shutdown')
|
||||||
|
})
|
||||||
|
|
||||||
|
function _error (e) {
|
||||||
|
if (!e.response) throw e
|
||||||
|
if (!e.response.data) throw e
|
||||||
|
if (!e.response.data.error) throw e
|
||||||
|
const { code, message } = e.response.data.error
|
||||||
|
throw new Error (code && code !== 'null' ? `${code} - ${message}` : message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return test
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-global-assign
|
||||||
|
require = (mod) => {
|
||||||
|
try { return module.require(mod) }
|
||||||
|
catch(e) { if (e.code === 'MODULE_NOT_FOUND') throw new Error (`
|
||||||
|
Failed to load required package '${mod}'. Please add it thru:
|
||||||
|
npm add -D ${mod === 'chai' ? 'chai chai-as-promised chai-subset' : mod}
|
||||||
|
`) }
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new CDSTestKit
|
||||||
|
exports.root = __dirname
|
||||||
1
test/capire.js
Normal file
1
test/capire.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./_cds_tests').for(__dirname,'..')
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
const { expect } = require('../test')
|
const { expect } = require('./capire')
|
||||||
const cds = require('@sap/cds/lib')
|
const cds = require('@sap/cds')
|
||||||
const CQL = ([cql]) => cds.parse.cql(cql)
|
const CQL = ([cql]) => cds.parse.cql(cql)
|
||||||
const Foo = { name: 'Foo' }
|
const Foo = { name: 'Foo' }
|
||||||
const Books = { name: 'capire.bookshop.Books' }
|
const Books = { name: 'capire.bookshop.Books' }
|
||||||
|
|
||||||
|
const is_cds_333 = cds.version >= '3.33.3'
|
||||||
|
if (!is_cds_333) {
|
||||||
|
// Monky-patching v3.33.3 features in older releases
|
||||||
|
const up = UPDATE('x').constructor.prototype
|
||||||
|
up.with = up.set
|
||||||
|
}
|
||||||
const { parse:cdr } = cds.ql
|
const { parse:cdr } = cds.ql
|
||||||
|
|
||||||
// while jest has 'test' as alias to 'it', mocha doesn't
|
// while jest has 'test' as alias to 'it', mocha doesn't
|
||||||
@@ -13,9 +19,8 @@ describe('cds.ql → cqn', () => {
|
|||||||
//
|
//
|
||||||
let cqn
|
let cqn
|
||||||
|
|
||||||
describe.skip(`BUGS + GAPS...`, () => {
|
describe(`BUGS + GAPS...`, () => {
|
||||||
|
!(cdr ? it : it.skip)('should consistently handle *', () => {
|
||||||
it('should consistently handle *', () => {
|
|
||||||
expect({
|
expect({
|
||||||
SELECT: { from: { ref: ['Foo'] }, columns: ['*'] },
|
SELECT: { from: { ref: ['Foo'] }, columns: ['*'] },
|
||||||
})
|
})
|
||||||
@@ -25,17 +30,29 @@ describe('cds.ql → cqn', () => {
|
|||||||
.to.eql(SELECT.from(Foo, ['*']))
|
.to.eql(SELECT.from(Foo, ['*']))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
!(cdr ? it : it.skip)('should correctly handle { ... and:{...} }', () => {
|
||||||
it('should consistently handle lists', () => {
|
expect(SELECT.from(Foo).where({ x: 1, and: { y: 2, or: { z: 3 } } })).to.eql({
|
||||||
const ID = 11, args = [`foo`, "'bar'", 3]
|
SELECT: {
|
||||||
const cqn = CQL`SELECT from Foo where ID=11 and x in (foo,'bar',3)`
|
from: { ref: ['Foo'] },
|
||||||
expect(SELECT.from(Foo).where(`ID=${ID} and x in (${args})`)).to.eql(cqn)
|
where: [
|
||||||
expect(SELECT.from(Foo).where(`ID=`, ID, `and x in`, args)).to.eql(cqn)
|
{ ref: ['x'] },
|
||||||
expect(SELECT.from(Foo).where({ ID, x:args })).to.eql(cqn)
|
'=',
|
||||||
|
{ val: 1 },
|
||||||
|
'and',
|
||||||
|
'(',
|
||||||
|
{ ref: ['y'] },
|
||||||
|
'=',
|
||||||
|
{ val: 2 },
|
||||||
|
'or',
|
||||||
|
{ ref: ['z'] },
|
||||||
|
'=',
|
||||||
|
{ val: 3 },
|
||||||
|
')',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
describe(`SELECT...`, () => {
|
describe(`SELECT...`, () => {
|
||||||
test('from ( Foo )', () => {
|
test('from ( Foo )', () => {
|
||||||
@@ -167,6 +184,7 @@ describe('cds.ql → cqn', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
is_cds_333 &&
|
||||||
test('from ( ..., => _.expand ( x=>{...}))', () => {
|
test('from ( ..., => _.expand ( x=>{...}))', () => {
|
||||||
// SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } }
|
// SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } }
|
||||||
expect(
|
expect(
|
||||||
@@ -194,6 +212,7 @@ describe('cds.ql → cqn', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
is_cds_333 &&
|
||||||
test('from ( ..., => _.inline ( _=>{...}))', () => {
|
test('from ( ..., => _.inline ( _=>{...}))', () => {
|
||||||
// SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } }
|
// SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } }
|
||||||
expect(
|
expect(
|
||||||
@@ -242,30 +261,7 @@ describe('cds.ql → cqn', () => {
|
|||||||
// same for works distinct
|
// same for works distinct
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should correctly handle { ... and:{...} }', () => {
|
test.skip('where ( ... cql | {x:y} )', () => {
|
||||||
expect(SELECT.from(Foo).where({ x: 1, and: { y: 2, or: { z: 3 } } })).to.eql({
|
|
||||||
SELECT: {
|
|
||||||
from: { ref: ['Foo'] },
|
|
||||||
where: [
|
|
||||||
{ ref: ['x'] },
|
|
||||||
'=',
|
|
||||||
{ val: 1 },
|
|
||||||
'and',
|
|
||||||
'(',
|
|
||||||
{ ref: ['y'] },
|
|
||||||
'=',
|
|
||||||
{ val: 2 },
|
|
||||||
'or',
|
|
||||||
{ ref: ['z'] },
|
|
||||||
'=',
|
|
||||||
{ val: 3 },
|
|
||||||
')',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('where ( ... cql | {x:y} )', () => {
|
|
||||||
const args = [`foo`, "'bar'", 3]
|
const args = [`foo`, "'bar'", 3]
|
||||||
const ID = 11
|
const ID = 11
|
||||||
|
|
||||||
@@ -302,7 +298,7 @@ describe('cds.ql → cqn', () => {
|
|||||||
')',
|
')',
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
// '(', //> this one is not required
|
'(', //> this one is not required
|
||||||
{ ref: ['ID'] },
|
{ ref: ['ID'] },
|
||||||
'=',
|
'=',
|
||||||
{ val: ID },
|
{ val: ID },
|
||||||
@@ -311,7 +307,7 @@ describe('cds.ql → cqn', () => {
|
|||||||
'in',
|
'in',
|
||||||
{ val: args },
|
{ val: args },
|
||||||
'and',
|
'and',
|
||||||
'(', //> this one is missing, and that's changing the logic -> that's a BUG
|
// '(', //> this one is missing, and that's changing the logic -> that's a BUG
|
||||||
{ ref: ['x'] },
|
{ ref: ['x'] },
|
||||||
'like',
|
'like',
|
||||||
{ val: '%x%' },
|
{ val: '%x%' },
|
||||||
@@ -325,26 +321,7 @@ describe('cds.ql → cqn', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// using CQL fragments -> uses cds.parse.expr
|
// using CQL fragments -> uses cds.parse.expr
|
||||||
const is_v2 = !!cds.parse.expr('(1,2)').list
|
expect((cqn = CQL`SELECT from Foo where ID=11 and x in ( foo, 'bar', 3)`)).to.eql({
|
||||||
if (is_v2) expect((cqn = CQL`SELECT from Foo where ID=11 and x in ( foo, 'bar', 3)`)).to.eql({
|
|
||||||
SELECT: {
|
|
||||||
from: { ref: ['Foo'] },
|
|
||||||
where: [
|
|
||||||
{ ref: ['ID'] },
|
|
||||||
'=',
|
|
||||||
{ val: ID },
|
|
||||||
'and',
|
|
||||||
{ ref: ['x'] },
|
|
||||||
'in',
|
|
||||||
{list:[
|
|
||||||
{ ref: ['foo'] },
|
|
||||||
{ val: 'bar' },
|
|
||||||
{ val: 3 },
|
|
||||||
]}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
else expect((cqn = CQL`SELECT from Foo where ID=11 and x in ( foo, 'bar', 3)`)).to.eql({
|
|
||||||
SELECT: {
|
SELECT: {
|
||||||
from: { ref: ['Foo'] },
|
from: { ref: ['Foo'] },
|
||||||
where: [
|
where: [
|
||||||
@@ -364,8 +341,10 @@ describe('cds.ql → cqn', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
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)
|
||||||
|
|
||||||
if (!is_v2) expect(
|
expect(
|
||||||
SELECT.from(Foo).where(`x=`, 1, `or y.z is null and (a>`, 2, `or b=`, 3, `)`)
|
SELECT.from(Foo).where(`x=`, 1, `or y.z is null and (a>`, 2, `or b=`, 3, `)`)
|
||||||
).to.eql(CQL`SELECT from Foo where x=1 or y.z is null and (a>2 or b=3)`)
|
).to.eql(CQL`SELECT from Foo where x=1 or y.z is null and (a>2 or b=3)`)
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,34 @@
|
|||||||
const { expect } = require('../test') .run (
|
const { expect } = require('./capire')
|
||||||
'serve', 'AdminService', '--from', '@capire/bookshop,@capire/common', '--in-memory'
|
const cds = require('@sap/cds')
|
||||||
)
|
|
||||||
const cds = require('@sap/cds/lib')
|
const cwd = process.cwd()
|
||||||
|
before (()=> process.chdir(__dirname))
|
||||||
|
after(()=> process.chdir(cwd))
|
||||||
|
|
||||||
describe('Consuming Services locally', () => {
|
describe('Consuming Services locally', () => {
|
||||||
//
|
//
|
||||||
it('bootrapped the database successfully', ()=>{
|
before('bootstrap db and services', async () => {
|
||||||
const { AdminService } = cds.services
|
const model = await cds.load(['@capire/bookshop', '@capire/common'])
|
||||||
|
await cds.deploy(model).to('sqlite::memory:')
|
||||||
|
const { AdminService } = await cds.serve('AdminService').from(model)
|
||||||
const { Authors } = AdminService.entities
|
const { Authors } = AdminService.entities
|
||||||
expect(AdminService).not.to.be.undefined
|
expect(AdminService).not.to.be.undefined
|
||||||
expect(Authors).not.to.be.undefined
|
expect(Authors).not.to.be.undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('bootrapped the database successfully', ()=>{})
|
||||||
|
|
||||||
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
|
||||||
const _ = expect (await AdminService.read(Authors))
|
expect(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)))
|
||||||
// temporary workaround
|
// temporary workaround
|
||||||
if (cds.version >= '4.2.0')
|
if (AdminService.read.fix_50)
|
||||||
_.to.eql(await AdminService.run(SELECT.from('Authors')))
|
expect(await AdminService.run(SELECT.from(Authors))).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 () => {
|
||||||
@@ -35,8 +43,9 @@ describe('Consuming Services locally', () => {
|
|||||||
})
|
})
|
||||||
}).where(`name like`, 'E%')
|
}).where(`name like`, 'E%')
|
||||||
// temporary workaround
|
// temporary workaround
|
||||||
if (cds.version < '4.2.0')
|
if (!AdminService.read.fix_50) {
|
||||||
query.SELECT.from.ref[0] = 'AdminService.Authors'
|
query.SELECT.from.ref[0] = 'AdminService.Authors'
|
||||||
|
}
|
||||||
const authors = await AdminService.run(query)
|
const authors = await AdminService.run(query)
|
||||||
expect(authors).to.containSubset([
|
expect(authors).to.containSubset([
|
||||||
{
|
{
|
||||||
@@ -74,14 +83,17 @@ describe('Consuming Services locally', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
const query1 = SELECT.from(Authors, projection).where(`name like`, 'E%')
|
const query1 = SELECT.from(Authors, projection).where(`name like`, 'E%')
|
||||||
const query2 = cds.read(Authors, projection).where(`name like`, 'E%')
|
|
||||||
expect(await cds.run(query1))
|
expect(await cds.run(query1))
|
||||||
.to.eql(await db.run(query1))
|
.to.eql(await db.run(query1))
|
||||||
.to.eql(await srv.run(query1))
|
.to.eql(await srv.run(query1))
|
||||||
.to.eql(await srv.read(Authors, projection).where(`name like`, 'E%'))
|
.to.eql(await srv.read(Authors, projection).where(`name like`, 'E%'))
|
||||||
|
const query2 = cds.read(Authors, projection).where(`name like`, 'E%')
|
||||||
|
// temporary workaround
|
||||||
|
if (srv.read.fix_50)
|
||||||
|
expect(await cds.run(query1))
|
||||||
.to.eql(await cds.run(query2))
|
.to.eql(await cds.run(query2))
|
||||||
.to.eql(await db.run(query2))
|
.to.eql(await db.run(query2))
|
||||||
.to.eql(await srv.run(query2))
|
.to.eql(await srv.run(query2))
|
||||||
.to.eql(await db.read(Authors, projection).where(`name like`, 'E%'))
|
.to.eql(await db.read(Authors, projection).where(`name like`, 'E%')) // FIXME!!
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
const { GET, POST, expect } = require('../test') .run ('bookshop')
|
const cwd = process.cwd()
|
||||||
const cds = require('@sap/cds/lib')
|
const is_jest = !!global.test
|
||||||
if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch
|
if (is_jest) { // it's jest
|
||||||
else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases
|
global.before = (msg,fn) => global.beforeAll(fn||msg)
|
||||||
|
global.after = (msg,fn) => global.afterAll(fn||msg)
|
||||||
|
}
|
||||||
|
before (()=> process.chdir(__dirname))
|
||||||
|
after (()=> process.chdir(cwd))
|
||||||
|
|
||||||
describe('Custom Handlers', () => {
|
describe('Custom Handlers', () => {
|
||||||
|
const { GET, POST, expect } = require('./capire').launch('bookshop')
|
||||||
|
|
||||||
it('should reject out-of-stock orders', async () => {
|
it('should reject out-of-stock orders', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const { GET, expect } = require('../test') .run ('serve','hello/world.cds')
|
|
||||||
|
|
||||||
describe('Hello world!', () => {
|
describe('Hello world!', () => {
|
||||||
|
const { GET, expect } = require('./capire').launch('cds serve', __dirname+'/../hello/world.cds', '')
|
||||||
|
|
||||||
it('should say hello with class impl', async () => {
|
it('should say hello with class impl', async () => {
|
||||||
const {data} = await GET `/say/hello(to='world')`
|
const {data} = await GET `/say/hello(to='world')`
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
const {expect} = require('../test')
|
const cwd = process.cwd(); process.chdir (__dirname) //> only for internal CI/CD@SAP
|
||||||
const cds = require('@sap/cds/lib')
|
const {expect} = require('./capire')
|
||||||
|
const cds = require ('@sap/cds')
|
||||||
|
|
||||||
|
// monkey patching older releases:
|
||||||
|
if (!cds.compile.cdl) cds.compile.cdl = cds.parse
|
||||||
const { parse:cdr } = cds.ql
|
const { parse:cdr } = cds.ql
|
||||||
|
|
||||||
// should become cds.compile(...) when cds5 is released
|
const model = cds.compile.cdl (`
|
||||||
const model = cds.compile.to.csn (`
|
|
||||||
entity Categories {
|
entity Categories {
|
||||||
key ID : Integer;
|
key ID : Integer;
|
||||||
name : String;
|
name : String;
|
||||||
@@ -23,6 +25,8 @@ describe('Hierarchical Data', ()=>{
|
|||||||
expect (cds.db.model) .to.exist
|
expect (cds.db.model) .to.exist
|
||||||
})
|
})
|
||||||
|
|
||||||
|
after(()=> process.chdir(cwd))
|
||||||
|
|
||||||
it ('supports deeply nested inserts', ()=> INSERT.into (Cats,
|
it ('supports deeply nested inserts', ()=> INSERT.into (Cats,
|
||||||
{ ID:100, name:'Some Cats...', children:[
|
{ ID:100, name:'Some Cats...', children:[
|
||||||
{ ID:101, name:'Cat', children:[
|
{ ID:101, name:'Cat', children:[
|
||||||
@@ -72,7 +76,7 @@ describe('Hierarchical Data', ()=>{
|
|||||||
|
|
||||||
it ('supports cascaded deletes', async()=>{
|
it ('supports cascaded deletes', async()=>{
|
||||||
const affectedRows = await DELETE.from (Cats) .where ({ID:[102,106]})
|
const affectedRows = await DELETE.from (Cats) .where ({ID:[102,106]})
|
||||||
expect (affectedRows) .to.be.greaterThan (0)
|
expect (affectedRows) .to.equal (5)
|
||||||
const expected = [
|
const expected = [
|
||||||
{ ID:100, name:'Some Cats...' },
|
{ ID:100, name:'Some Cats...' },
|
||||||
{ ID:101, name:'Cat' },
|
{ ID:101, name:'Cat' },
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
const cds = require('@sap/cds')
|
|
||||||
module.exports = cds.test.in(__dirname,'..')
|
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
const { GET, expect } = require('../test') .run ('serve', 'test/localized-data.cds', '--in-memory')
|
|
||||||
const cds = require('@sap/cds/lib')
|
|
||||||
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
|
|
||||||
|
|
||||||
describe('Localized Data', () => {
|
describe('Localized Data', () => {
|
||||||
|
const { GET, expect } = require('./capire').launch('cds serve',__dirname+'/localized-data.cds')
|
||||||
|
|
||||||
it('serves localized $metadata documents', async () => {
|
it('serves localized $metadata documents', async () => {
|
||||||
const { data } = await GET`/browse/$metadata?sap-language=de`
|
const { data } = await GET`/browse/$metadata?sap-language=de`
|
||||||
@@ -76,7 +72,7 @@ describe('Localized Data', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('supports @cds.localized:false', async ()=>{
|
xit('supports @cds.localized:false', async ()=>{
|
||||||
const { data } = await GET(`/browse/BooksSans?&$select=title,localized_title&$expand=currency&$filter=locale eq 'de' or locale eq null`, {
|
const { data } = await GET(`/browse/BooksSans?&$select=title,localized_title&$expand=currency&$filter=locale eq 'de' or locale eq null`, {
|
||||||
headers: { 'Accept-Language': 'de' },
|
headers: { 'Accept-Language': 'de' },
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
const { expect } = require('../test')
|
const cwd = process.cwd(); process.chdir (__dirname) //> only for internal CI/CD@SAP
|
||||||
const cds = require('@sap/cds/lib')
|
const {expect} = require('./capire')
|
||||||
|
const cds = require ('@sap/cds')
|
||||||
const _model = '@capire/reviews'
|
const _model = '@capire/reviews'
|
||||||
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
|
|
||||||
|
|
||||||
describe('Messaging', ()=>{
|
describe('Messaging', ()=>{
|
||||||
|
|
||||||
|
after(()=> process.chdir(cwd))
|
||||||
|
|
||||||
it ('should bootstrap sqlite in-memory db', async()=>{
|
it ('should bootstrap sqlite in-memory db', async()=>{
|
||||||
const db = await cds.deploy (_model) .to ('sqlite::memory:')
|
const db = await cds.deploy (_model) .to ('sqlite::memory:')
|
||||||
await db.delete('Reviews')
|
|
||||||
expect (db.model) .not.undefined
|
expect (db.model) .not.undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ describe('Messaging', ()=>{
|
|||||||
|
|
||||||
let N=0, received=[], M=0
|
let N=0, received=[], M=0
|
||||||
it ('should add messaging event handlers', ()=>{
|
it ('should add messaging event handlers', ()=>{
|
||||||
srv.on('reviewed', (msg)=> received.push(msg))
|
srv.on('reviewed', (msg)=> {received.push(msg)})
|
||||||
})
|
})
|
||||||
|
|
||||||
it ('should add more messaging event handlers', ()=>{
|
it ('should add more messaging event handlers', ()=>{
|
||||||
@@ -42,16 +43,16 @@ describe('Messaging', ()=>{
|
|||||||
// { ID: 111 + (++N), subject: "201", title: "Captivating", rating: N },
|
// { ID: 111 + (++N), subject: "201", title: "Captivating", rating: N },
|
||||||
// ),
|
// ),
|
||||||
srv.create ('Reviews') .entries (
|
srv.create ('Reviews') .entries (
|
||||||
{ ID: String(111 + (++N)), subject: "201", title: "Captivating", rating: N }
|
{ ID: 111 + (++N), subject: "201", title: "Captivating", rating: N }
|
||||||
),
|
),
|
||||||
srv.create ('Reviews') .entries (
|
srv.create ('Reviews') .entries (
|
||||||
{ ID: String(111 + (++N)), subject: "201", title: "Captivating", rating: N }
|
{ ID: 111 + (++N), subject: "201", title: "Captivating", rating: N }
|
||||||
),
|
),
|
||||||
srv.create ('Reviews') .entries (
|
srv.create ('Reviews') .entries (
|
||||||
{ ID: String(111 + (++N)), subject: "201", title: "Captivating", rating: N }
|
{ ID: 111 + (++N), subject: "201", title: "Captivating", rating: N }
|
||||||
),
|
),
|
||||||
srv.create ('Reviews') .entries (
|
srv.create ('Reviews') .entries (
|
||||||
{ ID: String(111 + (++N)), subject: "201", title: "Captivating", rating: N }
|
{ ID: 111 + (++N), subject: "201", title: "Captivating", rating: N }
|
||||||
),
|
),
|
||||||
]))
|
]))
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
const { GET, expect } = require('../test') .run ('bookshop')
|
|
||||||
const cds = require('@sap/cds/lib')
|
|
||||||
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
|
|
||||||
|
|
||||||
describe('OData Protocol', () => {
|
describe('OData Protocol', () => {
|
||||||
|
const { GET, expect } = require('./capire').launch('bookshop')
|
||||||
|
|
||||||
it('serves $metadata documents in v4', async () => {
|
it('serves $metadata documents in v4', async () => {
|
||||||
const { headers, status, data } = await GET`/browse/$metadata`
|
const { headers, status, data } = await GET`/browse/$metadata`
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user