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
76 changed files with 488 additions and 891 deletions

1
.env Normal file
View File

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

View File

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

2
.gitignore vendored
View File

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

View File

@@ -105,5 +105,5 @@
"description": "### Summary\n\nThat's it! You have seen: \n- How to integrate database-specific functions in a CDS model.\n- How to switch between the two implementations for SQLite and SAP HANA." "description": "### Summary\n\nThat's it! You have seen: \n- How to integrate database-specific functions in a CDS model.\n- How to switch between the two implementations for SQLite and SAP HANA."
} }
], ],
"ref": "main" "ref": "master"
} }

View File

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

View File

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

View File

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

View File

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

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 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 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 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,4 +1,4 @@
using { Currency, managed, sap } from '@sap/cds/common'; using { Currency, managed, temporal, sap } from '@sap/cds/common';
namespace sap.capire.bookshop; namespace sap.capire.bookshop;
entity Books : managed { entity Books : managed {

View File

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

View File

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

View File

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

View File

@@ -71,7 +71,7 @@ POST {{server}}/browse/submitOrder
Content-Type: application/json Content-Type: application/json
{{me}} {{me}}
{ "book":201, "quantity":5 } { "book":201, "amount":5 }
### ------------------------------------------------------------------------ ### ------------------------------------------------------------------------

View File

@@ -1,39 +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",
"[development]": {
"model": "db/sqlite"
},
"[production]": {
"model": "db/hana"
}
}
},
"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 AUD;de;Australischer Dollar;Australischer Dollar
GBP;de;Pfund;Britische Pfund GBP;de;Pfund;Britische Pfund
ILS;de;Schekel;Israelische Schekel ILS;de;Schekel;Israelische Schekel
JPY;de;Yen;Japanische Yen
EUR;fr;euro;de la Zone euro EUR;fr;euro;de la Zone euro
USD;fr;dollar;dollar des États-Unis USD;fr;dollar;dollar des États-Unis
CAD;fr;dollar canadien;dollar canadien CAD;fr;dollar canadien;dollar canadien
AUD;fr;dollar australien;dollar australien AUD;fr;dollar australien;dollar australien
GBP;fr;livre sterling;pound sterling GBP;fr;livre sterling;pound sterling
ILS;fr;Shekel;shekel israelien 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,5 +1,4 @@
using { AdminService } from '@capire/bookshop'; using { AdminService } from '../../db/schema';
using from '../common'; // to help UI linter get the complete annotations
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// //
@@ -71,7 +70,7 @@ annotate AdminService.Authors with @(
annotate sap.capire.bookshop.Books with @fiori.draft.enabled; annotate sap.capire.bookshop.Books with @fiori.draft.enabled;
annotate AdminService.Books with @odata.draft.enabled; annotate AdminService.Books with @odata.draft.enabled;
annotate AdminService.Books.texts with @( annotate AdminService.Books_texts with @(
UI: { UI: {
Identification: [{Value:title}], Identification: [{Value:title}],
SelectionFields: [ locale, title ], SelectionFields: [ locale, title ],
@@ -84,11 +83,11 @@ annotate AdminService.Books.texts with @(
); );
// Add Value Help for Locales // Add Value Help for Locales
annotate AdminService.Books.texts { annotate AdminService.Books_texts {
locale @ValueList:{entity:'Languages',type:#fixed} 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'; using { sap } from '@sap/cds/common';
extend service AdminService { extend service AdminService {
@readonly entity Languages as projection on sap.common.Languages; entity Languages as projection on sap.common.Languages;
} }

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

@@ -6,11 +6,11 @@ using CatalogService from '@capire/bookshop';
// //
annotate CatalogService.Books with @( annotate CatalogService.Books with @(
UI: { UI: {
HeaderInfo: { HeaderInfo: {
TypeName: 'Book', TypeName: 'Book',
TypeNamePlural: 'Books', TypeNamePlural: 'Books',
Description: {Value: author} Description: {Value: author}
}, },
HeaderFacets: [ HeaderFacets: [
{$Type: 'UI.ReferenceFacet', Label: '{i18n>Description}', Target: '@UI.FieldGroup#Descr'}, {$Type: 'UI.ReferenceFacet', Label: '{i18n>Description}', Target: '@UI.FieldGroup#Descr'},
], ],
@@ -38,7 +38,7 @@ annotate CatalogService.Books with @(
// //
annotate CatalogService.Books with @( annotate CatalogService.Books with @(
UI: { UI: {
SelectionFields: [ ID, price, currency_code ], SelectionFields: [ ID, price, currency_code ],
LineItem: [ LineItem: [
{Value: title}, {Value: title},
{Value: author, Label:'{i18n>Author}'}, {Value: author, Label:'{i18n>Author}'},

View File

@@ -54,7 +54,7 @@ annotate my.Books with {
title @title:'{i18n>Title}'; title @title:'{i18n>Title}';
genre @title:'{i18n>Genre}' @Common: { Text: genre.name, TextArrangement: #TextOnly }; genre @title:'{i18n>Genre}' @Common: { Text: genre.name, TextArrangement: #TextOnly };
author @title:'{i18n>Author}' @Common: { Text: author.name, TextArrangement: #TextOnly }; author @title:'{i18n>Author}' @Common: { Text: author.name, TextArrangement: #TextOnly };
price @title:'{i18n>Price}' @Measures.ISOCurrency: currency_code; price @title:'{i18n>Price}';
stock @title:'{i18n>Stock}'; stock @title:'{i18n>Stock}';
descr @UI.MultiLineText; descr @UI.MultiLineText;
} }

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

@@ -6,4 +6,7 @@ using from './admin/fiori-service';
using from './browse/fiori-service'; using from './browse/fiori-service';
using from './common'; using from './common';
using from '@capire/bookstore/srv/mashup'; using from '@capire/common';
// only works in case of embedded orders service
using from '@capire/orders/app/orders/fiori-service';

View File

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

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

View File

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

View File

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

View File

@@ -1 +0,0 @@
PORT = 4007

View File

@@ -1,4 +0,0 @@
book_ID;number;title
201;1;Chapter 1
201;2;Chapter 2
201;3;Chapter 3
Internal Server Error - Bruno's Gitea
500 Internal Server Error

Gitea Version: 1.24.4