Compare commits

..

2 Commits

Author SHA1 Message Date
Daniel
b629f8faf3 Merge branch 'master' into temporal 2021-04-10 13:08:31 +02:00
Daniel
9479aa4cad . 2021-04-08 19:17:48 +02:00
81 changed files with 789 additions and 14820 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
# cdsc

View File

@@ -5,9 +5,9 @@ name: CI
on:
push:
branches: [ main ]
branches: [ master ]
pull_request:
branches: [ main ]
branches: [ master ]
jobs:
build:
@@ -24,5 +24,5 @@ jobs:
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm install
- run: npm test

2
.gitignore vendored
View File

@@ -15,5 +15,3 @@ default-env.json
packages/messageBox
reviews/msg-box
reviews/db/test.db
*.openapi3.json

3
.npmrc
View File

@@ -1,3 +0,0 @@
# Ensure we always use public packages, i.e. avoid using local registries from ~/.npmrc
@sap:registry=https://registry.npmjs.org/
registry=https://registry.npmjs.org/

View File

@@ -1,25 +1,19 @@
const { exec } = require ('child_process')
const isWin = process.platform === 'win32'
const express = require ('express')
const fs = require ('fs')
const app = express()
const { PORT=4444 } = process.env
const [,,port=PORT,scope='@capire'] = process.argv
const cwd = __dirname
// clean up on start (exit handler might not complete on Windows)
exec(isWin ? 'del *.tgz' : 'rm *.tgz', {cwd})
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 ] = /^\w+-(\w+)/.exec(tarball)
const [, pkg ] = /^capire-(\w+)/.exec(tarball)
fs.lstat(tarball,(err => {
if (err) console.debug (`npm pack ../${pkg}`)
if (err) exec(`npm pack ../${pkg}`,{cwd},next)
if (err) exec(`npm pack ../${pkg}`,next)
else next()
}))
} catch (e) {
@@ -31,14 +25,12 @@ app.use('/-/:tarball', (req,res,next) => {
app.use('/-', express.static(__dirname))
app.get('/*', (req,res)=>{
const urlRegex = /^\/(@\w+)\/(\w+)/
const url = decodeURIComponent(req.url)
console.debug ('GET',url)
try {
if (!urlRegex.test(url)) return res.sendStatus(404)
const [, scpe, pkg ] = urlRegex.exec(url)
const package = require (`${scpe}/${pkg}/package.json`)
const tarball = `${scpe.slice(1)}-${pkg}-${package.version}.tgz`
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,
@@ -50,30 +42,29 @@ app.get('/*', (req,res)=>{
"name": package.name,
"version": package.version,
"dist": {
"tarball": `${server.url}/-/${tarball}`
"tarball": `http://localhost:${port}/-/${tarball}`
},
}
},
})
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND') return res.sendStatus(404)
console.error(e); throw e
console.error(e)
res.sendStatus(404)
}
})
const server = app.listen(port, ()=>{
const url = server.url = `http://localhost:${server.address().port}`
console.log (`npm set ${scope}:registry=${url}`)
exec(`npm set ${scope}:registry=${url}`)
console.log (`${scope} registry listening on ${url}`)
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 = ()=>{
server.close()
exec(`npm conf rm "${scope}:registry"`, ()=> { process.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)

View File

@@ -68,35 +68,26 @@
},
{
"file": "fiori/package.json",
"description": "#### Configuration\n\nThe `cds.requires` section in `package.json` is a place to configure which of the `db/sqlite` and `db/hana` folders are used for which database.\n\nWe use [Node.js profiles](https://cap.cloud.sap/docs/node.js/cds-env#profiles) to separate the configuration.\nIn the `development` profile, you can see that `db/sqlite` is set as the model, while the `db/hana` folder is configured in the `production` profile. `db-ext` is a pseudo datasource, its name doesn't matter.\n\nSee [`cds.resolve`](https://cap.cloud.sap/docs/node.js/cds-compile#cds-resolve) to learn more about how models are found.",
"selection": {
"start": {
"line": 41,
"character": 1
},
"end": {
"line": 48,
"character": 1
}
},
"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": 43,
"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": 46,
"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": 72,
"line": 68,
"selection": {
"start": {
"line": 67,
@@ -113,5 +104,6 @@
"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"
}

View File

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

View File

@@ -13,5 +13,5 @@
"**/cds/lib/req/cls.js",
"**/odata-v4/okra/**"
]
}
},
}

View File

@@ -17,7 +17,7 @@ Find here a collection of samples for the [SAP Cloud Application Programming Mod
### Download
If you've [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, otherwise [download as ZIP file](archive/main.zip).
If you've [Git](https://git-scm.com/downloads) installed, clone this repo as shown below, otherwise [download as ZIP file](archive/master.zip).
```sh
git clone https://github.com/sap-samples/cloud-cap-samples samples

View File

@@ -10,7 +10,7 @@ const books = new Vue ({
data: {
list: [],
book: undefined,
order: { quantity:1, succeeded:'', failed:'' }
order: { amount:1, succeeded:'', failed:'' }
},
methods: {
@@ -26,18 +26,18 @@ const books = new Vue ({
const book = books.book = books.list [eve.currentTarget.rowIndex-1]
const res = await GET(`/Books/${book.ID}?$select=descr,stock,image`)
Object.assign (book, res.data)
books.order = { quantity:1 }
books.order = { amount:1 }
setTimeout (()=> $('form > input').focus(), 111)
},
async submitOrder () {
const {book,order} = books, quantity = parseInt (order.quantity) || 1 // REVISIT: Okra should be less strict
const {book,order} = books, amount = parseInt (order.amount) || 1 // REVISIT: Okra should be less strict
try {
const res = await POST(`/submitOrder`, { quantity, book: book.ID })
const res = await POST(`/submitOrder`, { amount, book: book.ID })
book.stock = res.data.stock
books.order = { quantity, succeeded: `Successfully ordered ${quantity} item(s).` }
books.order = { amount, succeeded: `Successfully orderd ${amount} item(s).` }
} catch (e) {
books.order = { quantity, failed: e.response.data.error.message }
books.order = { amount, failed: e.response.data.error.message }
}
}

View File

@@ -34,7 +34,7 @@
<td>{{ book.author }}</td>
<td>{{ book.genre.name }}</td>
<td class="rating-stars">
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }} ({{ book.numberOfReviews }})
{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}
</td>
<td>{{ book.currency && book.currency.symbol }} {{ book.price }}</td>
</tr>
@@ -48,7 +48,7 @@
&nbsp;&nbsp; {{ book.stock }} in stock
</label>
<form @submit.prevent="submitOrder" style="float:right; display:flex; flex-direction:row-reverse">
<input type="number" v-model="order.quantity" v-bind:class="{ failed: order.failed }" style="width:5em">
<input type="number" v-model="order.amount" v-bind:class="{ failed: order.failed }" style="width:5em">
<input type="submit" value="Order:" class="muted-button">
</form>
<h4> {{ book.title }} </h4>

View File

@@ -3,4 +3,4 @@ ID;title;descr;author_ID;stock;price;currency_code;genre_ID
207;Jane Eyre;"Jane Eyre /ɛər/ (originally published as Jane Eyre: An Autobiography) is a novel by English writer Charlotte Brontë, published under the pen name ""Currer Bell"", on 16 October 1847, by Smith, Elder & Co. of London. The first American edition was published the following year by Harper & Brothers of New York. Primarily a bildungsroman, Jane Eyre follows the experiences of its eponymous heroine, including her growth to adulthood and her love for Mr. Rochester, the brooding master of Thornfield Hall. The novel revolutionised prose fiction in that the focus on Jane's moral and spiritual development is told through an intimate, first-person narrative, where actions and events are coloured by a psychological intensity. The book contains elements of social criticism, with a strong sense of Christian morality at its core and is considered by many to be ahead of its time because of Jane's individualistic character and how the novel approaches the topics of class, sexuality, religion and feminism.";107;11;12.34;GBP;11
251;The Raven;"""The Raven"" is a narrative poem by American writer Edgar Allan Poe. First published in January 1845, the poem is often noted for its musicality, stylized language, and supernatural atmosphere. It tells of a talking raven's mysterious visit to a distraught lover, tracing the man's slow fall into madness. The lover, often identified as being a student, is lamenting the loss of his love, Lenore. Sitting on a bust of Pallas, the raven seems to further distress the protagonist with its constant repetition of the word ""Nevermore"". The poem makes use of folk, mythological, religious, and classical references.";150;333;13.13;USD;16
252;Eleonora;"""Eleonora"" is a short story by Edgar Allan Poe, first published in 1842 in Philadelphia in the literary annual The Gift. It is often regarded as somewhat autobiographical and has a relatively ""happy"" ending.";150;555;14;USD;16
271;Catweazle;Catweazle is a British fantasy television series, starring Geoffrey Bayldon in the title role, and created by Richard Carpenter for London Weekend Television. The first series, produced and directed by Quentin Lawrence, was screened in the UK on ITV in 1970. The second series, directed by David Reid and David Lane, was shown in 1971. Each series had thirteen episodes, most but not all written by Carpenter, who also published two books based on the scripts.;170;22;150;JPY;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 ID title descr author_ID stock price currency_code genre_ID
3 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
4 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
5 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
6 271 Catweazle Catweazle is a British fantasy television series, starring Geoffrey Bayldon in the title role, and created by Richard Carpenter for London Weekend Television. The first series, produced and directed by Quentin Lawrence, was screened in the UK on ITV in 1970. The second series, directed by David Reid and David Lane, was shown in 1971. Each series had thirteen episodes, most but not all written by Carpenter, who also published two books based on the scripts. 170 22 150 15 JPY EUR 13

View File

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

View File

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

View File

@@ -1,5 +1,27 @@
using { sap.capire.bookshop as my } from '../db/schema';
service AdminService @(requires:'admin') {
entity Books as projection on my.Books;
entity Books as SELECT from my.Books { *,
// key ID, key validFrom
};
entity Authors as projection on my.Authors;
// @cds.redirection.target:false
// entity Books.history @(cds.temporal:false) as projection on Books {
// *,
// key ID,
// key validFrom @(cds.valid.from:false),
// validTo @(cds.valid.to:false),
// };
}
// entity NonTemporalBook as projection on my.Books {
// *,
// key ID,
// key validFrom @(cds.valid.from:false),
// validTo @(cds.valid.to:false),
// };
extend my.Books with {
history : Composition of many my.Books on history.ID = $self.ID;
}

View File

@@ -1,16 +1,14 @@
using { sap.capire.bookshop as my } from '../db/schema';
service CatalogService @(path:'/browse') {
/** For displaying lists of Books */
@readonly entity ListOfBooks as projection on Books
excluding { descr };
/** For display in details pages */
@readonly entity Books as projection on my.Books { *,
@readonly entity Books as SELECT from my.Books { *,
author.name as author
} excluding { createdBy, modifiedBy };
@readonly entity ListOfBooks as SELECT from Books
excluding { descr };
@requires: 'authenticated-user'
action submitOrder ( book: Books:ID, quantity: Integer ) returns { stock: Integer };
event OrderedBook : { book: Books:ID; quantity: Integer; buyer: String };
action submitOrder ( book: Books:ID, amount: Integer ) returns { stock: Integer };
event OrderedBook : { book: Books:ID; amount: Integer; buyer: String };
}

View File

@@ -1,18 +1,18 @@
const cds = require('@sap/cds')
const { Books } = cds.entities ('sap.capire.bookshop')
class CatalogService extends cds.ApplicationService { init(){
const { Books } = cds.entities ('sap.capire.bookshop')
// Reduce stock of ordered books if available stock suffices
this.on ('submitOrder', async req => {
const {book,quantity} = req.data
if (quantity < 1) return req.reject (400,`quantity has to be 1 or more`)
const {book,amount} = req.data
let {stock} = await SELECT `stock` .from (Books,book)
if (quantity > stock) return req.reject (409,`${quantity} exceeds stock for book #${book}`)
await UPDATE (Books,book) .with ({ stock: stock -= quantity })
await this.emit ('OrderedBook', { book, quantity, buyer:req.user.id })
if (stock >= amount) {
await UPDATE (Books,book) .with (`stock -=`, amount)
await this.emit ('OrderedBook', { book, amount, buyer:req.user.id })
return { stock }
}
else return req.error (409,`${amount} exceeds stock for book #${book}`)
})
// Add some discount for overstocked books

View File

@@ -32,19 +32,6 @@ GET {{server}}/admin/Authors?
# &sap-language=de
Authorization: Basic alice:
### ------------------------------------------------------------------------
# Create Author
POST {{server}}/admin/Authors
Content-Type: application/json;IEEE754Compatible=true
Authorization: Basic alice:
{
"ID": 112,
"name": "Shakespeeeeere",
"age": 22
}
### ------------------------------------------------------------------------
# Create book
POST {{server}}/admin/Books
@@ -84,7 +71,7 @@ POST {{server}}/browse/submitOrder
Content-Type: application/json
{{me}}
{ "book":201, "quantity":5 }
{ "book":201, "amount":5 }
### ------------------------------------------------------------------------

View File

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

View File

@@ -1,33 +0,0 @@
{
"name": "@capire/bookstore",
"version": "1.0.0",
"dependencies": {
"@capire/bookshop": "*",
"@capire/reviews": "*",
"@capire/orders": "*",
"@capire/common": "*",
"@sap/cds": "^5",
"express": "^4.17.1"
},
"cds": {
"requires": {
"ReviewsService": {
"kind": "odata",
"model": "@capire/reviews"
},
"OrdersService": {
"kind": "odata",
"model": "@capire/orders"
},
"messaging": {
"[development]": { "kind": "file-based-messaging" },
"[hybrid]": { "kind": "enterprise-messaging-shared" },
"[production]": { "kind": "enterprise-messaging" }
},
"db": {
"kind": "sql"
}
},
"log": { "service": true }
}
}

View File

@@ -1,21 +0,0 @@
const cds = require ('@sap/cds')
// Add mashup logic
cds.once('served', require('./srv/mashup'))
// Add routes to UIs from imported packages
cds.once('bootstrap',(app)=>{
app.serve ('/bookshop') .from ('@capire/bookshop','app/vue')
app.serve ('/reviews') .from ('@capire/reviews','app/vue')
app.serve ('/orders') .from('@capire/orders','app/orders')
})
// Add Swagger UI
require('./srv/swagger-ui')
// Returning cds.server
module.exports = cds.server
// For didactic reasons in capire
const { ReviewsService, OrdersService } = cds.requires
if (!ReviewsService.credentials && !OrdersService.credentials) cds.requires.messaging = false

View File

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

View File

@@ -5,11 +5,9 @@ CAD;de;Kanadischer Dollar;Kanadischer Dollar
AUD;de;Australischer Dollar;Australischer Dollar
GBP;de;Pfund;Britische Pfund
ILS;de;Schekel;Israelische Schekel
JPY;de;Yen;Japanische Yen
EUR;fr;euro;de la Zone euro
USD;fr;dollar;dollar des États-Unis
CAD;fr;dollar canadien;dollar canadien
AUD;fr;dollar australien;dollar australien
GBP;fr;livre sterling;pound sterling
ILS;fr;Shekel;shekel israelien
JPY;fr;Yen;Yen japonais
1 code locale name descr
5 AUD de Australischer Dollar Australischer Dollar
6 GBP de Pfund Britische Pfund
7 ILS de Schekel Israelische Schekel
JPY de Yen Japanische Yen
8 EUR fr euro de la Zone euro
9 USD fr dollar dollar des États-Unis
10 CAD fr dollar canadien dollar canadien
11 AUD fr dollar australien dollar australien
12 GBP fr livre sterling pound sterling
13 ILS fr Shekel shekel israelien
JPY fr Yen Yen japonais

2
fiori/.env Normal file
View File

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

View File

@@ -1,47 +0,0 @@
using {AdminService} from '@capire/bookshop';
annotate AdminService.Authors with @odata.draft.enabled;
////////////////////////////////////////////////////////////////////////////
//
// Authors Object Page
//
annotate AdminService.Authors with @(UI : {
HeaderInfo : {
TypeName : 'Author',
TypeNamePlural : 'Authors',
Description : {Value : lifetime}
},
Facets : [
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Details}',
Target : '@UI.FieldGroup#Details'
},
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Books}',
Target : 'books/@UI.LineItem'
},
],
FieldGroup #Details : {Data : [
{Value : placeOfBirth},
{Value : placeOfDeath},
{Value : dateOfBirth},
{Value : dateOfDeath},
{
Value : age,
Label : '{i18n>Age}'
},
]},
});
// Workaround to avoid errors for unknown db-specific calculated fields above
extend sap.capire.bookshop.Authors with {
virtual age : Integer;
virtual lifetime : String;
}
// Workaround for Fiori popup for asking user to enter a new UUID on Create
annotate AdminService.Authors with { ID @Core.Computed; }

View File

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

View File

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

View File

@@ -1,145 +0,0 @@
{
"_version": "1.28.0",
"sap.app": {
"id": "authors",
"type": "application",
"title": "Manage Authors",
"description": "Sample Application",
"i18n": "i18n/i18n.properties",
"applicationVersion": {
"version": "1.0.0"
},
"dataSources": {
"AdminService": {
"uri": "/admin/",
"type": "OData",
"settings": {
"odataVersion": "4.0"
}
}
},
"sourceTemplate": {
"id": "ui5template.basicSAPUI5ApplicationProject",
"-id": "ui5template.smartTemplate",
"version": "1.40.12"
},
"crossNavigation": {
"inbounds": {
"intent1": {
"signature": {
"parameters": {
"Books.author.ID":{
"renameTo": "ID"
}
},
"additionalParameters": "ignored"
},
"semanticObject": "Authors",
"action": "display",
"title": "{{appTitle}}",
"info": "{{appInfo}}",
"subTitle": "{{appSubTitle}}",
"icon": "sap-icon://SAP-icons-TNT/user",
"indicatorDataSource": {
"dataSource": "AdminService",
"path": "Authors/$count",
"refresh": 1800
}
}
}
}
},
"sap.ui5": {
"flexEnabled": true,
"config": {
"experimentalCAPScenario": true
},
"dependencies": {
"minUI5Version": "1.81.0",
"libs": {
"sap.fe.templates": {}
}
},
"models": {
"i18n": {
"type": "sap.ui.model.resource.ResourceModel",
"uri": "i18n/i18n.properties"
},
"": {
"dataSource": "AdminService",
"settings": {
"synchronizationMode": "None",
"operationMode": "Server",
"autoExpandSelect": true,
"earlyRequests": true,
"groupProperties": {
"default": {
"submit": "Auto"
}
}
}
}
},
"routing": {
"routes": [
{
"pattern": ":?query:",
"name": "AuthorsList",
"target": "AuthorsList"
},
{
"pattern": "Authors({key}):?query:",
"name": "AuthorsDetails",
"target": "AuthorsDetails"
}
],
"targets": {
"AuthorsList": {
"type": "Component",
"id": "AuthorsList",
"name": "sap.fe.templates.ListReport",
"options": {
"settings": {
"entitySet": "Authors",
"initialLoad": true,
"navigation": {
"Authors": {
"detail": {
"route": "AuthorsDetails"
}
}
}
}
}
},
"AuthorsDetails": {
"type": "Component",
"id": "AuthorsDetailsList",
"name": "sap.fe.templates.ObjectPage",
"options": {
"settings": {
"entitySet": "Authors"
}
}
}
}
},
"contentDensities": {
"compact": true,
"cozy": true
}
},
"sap.ui": {
"technology": "UI5",
"fullWidth": false,
"deviceTypes":{
"desktop": true,
"tablet": true,
"phone": true
}
},
"sap.fiori": {
"registrationIds": [],
"archeType": "transactional"
}
}

View File

@@ -10,20 +10,31 @@
<script>
window["sap-ushell-config"] = {
defaultRenderer: "fiori2",
applications: {},
bootstrapPlugins: {
RuntimeAuthoringPlugin: {
component: "sap.ushell.plugins.rta",
config: {
validateAppVersion: false,
},
},
PersonalizePlugin: {
component: "sap.ushell.plugins.rta-personalize",
config: {
validateAppVersion: false,
applications: {
"browse-books": {
title: "Browse Books",
description: "w/ SAP Fiori Elements",
additionalInformation: "SAPUI5.Component=bookshop",
applicationType : "URL",
url: "/browse/webapp",
navigationMode: "embedded"
},
"manage-books": {
title: "Manage Books",
description: "w/ SAP Fiori Elements",
additionalInformation: "SAPUI5.Component=admin",
applicationType : "URL",
url: "/admin/webapp",
navigationMode: "embedded"
},
"manage-orders": {
title: "Manage Orders",
description: "w/ SAP Fiori Elements",
additionalInformation: "SAPUI5.Component=orders",
applicationType : "URL",
url: "/orders/webapp",
navigationMode: "embedded"
}
}
};
</script>
@@ -36,12 +47,7 @@
data-sap-ui-frameOptions="allow"
></script>
<script>
sap.ui.getCore().attachInit(()=> sap.ushell.Container.createRenderer().placeAt("content"));
sap.ui
.getCore()
.getConfiguration()
.setFlexibilityServices([{ connector: "SessionStorageConnector" }]);
sap.ui.getCore().getConfiguration().setLanguage("en");
sap.ui.getCore().attachInit(()=> sap.ushell.Container.createRenderer().placeAt("content"))
</script>
</head>

View File

@@ -1,5 +1,4 @@
using { AdminService } from '@capire/bookstore';
using from '../common'; // to help UI linter get the complete annotations
using { AdminService } from '../../db/schema';
////////////////////////////////////////////////////////////////////////////
//
@@ -40,6 +39,27 @@ annotate AdminService.Books with @(
}
);
annotate AdminService.Authors with @(
UI: {
HeaderInfo: {
Description: {Value: lifetime}
},
Facets: [
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Books}', Target: 'books/@UI.LineItem'},
],
FieldGroup#Details: {
Data: [
{Value: placeOfBirth},
{Value: placeOfDeath},
{Value: dateOfBirth},
{Value: dateOfDeath},
{Value: age, Label: '{i18n>Age}'},
]
},
}
);
////////////////////////////////////////////////////////////
@@ -50,7 +70,7 @@ annotate AdminService.Books with @(
annotate sap.capire.bookshop.Books with @fiori.draft.enabled;
annotate AdminService.Books with @odata.draft.enabled;
annotate AdminService.Books.texts with @(
annotate AdminService.Books_texts with @(
UI: {
Identification: [{Value:title}],
SelectionFields: [ locale, title ],
@@ -63,15 +83,11 @@ annotate AdminService.Books.texts with @(
);
// Add Value Help for Locales
annotate AdminService.Books.texts {
locale @ValueList:{entity:'Languages'};
locale @Common.ValueListWithFixedValues:true; //show as drop down, not a dialog
annotate AdminService.Books_texts {
locale @ValueList:{entity:'Languages',type:#fixed}
}
// In addition we need to expose Languages through AdminService as a target for ValueList
// In addition we need to expose Languages through AdminService
using { sap } from '@sap/cds/common';
extend service AdminService {
@readonly entity Languages as projection on sap.common.Languages;
entity Languages as projection on sap.common.Languages;
}
// Workaround for Fiori popup for asking user to enter a new UUID on Create
annotate AdminService.Books with { ID @Core.Computed; }

View File

@@ -1,6 +1,6 @@
sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) {
"use strict";
return AppComponent.extend("books.Component", {
return AppComponent.extend("admin.Component", {
metadata: { manifest: "json" }
});
});

View File

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

View File

@@ -1,142 +0,0 @@
{
"services": {
"LaunchPage": {
"adapter": {
"config": {
"catalogs": [],
"groups": [
{
"id": "Bookshop",
"title": "Bookshop",
"isPreset": true,
"isVisible": true,
"isGroupLocked": false,
"tiles": [
{
"id": "BrowseBooks",
"tileType": "sap.ushell.ui.tile.StaticTile",
"properties": {
"title": "Browse Books",
"targetURL": "#Books-display"
}
}
]
},
{
"id": "Administration",
"title": "Administration",
"isPreset": true,
"isVisible": true,
"isGroupLocked": false,
"tiles": [
{
"id": "ManageBooks",
"tileType": "sap.ushell.ui.tile.StaticTile",
"properties": {
"title": "Manage Books",
"targetURL": "#Books-manage"
}
},
{
"id": "ManageAuthors",
"tileType": "sap.ushell.ui.tile.StaticTile",
"properties": {
"title": "Manage Authors",
"targetURL": "#Authors-display"
}
},
{
"id": "ManageOrders",
"tileType": "sap.ushell.ui.tile.StaticTile",
"properties": {
"title": "Manage Orders",
"targetURL": "#Orders-manage"
}
}
]
}
]
}
}
},
"NavTargetResolution": {
"config": {
"enableClientSideTargetResolution": true
}
},
"ClientSideTargetResolution": {
"adapter": {
"config": {
"inbounds": {
"BrowseBooks": {
"semanticObject": "Books",
"action": "display",
"title": "Browse Books",
"signature": {
"parameters": {
"Books.ID": {
"renameTo": "ID"
},
"Authors.books.ID": {
"renameTo": "ID"
}
},
"additionalParameters": "ignored"
},
"resolutionResult": {
"applicationType": "SAPUI5",
"additionalInformation": "SAPUI5.Component=bookshop",
"url": "/browse/webapp"
}
},
"BrowseAuthors": {
"semanticObject": "Authors",
"action": "display",
"title": "Browse Authors",
"signature": {
"parameters": {
"Books.author.ID":{
"renameTo": "ID"
}
},
"additionalParameters": "ignored"
},
"resolutionResult": {
"applicationType": "SAPUI5",
"additionalInformation": "SAPUI5.Component=authors",
"url": "/admin-authors/webapp"
}
},
"ManageBooks": {
"semanticObject": "Books",
"action": "manage",
"title": "Manage Books",
"signature": {
"parameters": {},
"additionalParameters": "allowed"
},
"resolutionResult": {
"applicationType": "SAPUI5",
"additionalInformation": "SAPUI5.Component=books",
"url": "/admin-books/webapp"
}
},
"ManageOrders": {
"semanticObject": "Orders",
"action": "manage",
"signature": {
"parameters": {},
"additionalParameters": "allowed"
},
"resolutionResult": {
"applicationType": "SAPUI5",
"additionalInformation": "SAPUI5.Component=orders",
"url": "/orders/webapp"
}
}
}
}
}
}
}
}

3
fiori/app/bookshop.html Normal file
View File

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

View File

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

View File

@@ -1,14 +1,11 @@
{
"_version": "1.28.0",
"_version": "1.8.0",
"sap.app": {
"id": "bookshop",
"type": "application",
"title": "Browse Books",
"description": "Sample Application",
"i18n": "i18n/i18n.properties",
"applicationVersion": {
"version": "1.0.0"
},
"dataSources": {
"CatalogService": {
"uri": "/browse/",
@@ -18,43 +15,14 @@
}
}
},
"sourceTemplate": {
"-sourceTemplate": {
"id": "ui5template.basicSAPUI5ApplicationProject",
"-id": "ui5template.smartTemplate",
"version": "1.40.12"
},
"crossNavigation": {
"inbounds": {
"intent1": {
"signature": {
"parameters": {
"Books.ID":{
"renameTo": "ID"
},
"Authors.books.ID": {
"renameTo": "ID"
}
},
"additionalParameters": "ignored"
},
"semanticObject": "Books",
"action": "display",
"title": "{{appTitle}}",
"info": "{{appInfo}}",
"subTitle": "{{appSubTitle}}",
"icon": "sap-icon://course-book",
"indicatorDataSource": {
"dataSource": "CatalogService",
"path": "Books/$count",
"refresh": 1800
}
}
}
"-version": "1.40.12"
}
},
"sap.ui5": {
"dependencies": {
"minUI5Version": "1.81.0",
"libs": {
"sap.fe.templates": {}
}
@@ -100,7 +68,6 @@
"options": {
"settings": {
"entitySet": "Books",
"initialLoad": true,
"navigation": {
"Books": {
"detail": {
@@ -130,12 +97,7 @@
},
"sap.ui": {
"technology": "UI5",
"fullWidth": false,
"deviceTypes":{
"desktop": true,
"tablet": true,
"phone": true
}
"fullWidth": false
},
"sap.fiori": {
"registrationIds": [],

View File

@@ -2,7 +2,7 @@
Common Annotations shared by all apps
*/
using { sap.capire.bookshop as my } from '@capire/bookstore';
using { sap.capire.bookshop as my } from '@capire/bookshop';
using { sap.common } from '@capire/common';
////////////////////////////////////////////////////////////////////////////
@@ -10,39 +10,21 @@ using { sap.common } from '@capire/common';
// Books Lists
//
annotate my.Books with @(
Common.SemanticKey : [ID],
Common.SemanticKey: [title],
UI: {
Identification: [{Value:title}],
SelectionFields : [
ID,
author_ID,
price,
currency_code
],
SelectionFields: [ ID, author_ID, price, currency_code ],
LineItem: [
{
Value : ID,
Label : '{i18n>Title}'
},
{
Value : author.ID,
Label : '{i18n>Author}'
},
{Value: ID},
{Value: title},
{Value: author.name, Label:'{i18n>Author}'},
{Value: genre.name},
{Value: stock},
{Value: price},
{
Value : currency.symbol,
Label : ' '
},
{Value: currency.symbol, Label:' '},
]
}
) {
ID @Common: {
SemanticObject : 'Books',
Text: title,
TextArrangement : #TextOnly
};
author @ValueList.entity:'Authors';
};
@@ -50,12 +32,17 @@ annotate my.Books with @(
//
// Books Details
//
annotate my.Books with @(UI : {HeaderInfo : {
annotate my.Books with @(
UI: {
HeaderInfo: {
TypeName: '{i18n>Book}',
TypeNamePlural: '{i18n>Books}',
Title: {Value: title},
Description: {Value: author.name}
}, });
},
}
);
////////////////////////////////////////////////////////////////////////////
@@ -63,17 +50,11 @@ annotate my.Books with @(UI : {HeaderInfo : {
// Books Elements
//
annotate my.Books with {
ID @title : '{i18n>ID}';
ID @title:'{i18n>ID}' @UI.HiddenFilter;
title @title:'{i18n>Title}';
genre @title : '{i18n>Genre}' @Common : {
Text : genre.name,
TextArrangement : #TextOnly
};
author @title : '{i18n>Author}' @Common : {
Text : author.name,
TextArrangement : #TextOnly
};
price @title : '{i18n>Price}' @Measures.ISOCurrency : currency_code;
genre @title:'{i18n>Genre}' @Common: { Text: genre.name, TextArrangement: #TextOnly };
author @title:'{i18n>Author}' @Common: { Text: author.name, TextArrangement: #TextOnly };
price @title:'{i18n>Price}';
stock @title:'{i18n>Stock}';
descr @UI.MultiLineText;
}
@@ -88,10 +69,7 @@ annotate my.Genres with @(
SelectionFields: [ name ],
LineItem:[
{Value: name},
{
Value : parent.name,
Label : 'Main Genre'
},
{Value: parent.name, Label: 'Main Genre'},
],
}
);
@@ -100,7 +78,8 @@ annotate my.Genres with @(
//
// Genre Details
//
annotate my.Genres with @(UI : {
annotate my.Genres with @(
UI: {
Identification: [{Value:name}],
HeaderInfo: {
TypeName: '{i18n>Genre}',
@@ -108,12 +87,11 @@ annotate my.Genres with @(UI : {
Title: {Value: name},
Description: {Value: ID}
},
Facets : [{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>SubGenres}',
Target : 'children/@UI.LineItem'
}, ],
});
Facets: [
{$Type: 'UI.ReferenceFacet', Label: '{i18n>SubGenres}', Target: 'children/@UI.LineItem'},
],
}
);
////////////////////////////////////////////////////////////////////////////
//
@@ -129,42 +107,38 @@ annotate my.Genres with {
// Authors List
//
annotate my.Authors with @(
Common.SemanticKey : [ID],
Common.SemanticKey: [name],
UI: {
Identification: [{Value:name}],
SelectionFields: [ name ],
LineItem:[
{Value: ID},
{Value: name},
{Value: dateOfBirth},
{Value: dateOfDeath},
{Value: placeOfBirth},
{Value: placeOfDeath},
],
}
) {
ID @Common: {
SemanticObject : 'Authors',
Text: name,
TextArrangement : #TextOnly,
};
};
);
////////////////////////////////////////////////////////////////////////////
//
// Author Details
//
annotate my.Authors with @(UI : {
annotate my.Authors with @(
UI: {
HeaderInfo: {
TypeName: '{i18n>Author}',
TypeNamePlural: '{i18n>Authors}',
Title: {Value: name},
Description: {Value: dateOfBirth}
},
Facets : [{
$Type : 'UI.ReferenceFacet',
Target : 'books/@UI.LineItem'
}, ],
});
Facets: [
{$Type: 'UI.ReferenceFacet', Target: 'books/@UI.LineItem'},
],
}
);
////////////////////////////////////////////////////////////////////////////
@@ -172,7 +146,7 @@ annotate my.Authors with @(UI : {
// Authors Elements
//
annotate my.Authors with {
ID @title : '{i18n>ID}';
ID @title:'{i18n>ID}' @UI.HiddenFilter;
name @title:'{i18n>Name}';
dateOfBirth @title:'{i18n>DateOfBirth}';
dateOfDeath @title:'{i18n>DateOfDeath}';
@@ -188,10 +162,7 @@ annotate common.Languages with @(
Common.SemanticKey: [code],
Identification: [{Value:code}],
UI: {
SelectionFields : [
name,
descr
],
SelectionFields: [ name, descr ],
LineItem:[
{Value: code},
{Value: name},
@@ -203,24 +174,26 @@ annotate common.Languages with @(
//
// Language Details
//
annotate common.Languages with @(UI : {
annotate common.Languages with @(
UI: {
HeaderInfo: {
TypeName: '{i18n>Language}',
TypeNamePlural: '{i18n>Languages}',
Title: {Value: name},
Description: {Value: descr}
},
Facets : [{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Details}',
Target : '@UI.FieldGroup#Details'
}, ],
FieldGroup #Details : {Data : [
Facets: [
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
],
FieldGroup#Details: {
Data: [
{Value: code},
{Value: name},
{Value: descr}
]},
});
]
},
}
);
////////////////////////////////////////////////////////////////////////////
//
@@ -230,10 +203,7 @@ annotate common.Currencies with @(
Common.SemanticKey: [code],
Identification: [{Value:code}],
UI: {
SelectionFields : [
name,
descr
],
SelectionFields: [ name, descr ],
LineItem:[
{Value: descr},
{Value: symbol},
@@ -246,7 +216,8 @@ annotate common.Currencies with @(
//
// Currency Details
//
annotate common.Currencies with @(UI : {
annotate common.Currencies with @(
UI: {
HeaderInfo: {
TypeName: '{i18n>Currency}',
TypeNamePlural: '{i18n>Currencies}',
@@ -254,29 +225,26 @@ annotate common.Currencies with @(UI : {
Description: {Value: code}
},
Facets: [
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Details}',
Target : '@UI.FieldGroup#Details'
},
{
$Type : 'UI.ReferenceFacet',
Label : '{i18n>Extended}',
Target : '@UI.FieldGroup#Extended'
},
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'},
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Extended}', Target: '@UI.FieldGroup#Extended'},
],
FieldGroup #Details : {Data : [
FieldGroup#Details: {
Data: [
{Value: name},
{Value: symbol},
{Value: code},
{Value: descr}
]},
FieldGroup #Extended : {Data : [
]
},
FieldGroup#Extended: {
Data: [
{Value: numcode},
{Value: minor},
{Value: exponent}
]},
});
]
},
}
);
////////////////////////////////////////////////////////////////////////////
//

3
fiori/app/reviews.html Normal file
View File

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

View File

@@ -2,8 +2,11 @@
This model controls what gets served to Fiori frontends...
*/
using from './admin-authors/fiori-service';
using from './admin-books/fiori-service';
using from './admin/fiori-service';
using from './browse/fiori-service';
using from './common';
using from '@capire/bookstore/srv/mashup';
using from '@capire/common';
// only works in case of embedded orders service
using from '@capire/orders/app/orders/fiori-service';

View File

@@ -2,7 +2,7 @@
// Add Author.age and .lifetime with a DB-specific function
//
using { AdminService } from '@capire/bookshop';
using { AdminService } from '../schema';
extend projection AdminService.Authors with {
YEARS_BETWEEN(dateOfBirth, dateOfDeath) as age: Integer,

8
fiori/db/schema.cds Normal file
View File

@@ -0,0 +1,8 @@
using { sap.capire.bookshop } from '@capire/bookshop';
// Forward-declare calculated fields to be filled in database-specific ways
// TODO find a better way to have 'default' fields that still can be overwritten.
extend bookshop.Authors with {
virtual age: Integer;
virtual lifetime: String;
}

View File

@@ -2,7 +2,7 @@
// Add Author.age and .lifetime with a DB-specific function
//
using { AdminService } from '@capire/bookshop';
using { AdminService } from '../schema';
extend projection AdminService.Authors with {
strftime('%Y',dateOfDeath)-strftime('%Y',dateOfBirth) as age: Integer,

View File

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

View File

@@ -1 +1,18 @@
module.exports = require('@capire/bookstore/server.js')
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)))

View File

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

View File

@@ -1,6 +1,6 @@
////////////////////////////////////////////////////////////////////////////
//
// Mashing up bookshop services with required services...
// Mashing up provided and required services...
//
module.exports = async()=>{ // called by server.js
@@ -20,29 +20,30 @@ module.exports = async()=>{ // called by server.js
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
console.debug ('> delegating request to ReviewsService')
const [id] = req.params, { columns, limit } = req.query.SELECT
return ReviewsService.read ('Reviews',columns).limit(limit).where({subject:String(id)})
return ReviewsService.tx(req).read ('Reviews',columns).limit(limit).where({subject:String(id)})
}))
//
// Create an order with the OrdersService when CatalogService signals a new order
//
CatalogService.on ('OrderedBook', async (msg) => {
const { book, quantity, buyer } = msg.data
const { book, amount, buyer } = msg.data
const { title, price } = await db.tx(msg).read (Books, book, b => { b.title, b.price })
return OrdersService.tx(msg).create ('Orders').entries({
OrderNo: 'Order at '+ (new Date).toLocaleString(),
Items: [{ product:{ID:`${book}`}, title, price, quantity }],
Items: [{ product:{ID:`${book}`}, title, price, amount }],
buyer, createdBy: buyer
})
})
//
// Update Books' average ratings when ReviewsService signals updated reviews
// Update Books' average ratings when ReviewsService signals updatd reviews
//
ReviewsService.on ('reviewed', (msg) => {
console.debug ('> received:', msg.event, msg.data)
const { subject, count, rating } = msg.data
return UPDATE(Books,subject).with({ numberOfReviews:count, rating })
const { subject, rating } = msg.data
return UPDATE(Books,subject).with({rating})
// ^ Note: the framework will execute this and take care for db.tx
})
//
@@ -50,9 +51,9 @@ module.exports = async()=>{ // called by server.js
//
OrdersService.on ('OrderChanged', (msg) => {
console.debug ('> received:', msg.event, msg.data)
const { product, deltaQuantity } = msg.data
const { product, deltaAmount } = msg.data
return UPDATE (Books) .where ('ID =', product)
.and ('stock >=', deltaQuantity)
.set ('stock -=', deltaQuantity)
.and ('stock >=', deltaAmount)
.set ('stock -=', deltaAmount)
})
}

View File

@@ -2,7 +2,7 @@
@bookshop = http://localhost:4004
@reviews-service = {{bookshop}}/reviews
# Uncomment this when running a separate reviews service
# @reviews-service = http://localhost:4005/reviews
@reviews-service = http://localhost:4005/reviews
@@ -38,11 +38,7 @@ GET {{bookshop}}/browse/Books(201)?
&$select=ID,title,rating
&$expand=reviews
###
GET {{bookshop}}/browse/Books?
&$select=title,author&$expand=currency
Accept-Language: de
#################################################
#

View File

@@ -1,15 +0,0 @@
# Hello World Getting Started Sample
## Next Steps
- To run the JavaScript implementation, open a new terminal and run `cds watch`.
- To run the TypeScript implementation, open a new terminal and run `cds-ts watch`.
Then call the service at: http://localhost:4004/say/hello(to='world')
## Learn More
Learn more about:
- [Hello World!](https://cap.cloud.sap/docs/get-started/hello-world)
- [Using TypeScript](https://cap.cloud.sap/docs/get-started/using-typescript)

View File

@@ -2,53 +2,6 @@
"name": "@capire/hello-world",
"version": "1.0.0",
"scripts": {
"test": "npx jest --silent",
"start": "cds serve srv/world.cds",
"start:ts": "cds-ts serve srv/world.cds"
},
"devDependencies": {
"@types/jest": "^27.0.2",
"@types/node": "^16.11.6",
"ts-jest": "^27.0.2",
"typescript": "^4.3.5"
},
"jest": {
"testEnvironment": "node",
"preset": "ts-jest",
"globals": {
"ts-jest": {
"diagnostics": {
"_comment": "see https://githubmemory.com/repo/kulshekhar/ts-jest/issues/2722",
"ignoreCodes": [
151001
]
}
}
}
},
"eslintConfig": {
"extends": "eslint:recommended",
"env": {
"es2020": true,
"node": true,
"jest": true,
"mocha": true
},
"globals": {
"SELECT": true,
"INSERT": true,
"UPDATE": true,
"DELETE": true,
"CREATE": true,
"DROP": true,
"CDL": true,
"CQL": true,
"CXL": true,
"cds": true
},
"rules": {
"no-console": "off",
"require-atomic-updates": "off"
}
"watch": "cds serve world.cds"
}
}

View File

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

View File

@@ -1,15 +0,0 @@
process.env.CDS_TYPESCRIPT = 'true';
import * as cds from '@sap/cds';
//@ts-ignore
const {GET} = cds.test.in(__dirname,'../srv').run('serve', 'world.cds');
describe('Hello world!', () => {
afterAll(() => { delete process.env.CDS_TYPESCRIPT; });
it('should say hello with class impl from a typescript file', async () => {
const {data} = await GET`/say/hello(to='world')`
expect(data.value).toMatch(/Hello world.*typescript.*/i)
})
})

5
orders/app/index.cds Normal file
View File

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

View File

@@ -10,7 +10,7 @@
using { OrdersService } from '../srv/orders-service';
using { OrdersService } from '../../srv/orders-service';
@odata.draft.enabled
@@ -68,16 +68,16 @@ annotate OrdersService.Orders with @(
annotate OrdersService.Orders.Items with @(
annotate OrdersService.Orders_Items with @(
UI: {
LineItem: [
{Value: product_ID, Label:'Product ID'},
{Value: title, Label:'Product Title'},
{Value: price, Label:'Unit Price'},
{Value: quantity, Label:'Quantity'},
{Value: amount, Label:'Quantity'},
],
Identification: [ //Is the main field group
{Value: quantity, Label:'Quantity'},
{Value: amount, Label:'Amount'},
{Value: title, Label:'Product'},
{Value: price, Label:'Unit Price'},
],
@@ -86,7 +86,7 @@ annotate OrdersService.Orders.Items with @(
],
},
) {
quantity @(
amount @(
Common.FieldControl: #Mandatory
);
};

View File

@@ -1,4 +1,4 @@
ID;up__ID;quantity;product_ID;title;price
ID;up__ID;amount;product_ID;title;price
58040e66-1dcd-4ffb-ab10-fdce32028b79;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;201;Wuthering Heights;11.11
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 ID up__ID quantity amount product_ID title price
2 58040e66-1dcd-4ffb-ab10-fdce32028b79 7e2f2640-6866-4dcf-8f4d-3027aa831cad 1 1 201 Wuthering Heights 11.11
3 64e718c9-ff99-47f1-8ca3-950c850777d4 7e2f2640-6866-4dcf-8f4d-3027aa831cad 1 1 271 Catweazle 15
4 e9641166-e050-4261-bfee-d1e797e6cb7f 64e718c9-ff99-47f1-8ca3-950c850777d4 2 2 252 Eleonora 28

View File

@@ -3,22 +3,21 @@ namespace sap.capire.orders;
entity Orders : cuid, managed {
OrderNo : String @title:'Order Number'; //> readable key
Items : Composition of many {
key ID : UUID;
product : Association to Products;
quantity : Integer;
title : String; //> intentionally replicated as snapshot from product.title
price : Double; //> materialized calculated field
};
Items : Composition of many Orders_Items on Items.up_ = $self;
buyer : User;
currency : Currency;
}
entity Orders_Items {
key ID : UUID;
up_ : Association to Orders;
product : Association to Products @assert.integrity:false; // REVISIT: this is a temporary workaround for a glitch in cds-runtime
amount : Integer;
title : String; //> intentionally replicated as snapshot from product.title
price : Double;
}
/** This is a stand-in for arbitrary ordered Products */
entity Products @(cds.persistence.skip:'always') {
key ID : String;
}
// this is to ensure we have filled-in currencies
using from '@capire/common';

View File

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

View File

@@ -3,34 +3,34 @@ class OrdersService extends cds.ApplicationService {
/** register custom handlers */
init(){
const { 'Orders.Items':OrderItems } = this.entities
const { Orders_Items:OrderItems } = this.entities
this.before ('UPDATE', 'Orders', async function(req) {
const { ID, Items } = req.data
if (Items) for (let { product_ID, quantity } of Items) {
const { quantity:before } = await cds.tx(req).run (
SELECT.one.from (OrderItems, oi => oi.quantity) .where ({up__ID:ID, product_ID})
if (Items) for (let { product_ID, amount } of Items) {
const { amount:before } = await cds.tx(req).run (
SELECT.one.from (OrderItems, oi => oi.amount) .where ({up__ID:ID, product_ID})
)
if (quantity != before) await this.orderChanged (product_ID, quantity-before)
if (amount != before) await this.orderChanged (product_ID, amount-before)
}
})
this.before ('DELETE', 'Orders', async function(req) {
const { ID } = req.data
const Items = await cds.tx(req).run (
SELECT.from (OrderItems, oi => { oi.product_ID, oi.quantity }) .where ({up__ID:ID})
SELECT.from (OrderItems, oi => { oi.product_ID, oi.amount }) .where ({up__ID:ID})
)
if (Items) await Promise.all (Items.map(it => this.orderChanged (it.product_ID, -it.quantity)))
if (Items) await Promise.all (Items.map(it => this.orderChanged (it.product_ID, -it.amount)))
})
return super.init()
}
/** order changed -> broadcast event */
orderChanged (product, deltaQuantity) {
orderChanged (product, deltaAmount) {
// Emit events to inform subscribers about changes in orders
console.log ('> emitting:', 'OrderChanged', { product, deltaQuantity })
return this.emit ('OrderChanged', { product, deltaQuantity })
console.log ('> emitting:', 'OrderChanged', { product, deltaAmount })
return this.emit ('OrderChanged', { product, deltaAmount })
}
}

13176
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,45 +5,36 @@
"repository": "https://github.com/sap-samples/cloud-cap-samples.git",
"author": "daniel.hutzel@sap.com",
"dependencies": {
"@capire/bookstore": "./bookstore",
"@capire/bookshop": "./bookshop",
"@capire/common": "./common",
"@capire/fiori": "./fiori",
"@capire/hello": "./hello",
"@capire/media": "./media",
"@capire/orders": "./orders",
"@capire/reviews": "./reviews",
"@sap/cds": "^5.5.3"
"@capire/reviews": "./reviews"
},
"devDependencies": {
"chai": "^4.3.4",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"chai-subset": "^1.6.0",
"sqlite3": "^5"
"sqlite3": "^5.0.0"
},
"scripts": {
"cleanup": "rm -rf node_modules && rm -rf */node_modules && rm -rf */*/node_modules",
"registry": "node .registry/server.js",
"bookshop": "cds watch bookshop",
"fiori": "cds watch fiori",
"hello": "cds watch hello",
"media": "cds watch media",
"mocha": "npx mocha || echo",
"jest": "npx jest",
"start": "cds watch fiori",
"test": "npm run jest -- --silent",
"test:hello": "cd hello && npm test"
},
"jest": {
"testEnvironment": "node",
"testTimeout": 20000,
"testMatch": [
"**/*.test.js"
]
"test": "npm run jest --silent"
},
"mocha": {
"parallel": true
},
"jest": {
"testEnvironment": "node"
},
"license": "SAP SAMPLE CODE LICENSE",
"private": true
}

View File

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

View File

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

View File

@@ -7,17 +7,18 @@
"index.cds"
],
"dependencies": {
"@sap/cds": "^5",
"@sap/cds": ">=4",
"express": "^4.17.1"
},
"scripts": {
"reviews-service": "cds watch",
"books-reviewed": "cds watch ../reviewed"
},
"cds": {
"requires": {
"messaging": {
"[development]": { "kind": "file-based-messaging" },
"[hybrid]": { "kind": "enterprise-messaging-shared" },
"[production]": { "kind": "enterprise-messaging" }
},
"db": { "kind": "sql" }
"db": {
"kind": "sql"
}
}
}
}

View File

@@ -10,8 +10,7 @@ service ReviewsService {
// Async API
event reviewed : {
subject: type of Reviews:subject;
count : Integer;
rating : Decimal;
rating: Decimal(2,1)
}
// Input validation
@@ -28,7 +27,14 @@ service ReviewsService {
annotate ReviewsService.Reviews with @restrict:[
{ grant:'READ', to:'any' }, // everybody can read 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' },
];

View File

@@ -12,11 +12,11 @@ module.exports = cds.service.impl (function(){
// Emit an event to inform subscribers about new avg ratings for reviewed subjects
this.after (['CREATE','UPDATE','DELETE'], 'Reviews', async function(_,req) {
const {subject} = req.data
const { count, rating } = await cds.tx(req) .run (
SELECT.one `round(avg(rating),2) as rating, count(*) as count` .from (Reviews) .where ({subject})
const {rating} = await cds.tx(req) .run (
SELECT.one (['round(avg(rating),2) as rating']) .from (Reviews) .where ({subject})
)
global.it || console.log ('< emitting:', 'reviewed', { subject, count, rating })
await this.emit ('reviewed', { subject, count, rating })
global.it || console.log ('< emitting:', 'reviewed', { subject, rating })
await this.emit ('reviewed', { subject, rating })
})
// Increment counter for reviews considered helpful

View File

@@ -7,7 +7,6 @@ Each sub directory essentially is an individual npm package arranged in an [all-
## [@capire/hello-world](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).
- [Typescript support](https://cap.cloud.sap/docs/get-started/using-typescript)
## [@capire/bookshop](bookshop)
@@ -51,27 +50,19 @@ Each sub directory essentially is an individual npm package arranged in an [all-
- As well as managed data, input validations, and authorization
## [@capire/bookstore](bookstore)
## [@capire/fiori](fiori)
- A [composite app, reusing and combining](https://cap.cloud.sap/docs/guides/verticalize) these packages:
- [@capire/bookshop](bookshop)
- [@capire/reviews](reviews)
- [@capire/orders](orders)
- [@capire/common](common)
- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well
- [The Vue.js app](reviews/app/vue) imported from reviews is served as well
- [The Fiori app](orders/app) imported from orders is served as well
- [OpenAPI export + Swagger UI](https://cap.cloud.sap/docs/advanced/openapi)
## [@capire/fiori](fiori)
- [Adds an SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookstore, thereby introducing to:
- [Adds an SAP Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookshop, thereby introducing to:
- [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files
- Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)
- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)
- Serving SAP Fiori apps locally
- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well
<br>

View File

@@ -1,267 +1,96 @@
const { expect } = require('../test')
const cds = require('@sap/cds/lib')
const { expect } = cds.test
const { cdr } = cds.ql
const CQL = ([cql]) => cds.parse.cql(cql)
const Foo = { name: 'Foo' }
const Books = { name: 'capire.bookshop.Books' }
const { parse:cdr } = cds.ql
const STAR = cdr ? '*' : { ref: ['*'] }
const skip = {to:{eql:()=>skip}}
const srv = new cds.Service
let cqn
expect.plain = (cqn) => !cqn.SELECT.one && !cqn.SELECT.distinct ? expect(cqn) : skip
expect.one = (cqn) => !cqn.SELECT.distinct ? expect(cqn) : skip
// while jest has 'test' as alias to 'it', mocha doesn't
if (!global.test) global.test = it
describe('cds.ql → cqn', () => {
//
let cqn
for (let each of ['SELECT', 'SELECT one', 'SELECT distinct']) {
let SELECT; beforeEach(()=> SELECT = (
each === 'SELECT distinct' ? cds.ql.SELECT.distinct :
each === 'SELECT one' ? cds.ql.SELECT.one :
cds.ql.SELECT
))
describe(`${each}...`, () => {
describe.skip(`BUGS + GAPS...`, () => {
test(`from Foo`, () => {
expect(cqn = SELECT `from Foo`)
.to.eql(SELECT.from `Foo`)
.to.eql(SELECT.from('Foo'))
.to.eql(SELECT.from(Foo))
.to.eql(SELECT`Foo`)
.to.eql(SELECT('Foo'))
.to.eql(SELECT(Foo))
expect.plain(cqn)
.to.eql(CQL`SELECT from Foo`)
.to.eql(srv.read `Foo`)
.to.eql(srv.read('Foo'))
.to.eql(srv.read(Foo))
.to.eql({
SELECT: { from: { ref: ['Foo'] } },
})
})
if (each === 'SELECT')
test('SELECT ( Foo )', () => {
it('should consistently handle *', () => {
expect({
SELECT: { from: { ref: ['Foo'] } },
SELECT: { from: { ref: ['Foo'] }, columns: ['*'] },
})
.to.eql(CQL`SELECT from Foo`)
.to.eql(SELECT(Foo))
.to.eql(CQL`SELECT * from Foo`)
.to.eql(CQL`SELECT from Foo{*}`)
.to.eql(SELECT('*').from(Foo))
.to.eql(SELECT.from(Foo,['*']))
})
if (each === 'SELECT')
test('SELECT ( Foo ) .from ( Bar )', () => {
expect({
SELECT: { columns:[{ref:['Foo']}], from: { ref: ['Bar'] } },
it('should consistently handle lists', () => {
const ID = 11, args = [`foo`, "'bar'", 3]
const cqn = CQL`SELECT from Foo where ID=11 and x in (foo,'bar',3)`
expect(SELECT.from(Foo).where(`ID=${ID} and x in (${args})`)).to.eql(cqn)
expect(SELECT.from(Foo).where(`ID=`, ID, `and x in`, args)).to.eql(cqn)
expect(SELECT.from(Foo).where({ ID, x:args })).to.eql(cqn)
})
.to.eql(CQL`SELECT Foo from Bar`)
.to.eql(SELECT `Foo` .from `Bar`)
.to.eql(SELECT `Foo` .from('Bar'))
.to.eql(SELECT('Foo').from('Bar'))
.to.eql(SELECT(['Foo']).from('Bar'))
.to.eql(SELECT(['Foo']).from('Bar'))
.to.eql(SELECT `Bar` .columns `Foo`)
.to.eql(SELECT `Bar` .columns ('Foo'))
.to.eql(SELECT `Bar` .columns (['Foo']))
.to.eql(SELECT.from `Bar` .columns ('Foo'))
.to.eql(SELECT.from `Bar` .columns (['Foo']))
expect({
SELECT: { columns:[
{ref:['Foo']},
{ref:['Boo']},
], from: { ref: ['Bar'] } },
})
.to.eql(CQL`SELECT Foo, Boo from Bar`)
.to.eql(SELECT `Foo, Boo` .from `Bar`)
.to.eql(SELECT `Foo, Boo` .from('Bar'))
.to.eql(SELECT('Foo','Boo').from('Bar'))
.to.eql(SELECT(['Foo','Boo']).from('Bar'))
.to.eql(SELECT `Bar` .columns `Foo, Boo`)
.to.eql(SELECT `Bar` .columns ('Foo','Boo'))
.to.eql(SELECT `Bar` .columns (['Foo','Boo']))
.to.eql(SELECT.from `Bar` .columns ('Foo','Boo'))
.to.eql(SELECT.from `Bar` .columns (['Foo','Boo']))
expect({
SELECT: { columns:[
{ref:['Foo']},
{ref:['Boo']},
{ref:['Moo']},
], from: { ref: ['Bar'] } },
})
.to.eql(CQL`SELECT Foo, Boo, Moo from Bar`)
.to.eql(SELECT `Foo, Boo, Moo` .from `Bar`)
.to.eql(SELECT `Foo, Boo, Moo` .from('Bar'))
.to.eql(SELECT('Foo','Boo','Moo').from('Bar'))
.to.eql(SELECT(['Foo','Boo','Moo']).from('Bar'))
.to.eql(SELECT `Bar` .columns `Foo, Boo, Moo`)
.to.eql(SELECT `Bar` .columns ('Foo','Boo','Moo'))
.to.eql(SELECT `Bar` .columns (['Foo','Boo','Moo']))
.to.eql(SELECT.from `Bar` .columns ('Foo','Boo','Moo'))
.to.eql(SELECT.from `Bar` .columns (['Foo','Boo','Moo']))
expect({
SELECT: { one:true, columns:[{ref:['Foo']}], from: { ref: ['Bar'] } },
})
// .to.eql(CQL`SELECT one Foo from Bar`)
.to.eql(SELECT.one `Foo` .from `Bar`)
.to.eql(SELECT.one `Foo` .from('Bar'))
.to.eql(SELECT.one('Foo').from('Bar'))
.to.eql(SELECT.one(['Foo']).from('Bar'))
.to.eql(SELECT.one(['Foo']).from('Bar'))
.to.eql(SELECT.one('Bar',['Foo']))
.to.eql(SELECT.one `Bar` .columns `Foo`)
.to.eql(SELECT.one('Bar').columns('Foo'))
.to.eql(SELECT.one('Bar').columns(['Foo']))
.to.eql(SELECT.one.from('Bar',['Foo']))
.to.eql(SELECT.one.from('Bar').columns('Foo'))
.to.eql(SELECT.one.from('Bar').columns(['Foo']))
expect({
SELECT: { one:true, columns:[
{ref:['Foo']},
{ref:['Boo']},
], from: { ref: ['Bar'] } },
})
// .to.eql(CQL`SELECT Foo, Boo from Bar`)
.to.eql(SELECT.one `Foo, Boo` .from `Bar`)
.to.eql(SELECT.one `Foo, Boo` .from('Bar'))
.to.eql(SELECT.one('Foo','Boo').from('Bar'))
.to.eql(SELECT.one(['Foo','Boo']).from('Bar'))
.to.eql(SELECT.one('Bar',['Foo','Boo']))
.to.eql(SELECT.one `Bar` .columns `Foo, Boo`)
.to.eql(SELECT.one('Bar').columns('Foo','Boo'))
.to.eql(SELECT.one('Bar').columns(['Foo','Boo']))
.to.eql(SELECT.one.from('Bar',['Foo','Boo']))
.to.eql(SELECT.one.from('Bar').columns('Foo','Boo'))
.to.eql(SELECT.one.from('Bar').columns(['Foo','Boo']))
expect({
SELECT: { one:true, columns:[
{ref:['Foo']},
{ref:['Boo']},
{ref:['Moo']},
], from: { ref: ['Bar'] } },
})
// .to.eql(CQL`SELECT Foo, Boo, Moo from Bar`)
.to.eql(SELECT.one `Foo, Boo, Moo` .from `Bar`)
.to.eql(SELECT.one `Foo, Boo, Moo` .from('Bar'))
.to.eql(SELECT.one('Foo','Boo','Moo').from('Bar'))
.to.eql(SELECT.one(['Foo','Boo','Moo']).from('Bar'))
.to.eql(SELECT.one('Bar',['Foo','Boo','Moo']))
.to.eql(SELECT.one `Bar` .columns `Foo, Boo, Moo`)
.to.eql(SELECT.one('Bar').columns('Foo','Boo','Moo'))
.to.eql(SELECT.one('Bar').columns(['Foo','Boo','Moo']))
.to.eql(SELECT.one.from('Bar',['Foo','Boo','Moo']))
.to.eql(SELECT.one.from('Bar').columns('Foo','Boo','Moo'))
.to.eql(SELECT.one.from('Bar').columns(['Foo','Boo','Moo']))
})
if (each === 'SELECT')
describe(`SELECT...`, () => {
test('from ( Foo )', () => {
expect({
SELECT: { from: {ref: [{ id:'Foo', where: [{val:11}] }] }}
SELECT: { from: { ref: ['Foo'] } },
})
.to.eql(CQL`SELECT from Foo`)
.to.eql(SELECT.from(Foo))
})
.to.eql(srv.read`Foo[${11}]`)
.to.eql(SELECT`Foo[${11}]`)
expect((cqn = SELECT`from Foo[ID=11]`))
.to.eql(SELECT`from Foo[ID=${11}]`)
.to.eql(SELECT.from `Foo[ID=11]`)
.to.eql(SELECT.from `Foo[ID=${11}]`)
.to.eql(SELECT`Foo[ID=11]`)
expect.plain(cqn)
.to.eql(CQL`SELECT from Foo[ID=11]`)
.to.eql(srv.read`Foo[ID=11]`)
.to.eql({
SELECT: { from: {
test('from ( ..., <key>)', () => {
// Compiler
expect(CQL`SELECT from Foo[11]`).to.eql({
SELECT: {
// REVISIT: add one:true?
from: { ref: [{ id: 'Foo', where: [{ val: 11 }] }] },
},
})
expect(CQL`SELECT from Foo[ID=11]`).to.eql({
SELECT: {
// REVISIT: add one:true
from: {
ref: [{ id: 'Foo', where: [{ ref: ['ID'] }, '=', { val: 11 }] }],
}},
},
},
})
if (cdr) expect.plain (cqn)
.to.eql(SELECT`Foo[ID=${11}]`)
.to.eql(srv.read`Foo[ID=${11}]`)
// Following implicitly resolve to SELECT.one
expect(cqn = SELECT.from(Foo,11))
// Runtime ds.ql
expect(SELECT.from(Foo, 11))
.to.eql(SELECT.from(Foo, { ID: 11 }))
.to.eql(SELECT.from(Foo).byKey(11))
.to.eql(SELECT.from(Foo).byKey({ ID: 11 }))
if (cds.version >= '5.6.0') {
expect.one(cqn)
.to.eql({
SELECT: {
one: true,
from: { ref: [{ id: 'Foo', where: [{ ref: ['ID'] }, '=', { val: 11 }] }] },
},
})
} else {
expect.one(cqn)
.to.eql(SELECT.one.from(Foo).where({ ID: 11 }))
.to.eql({
// REVISIT: should produce CQN as the ones above?
SELECT: {
one: true,
from: { ref: ['Foo'] },
where: [{ ref: ['ID'] }, '=', { val: 11 }],
},
})
}
})
test('from Foo {...}', () => {
expect(cqn = SELECT `*,a,b as c` .from `Foo`)
.to.eql(SELECT `*,a,b as c`. from(Foo))
.to.eql(SELECT('*','a',{b:'c'}).from`Foo`)
.to.eql(SELECT('*','a',{b:'c'}).from(Foo))
.to.eql(SELECT(['*','a',{b:'c'}]).from(Foo))
.to.eql(SELECT.columns('*','a',{b:'c'}).from(Foo))
.to.eql(SELECT.columns(['*','a',{b:'c'}]).from(Foo))
.to.eql(SELECT.columns((foo) => { foo`.*`, foo.a, foo.b`as c` }).from(Foo))
.to.eql(SELECT.columns((foo) => { foo('*'), foo.a, foo.b.as('c') }).from(Foo))
.to.eql(SELECT.from(Foo).columns('*','a',{b:'c'}))
.to.eql(SELECT.from(Foo).columns(['*','a',{b:'c'}]))
.to.eql(SELECT.from(Foo).columns((foo) => { foo`.*`, foo.a, foo.b`as c` }))
.to.eql(SELECT.from(Foo).columns((foo) => { foo('*'), foo.a, foo.b.as('c') }))
.to.eql(SELECT.from(Foo,['*','a',{b:'c'}]))
.to.eql(SELECT.from(Foo, (foo) => { foo`.*`, foo.a, foo.b`as c` }))
.to.eql(SELECT.from(Foo, (foo) => { foo('*'), foo.a, foo.b.as('c') }))
expect.plain(cqn)
.to.eql({
expect(CQL`SELECT from Foo[11]{a}`).to.eql({
SELECT: {
from: { ref: ['Foo'] },
columns: [ STAR, { ref: ['a'] }, { ref: ['b'], as: 'c' }],
// REVISIT: add one:true?
from: { ref: [{ id: 'Foo', where: [{ val: 11 }] }] },
columns: [{ ref: ['a'] }],
},
})
cdr && expect.plain(cqn)
.to.eql(CQL`SELECT *,a,b as c from Foo`)
.to.eql(CQL`SELECT from Foo {*,a,b as c}`)
// Test combination with key as second argument to .from
expect(cqn = SELECT.from(Foo, 11, ['a']))
.to.eql(SELECT.from(Foo, 11, foo => foo.a))
if (cds.version >= '5.6.0') {
expect.one(cqn)
.to.eql({
SELECT: {
one: true,
from: { ref: [{ id: 'Foo', where: [{ ref: ['ID'] }, '=', { val: 11 }]}] },
columns: [{ ref: ['a'] }]
},
})
} else {
expect.one(cqn)
expect(SELECT.from(Foo, 11, ['a']))
.to.eql(SELECT.from(Foo, 11, (foo) => foo.a))
.to.eql({
// REVISIT: should produce CQN as the ones above?
SELECT: {
one: true,
from: { ref: ['Foo'] },
@@ -269,36 +98,91 @@ describe('cds.ql → cqn', () => {
where: [{ ref: ['ID'] }, '=', { val: 11 }],
},
})
}
})
test('with nested expands', () => {
// SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } }
expect(cqn =
SELECT.from (Foo, foo => {
foo`*`, foo.x, foo.car`*`, foo.boo (b => {
b`*`, b.moo.zoo(
x => x.y.z
)
test('from ( ..., => {...})', () => {
// single *, prefix and postfix, as array and function
let parsed, fluid
expect((parsed = CQL`SELECT * from Foo`)).to.eql(CQL`SELECT from Foo{*}`)
//> .to.eql... FIXME: see skipped 'should handle * correctly' below
expect((fluid = SELECT('*').from(Foo)))
.to.eql(SELECT.from(Foo, ['*']))
.to.eql(SELECT.from(Foo, (foo) => foo('*')))
.to.eql(SELECT.from(Foo).columns('*'))
.to.eql(SELECT.from(Foo).columns((foo) => foo('*')))
.to.eql({
SELECT: { from: { ref: ['Foo'] }, columns: [cdr ? '*' : { ref: ['*'] }] },
})
})
).to.eql(
SELECT.from (Foo, foo => {
foo('*'), foo.x, foo.car('*'), foo.boo (b => {
b('*'), b.moo.zoo(
x => x.y.z
)
})
})
)
expect.plain(cqn)
if (cdr) expect(parsed).to.eql(fluid)
// single column, prefix and postfix, as array and function
expect(CQL`SELECT a from Foo`)
expect(CQL`SELECT from Foo {a}`)
.to.eql(SELECT.from(Foo, ['a']))
.to.eql(SELECT.from(Foo, (foo) => foo.a))
.to.eql({
SELECT: { from: { ref: ['Foo'] }, columns: [{ ref: ['a'] }] },
})
// multiple columns, prefix and postfix, as array and function
expect(CQL`SELECT a,b as c from Foo`)
expect (CQL`SELECT from Foo {a,b as c}`).to.eql(cqn = {
SELECT: {
from: { ref: ['Foo'] },
columns: [{ ref: ['a'] }, { ref: ['b'], as: 'c' }],
},
})
expect(SELECT.from(Foo, ['a', { b: 'c' }])).to.eql(cqn)
expect(
SELECT.from(Foo, (foo) => {
foo.a, foo.b.as('c')
})
).to.eql(cqn)
expect(SELECT.from(Foo).columns('a', { b: 'c' })).to.eql(cqn)
expect(SELECT.from(Foo).columns(['a', { b: 'c' }])).to.eql(cqn)
expect(
SELECT.from(Foo).columns((foo) => {
foo.a, foo.b.as('c')
})
).to.eql(cqn)
// multiple columns and *, prefix and postfix, as array and function
expect(CQL`SELECT *,a,b from Foo`).to.eql(CQL`SELECT from Foo{*,a,b}`)
//> .to.eql... FIXME: see skipped 'should handle * correctly' below
expect(SELECT.from(Foo, ['a', 'b', '*']))
.to.eql(SELECT.from(Foo).columns('a', 'b', '*'))
.to.eql(SELECT.from(Foo).columns(['a', 'b', '*']))
.to.eql(
SELECT.from(Foo, (foo) => {
foo.a, foo.b, foo('*')
})
)
.to.eql({
SELECT: {
from: { ref: ['Foo'] },
columns: [{ ref: ['a'] }, { ref: ['b'] }, cdr ? '*' : { ref: ['*'] }],
},
})
})
test('from ( ..., => _.expand ( x=>{...}))', () => {
// SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } }
expect(
SELECT.from(Foo, (foo) => {
foo('*'),
foo.x,
foo.car('*'),
foo.boo((b) => {
b('*'), b.moo.zoo((x) => x.y.z)
})
})
).to.eql({
SELECT: {
from: { ref: ['Foo'] },
columns: [
STAR,
cdr ? '*' : { ref: ['*'] },
{ ref: ['x'] },
{ ref: ['car'], expand: ['*'] },
{
@@ -310,15 +194,14 @@ describe('cds.ql → cqn', () => {
})
})
test('with nested inlines', () => {
test('from ( ..., => _.inline ( _=>{...}))', () => {
// SELECT from Foo { *, x, bar.*, car{*}, boo { *, moo.zoo } }
expect.plain(
SELECT.from (Foo, foo => {
foo.bar `*`,
foo.bar `.*`, //> leading dot indicates inline
foo.boo(_ => _.moo.zoo), //> underscore arg name indicates inline
foo.boo(x => x.moo.zoo)
expect(
SELECT.from(Foo, (foo) => {
foo.bar('*'),
foo.bar('.*'), //> leading dot indicates inline
foo.boo((x) => x.moo.zoo),
foo.boo((_) => _.moo.zoo) //> underscore arg name indicates inline
})
).to.eql({
SELECT: {
@@ -326,44 +209,49 @@ describe('cds.ql → cqn', () => {
columns: [
{ ref: ['bar'], expand: ['*'] },
{ ref: ['bar'], inline: ['*'] },
{ ref: ['boo'], inline: [{ ref: ['moo', 'zoo'] }] },
{ ref: ['boo'], expand: [{ ref: ['moo', 'zoo'] }] },
{ ref: ['boo'], inline: [{ ref: ['moo', 'zoo'] }] },
],
},
})
})
})}
test('one / distinct ...', () => {
expect(SELECT.distinct.from(Foo).SELECT)
// .to.eql(CQL(`SELECT distinct from Foo`).SELECT)
.to.eql(SELECT.distinct(Foo).SELECT)
.to.eql({ distinct: true, from: { ref: ['Foo'] } })
describe ('SELECT where...', ()=>{
expect(SELECT.one.from(Foo).SELECT)
// .to.eql(CQL(`SELECT one from Foo`).SELECT)
.to.eql(SELECT.one(Foo).SELECT)
.to.eql({ one: true, from: { ref: ['Foo'] } })
expect(SELECT.one('a').from(Foo).SELECT)
// .to.eql(CQL(`SELECT distinct a from Foo`).SELECT)
.to.eql(SELECT.one(['a']).from(Foo).SELECT)
.to.eql(SELECT.one(Foo, ['a']).SELECT)
.to.eql(SELECT.one(Foo, (foo) => foo.a).SELECT)
.to.eql(SELECT.one.from(Foo, (foo) => foo.a).SELECT)
.to.eql(SELECT.one.from(Foo, ['a']).SELECT)
.to.eql({
one: true,
from: { ref: ['Foo'] },
columns: [{ ref: ['a'] }],
})
// same for works distinct
})
it('should correctly handle { ... and:{...} }', () => {
expect(SELECT.from(Foo).where({ x: 1, and: { y: 2, or: { z: 3 } } })).to.eql({
SELECT: {
from: { ref: ['Foo'] },
where: cdr ? [
{ ref: ['x'] },
'=',
{ val: 1 },
'and',
// '(',
{xpr:[
{ ref: ['y'] },
'=',
{ val: 2 },
'or',
{ ref: ['z'] },
'=',
{ val: 3 },
]},
// ')',
] : [
where: [
{ ref: ['x'] },
'=',
{ val: 1 },
'and',
'(',
// {xpr:[
{ ref: ['y'] },
'=',
{ val: 2 },
@@ -371,97 +259,12 @@ describe('cds.ql → cqn', () => {
{ ref: ['z'] },
'=',
{ val: 3 },
// ]},
')',
],
},
})
})
test ("where x='*'", ()=>{
if (cdr)
expect (SELECT.from(Foo).where({x:'*'}))
.to.eql(SELECT.from(Foo).where("x='*'"))
.to.eql(SELECT.from(Foo).where("x=",'*'))
.to.eql(SELECT.from(Foo).where`x=${'*'}`)
.to.eql(
CQL`SELECT from Foo where x='*'`
)
if (cdr)
expect (SELECT.from(Foo).where({x:['*',1]}))
.to.eql(SELECT.from(Foo).where("x in ('*',1)"))
.to.eql(SELECT.from(Foo).where("x in",['*',1]))
.to.eql(SELECT.from(Foo).where`x in ${['*',1]}`)
.to.eql(
CQL`SELECT from Foo where x in ('*',1)`
)
})
test ('where, and, or', ()=>{
expect (
SELECT.from(Foo).where({x:1,and:{y:2}})
).to.eql (
CQL`SELECT from Foo where x=1 and y=2`
) .to.eql ({ SELECT: {
from: {ref:['Foo']},
where: [
{ref:['x']}, '=', {val:1},
'and',
{ref:['y']}, '=', {val:2}
]
}})
expect (
SELECT.from(Foo).where({x:1,or:{y:2}})
).to.eql (
CQL`SELECT from Foo where x=1 or y=2`
).to.eql ({ SELECT: {
from: {ref:['Foo']},
where: [
{ref:['x']}, '=', {val:1},
'or',
{ref:['y']}, '=', {val:2}
]
}})
expect (
SELECT.from(Foo).where({x:1,and:{y:2}}).or({z:3})
).to.eql (
CQL`SELECT from Foo where x=1 and y=2 or z=3`
)
if (cdr) expect (
SELECT.from(Foo).where({x:1}).and({y:2,or:{z:3}})
).to.eql (
CQL`SELECT from Foo where x=1 and ( y=2 or z=3 )`
)
if (cdr) expect (
SELECT.from(Foo).where({1:1}).and({x:1,or:{x:2}}).and({y:2,or:{z:3}})
).to.eql (
CQL`SELECT from Foo where 1=1 and ( x=1 or x=2 ) and ( y=2 or z=3 )`
)
if (cdr) expect (
SELECT.from(Foo).where({x:1,or:{x:2}}).and({y:2,or:{z:3}})
).to.eql (
CQL`SELECT from Foo where ( x=1 or x=2 ) and ( y=2 or z=3 )`
)
})
test('where ({x:[undefined]})', () => {
if (cdr) expect (
SELECT.from(Foo).where({x:[undefined]})
).to.eql ({ SELECT: {
from: {ref:['Foo']},
where: [
{ref:['x']},
'in',
{ list: [ {val:undefined} ] }
]
}})
})
test('where ( ... cql | {x:y} )', () => {
const args = [`foo`, "'bar'", 3]
const ID = 11
@@ -477,17 +280,18 @@ describe('cds.ql → cqn', () => {
).to.eql({
SELECT: {
from: { ref: ['Foo'] },
where: cdr ? [
where: cdr
? [
// '(', //> this one is not required
{ ref: ['ID'] },
'=',
{ val: ID },
'and',
{ ref: ['args'] },
'in',
{ list: args.map(val => ({ val })) },
{ val: args },
'and',
{
xpr: [
'(', //> this one is missing, and that's changing the logic -> that's a BUG
{ ref: ['x'] },
'like',
{ val: '%x%' },
@@ -495,18 +299,19 @@ describe('cds.ql → cqn', () => {
{ ref: ['y'] },
'>=',
{ val: 9 },
')',
]
},
] : [
: [
// '(', //> this one is not required
{ ref: ['ID'] },
'=',
{ val: ID },
'and',
{ ref: ['args'] },
'in',
{ list: args.map(val => ({ val })) },
{ val: args },
'and',
'(',
'(', //> this one is missing, and that's changing the logic -> that's a BUG
{ ref: ['x'] },
'like',
{ val: '%x%' },
@@ -516,7 +321,7 @@ describe('cds.ql → cqn', () => {
{ val: 9 },
')',
],
}
},
})
// using CQL fragments -> uses cds.parse.expr
@@ -601,32 +406,12 @@ describe('cds.ql → cqn', () => {
).to.eql(cqn)
})
test('w/ plain SQL', () => {
it('w/ plain SQL', () => {
expect(SELECT.from(Books) + 'WHERE ...').to.eql(
'SELECT * FROM capire_bookshop_Books WHERE ...'
)
})
it('should consistently handle *', () => {
if (!cdr) return
expect({
SELECT: { from: { ref: ['Foo'] }, columns: ['*'] },
})
.to.eql(CQL`SELECT * from Foo`)
.to.eql(CQL`SELECT from Foo{*}`)
.to.eql(SELECT('*').from(Foo))
.to.eql(SELECT.from(Foo,['*']))
})
it('should consistently handle lists', () => {
if (!cdr) return
const ID = 11, args = [{ref:['foo']}, "bar", 3]
const cqn = CQL`SELECT from Foo where ID=11 and x in (foo,'bar',3)`
expect(SELECT.from(Foo).where`ID=${ID} and x in ${args}`).to.eql(cqn)
expect(SELECT.from(Foo).where(`ID=`, ID, `and x in`, args)).to.eql(cqn)
expect(SELECT.from(Foo).where({ ID, x:args })).to.eql(cqn)
})
//
})
@@ -681,31 +466,21 @@ describe('cds.ql → cqn', () => {
describe(`UPDATE...`, () => {
test('entity (..., <key>)', () => {
const cqnWhere = {
UPDATE: {
entity: 'capire.bookshop.Books',
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
},
}
expect(UPDATE(Books).where({ ID: 4711 }))
.to.eql(UPDATE(Books).where(`ID=`, 4711))
.to.eql(cqnWhere)
const cqnKey = (cds.version >= '5.6.0') ?
{
UPDATE: {
entity: { ref: [{ id: 'capire.bookshop.Books', where: [{ ref: ['ID'] }, '=', { val: 4711 }] }] }
}
}
: cqnWhere
expect(UPDATE(Books, 4711))
.to.eql(UPDATE(Books, { ID: 4711 }))
.to.eql(UPDATE(Books).byKey(4711))
.to.eql(UPDATE(Books).byKey({ ID: 4711 }))
.to.eql(UPDATE(Books).where({ ID: 4711 }))
.to.eql(UPDATE(Books).where(`ID=`, 4711))
.to.eql(UPDATE.entity(Books, 4711))
.to.eql(UPDATE.entity(Books, { ID: 4711 }))
// etc...
.to.eql(cqnKey)
.to.eql({
UPDATE: {
entity: 'capire.bookshop.Books',
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
},
})
})
/*
@@ -756,29 +531,20 @@ describe('cds.ql → cqn', () => {
describe(`DELETE...`, () => {
test('from (..., <key>)', () => {
const cqnWhere = {
DELETE: {
from: 'capire.bookshop.Books',
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
},
}
expect(DELETE.from(Books).where({ ID: 4711 }))
.to.eql(DELETE.from(Books).where(`ID=`, 4711))
.to.eql(cqnWhere)
const cqnKey = (cds.version >= '5.6.0') ?
{
DELETE: {
from: { ref: [{ id: 'capire.bookshop.Books', where: [{ ref: ['ID'] }, '=', { val: 4711 }]}] }
},
} : cqnWhere
expect(DELETE(Books, 4711))
.to.eql(DELETE(Books, { ID: 4711 }))
.to.eql(DELETE.from(Books, 4711))
.to.eql(DELETE.from(Books, { ID: 4711 }))
.to.eql(DELETE.from(Books).byKey(4711))
.to.eql(DELETE.from(Books).byKey({ ID: 4711 }))
.to.eql(cqnKey)
.to.eql(DELETE.from(Books).where({ ID: 4711 }))
.to.eql(DELETE.from(Books).where(`ID=`, 4711))
.to.eql({
DELETE: {
from: 'capire.bookshop.Books',
where: [{ ref: ['ID'] }, '=', { val: 4711 }],
},
})
})
test('/w plain SQL', () => {

View File

@@ -1,11 +1,11 @@
const cds = require('@sap/cds/lib')
const { expect } = cds.test (
const { expect } = require('../test') .run (
'serve', 'AdminService', '--from', '@capire/bookshop,@capire/common', '--in-memory'
)
const cds = require('@sap/cds/lib')
describe('Consuming Services locally', () => {
//
it('bootstrapped the database successfully', ()=>{
it('bootrapped the database successfully', ()=>{
const { AdminService } = cds.services
const { Authors } = AdminService.entities
expect(AdminService).not.to.be.undefined
@@ -15,17 +15,17 @@ describe('Consuming Services locally', () => {
it('supports targets as strings or reflected defs', async () => {
const AdminService = await cds.connect.to('AdminService')
const { Authors } = AdminService.entities
expect (await SELECT.from(Authors))
.to.eql(await SELECT.from('Authors'))
.to.eql(await AdminService.read(Authors))
const _ = expect (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
if (cds.version >= '4.2.0')
_.to.eql(await AdminService.run(SELECT.from('Authors')))
})
it('allows reading from local services using cds.ql', async () => {
const AdminService = await cds.connect.to('AdminService')
const authors = await AdminService.read (`Authors`, a => {
const query = SELECT.from('Authors', (a) => {
a.name,
a.books((b) => {
b.title,
@@ -34,6 +34,10 @@ describe('Consuming Services locally', () => {
})
})
}).where(`name like`, 'E%')
// temporary workaround
if (cds.version < '4.2.0')
query.SELECT.from.ref[0] = 'AdminService.Authors'
const authors = await AdminService.run(query)
expect(authors).to.containSubset([
{
name: 'Emily Brontë',

View File

@@ -1,14 +1,18 @@
const { GET, POST, expect } = require('../test') .run ('bookshop')
const cds = require('@sap/cds/lib')
const { GET, POST, expect } = cds.test(__dirname+'/../bookshop')
if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch
else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases
describe('Custom Handlers', () => {
it('should reject out-of-stock orders', async () => {
await POST `/browse/submitOrder ${{ book: 201, quantity: 5 }}`
await POST `/browse/submitOrder ${{ book: 201, quantity: 5 }}`
await expect(POST `/browse/submitOrder ${{ book: 201, quantity: 5 }}`).to.be.rejectedWith(/409 - 5 exceeds stock for book #201/)
await expect(
Promise.all([
POST('/browse/submitOrder', { book: 201, amount: 5 }),
POST('/browse/submitOrder', { book: 201, amount: 5 }),
POST('/browse/submitOrder', { book: 201, amount: 5 }),
])
).to.be.rejectedWith(/409 - 5 exceeds stock for book #201/)
const { data } = await GET`/admin/Books/201/stock/$value`
expect(data).to.equal(2)
})

View File

@@ -1,5 +1,4 @@
const cds = require('@sap/cds/lib')
const { GET, expect } = cds.test (__dirname+'/../hello')
const { GET, expect } = require('../test') .run ('serve','hello/world.cds')
describe('Hello world!', () => {

View File

@@ -1,5 +1,7 @@
const {expect} = require('../test')
const cds = require('@sap/cds/lib')
const {expect} = cds.test
const { parse:cdr } = cds.ql
// should become cds.compile(...) when cds5 is released
const model = cds.compile.to.csn (`
@@ -74,9 +76,11 @@ describe('Hierarchical Data', ()=>{
const expected = [
{ ID:100, name:'Some Cats...' },
{ ID:101, name:'Cat' },
{ ID:104, name:'Aristocat' }, // REVISIT: Should be deleted as well?
{ ID:108, name:'Catweazle' }
]
expect ( await SELECT`ID,name`.from(Cats) ).to.eql (expected)
if (cdr) expect ( await SELECT.from(Cats) ).to.containSubset (expected)
else expect ( await SELECT.from(Cats) ).to.eql (expected)
})
})

2
test/index.js Normal file
View File

@@ -0,0 +1,2 @@
const cds = require('@sap/cds')
module.exports = cds.test.in(__dirname,'..')

View File

@@ -1,5 +1,5 @@
const { GET, expect } = require('../test') .run ('serve', 'test/localized-data.cds', '--in-memory')
const cds = require('@sap/cds/lib')
const { GET, expect } = cds.test.run ('serve', __dirname+'/localized-data.cds', '--in-memory')
if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch
else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases
@@ -43,7 +43,7 @@ describe('Localized Data', () => {
{ title: 'Jane Eyre', author: 'Charlotte Brontë', currency: { name: 'Pfund' } },
{ title: 'The Raven', author: 'Edgar Allen Poe', currency: { name: 'US-Dollar' } },
{ title: 'Eleonora', author: 'Edgar Allen Poe', currency: { name: 'US-Dollar' } },
{ title: 'Catweazle', author: 'Richard Carpenter', currency: { name: 'Yen' } },
{ title: 'Catweazle', author: 'Richard Carpenter', currency: { name: 'Euro' } },
])
})
@@ -85,7 +85,7 @@ describe('Localized Data', () => {
{ title: 'Jane Eyre', currency: { name: 'British Pound' } },
{ title: 'The Raven', currency: { name: 'US Dollar' } },
{ title: 'Eleonora', currency: { name: 'US Dollar' } },
{ title: 'Catweazle', currency: { name: 'Yen' } },
{ title: 'Catweazle', currency: { name: 'Euro' } },
])
})
})

View File

@@ -1,5 +1,5 @@
const { expect } = require('../test')
const cds = require('@sap/cds/lib')
const { expect } = cds.test
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
@@ -60,11 +60,11 @@ describe('Messaging', ()=>{
expect(M).equals(N)
expect(received.length).equals(N)
expect(received.map(m=>m.data)).to.deep.equal([
{ count: 1, subject: '201', rating: 1 },
{ count: 2, subject: '201', rating: 1.5 },
{ count: 3, subject: '201', rating: 2 },
{ count: 4, subject: '201', rating: 2.5 },
{ count: 5, subject: '201', rating: 3 },
{ subject: '201', rating: 1 },
{ subject: '201', rating: 1.5 },
{ subject: '201', rating: 2 },
{ subject: '201', rating: 2.5 },
{ subject: '201', rating: 3 },
])
})
})

View File

@@ -1,5 +1,5 @@
const { GET, expect } = require('../test') .run ('bookshop')
const cds = require('@sap/cds/lib')
const { GET, expect } = cds.test ('@capire/bookshop')
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
@@ -18,9 +18,9 @@ describe('OData Protocol', () => {
})
it('supports $search in multiple fields', async () => {
const { data } = await GET `/browse/Books ${{
const { data } = await GET(`/browse/Books`, {
params: { $search: 'Po', $select: `title,author` },
}}`
})
expect(data.value).to.eql([
{ ID: 201, title: 'Wuthering Heights', author: 'Emily Brontë' },
{ ID: 207, title: 'Jane Eyre', author: 'Charlotte Brontë' },

View File

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