composites
This commit is contained in:
@@ -15,28 +15,30 @@ const books = new Vue ({
|
|||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
search: ({target:{value:v}}) => books.fetch (v && '$search='+v),
|
search: ({target:{value:v}}) => books.fetch(v && '&$search='+v),
|
||||||
|
|
||||||
async fetch (_filter='') {
|
async fetch (etc='') {
|
||||||
const columns = 'ID,title,author,price,stock', details = 'genre,currency'
|
const {data} = await GET(`/ListOfBooks?$expand=genre,currency${etc}`)
|
||||||
const {data} = await GET(`/Books?$select=${columns}&$expand=${details}&${_filter}`)
|
|
||||||
books.list = data.value
|
books.list = data.value
|
||||||
},
|
},
|
||||||
|
|
||||||
async inspect () {
|
async inspect (eve) {
|
||||||
const book = books.book = books.list [event.currentTarget.rowIndex-1]
|
const book = books.book = books.list [eve.currentTarget.rowIndex-1]
|
||||||
book.imageSrc || await GET(`/Books/${book.ID}/image`) .then (({data}) => book.imageSrc = data )
|
const res = await GET(`/Books/${book.ID}?$select=descr,stock,image`)
|
||||||
book.descr || await GET(`/Books/${book.ID}/descr/$value`) .then (({data}) => book.descr = data)
|
Object.assign (book, res.data)
|
||||||
books.order = { amount:1 }
|
books.order = { amount:1 }
|
||||||
setTimeout (()=> $('form > input').focus(), 111)
|
setTimeout (()=> $('form > input').focus(), 111)
|
||||||
},
|
},
|
||||||
|
|
||||||
submitOrder () { event.preventDefault()
|
async submitOrder () {
|
||||||
const {book,order} = books, amount = parseInt (order.amount) || 1
|
const {book,order} = books, amount = parseInt (order.amount) || 1
|
||||||
POST(`/submitOrder`, { amount, book: book.ID })
|
try {
|
||||||
.then (()=> books.order = { amount, succeeded: `Successfully orderd ${amount} item(s).` })
|
const res = await POST(`/submitOrder`, { amount, book: book.ID })
|
||||||
.catch (e=> books.order = { amount, failed: e.response.data.error.message })
|
book.stock = res.data.stock
|
||||||
GET(`/Books/${book.ID}/stock/$value`).then (res => book.stock = res.data)
|
books.order = { amount, succeeded: `Successfully orderd ${amount} item(s).` }
|
||||||
|
} catch (e) {
|
||||||
|
books.order = { amount, failed: e.response.data.error.message }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,18 +27,20 @@
|
|||||||
<th> Book </th>
|
<th> Book </th>
|
||||||
<th> Author </th>
|
<th> Author </th>
|
||||||
<th> Genre </th>
|
<th> Genre </th>
|
||||||
|
<th> Rating </th>
|
||||||
<th> Price </th>
|
<th> Price </th>
|
||||||
</thead>
|
</thead>
|
||||||
<tr v-for="book in list" v-bind:id="book.ID" v-on:click="inspect">
|
<tr v-for="book in list" v-bind:id="book.ID" v-on:click="inspect">
|
||||||
<td>{{ book.title }}</td>
|
<td>{{ book.title }}</td>
|
||||||
<td>{{ book.author }}</td>
|
<td>{{ book.author }}</td>
|
||||||
<td>{{ book.genre.name }}</td>
|
<td>{{ book.genre.name }}</td>
|
||||||
|
<td style="color:teal">{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }}</td>
|
||||||
<td>{{ book.currency.symbol }} {{ book.price }}</td>
|
<td>{{ book.currency.symbol }} {{ book.price }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div v-if="book.title">
|
<div v-if="book.title">
|
||||||
<img v-bind:src="book.imageSrc" alt=""/>
|
<img v-bind:src="book.image" alt=""/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="book.title">
|
<div v-if="book.title">
|
||||||
@@ -47,7 +49,7 @@
|
|||||||
<span class="has-error"> {{ order.failed }} </span>
|
<span class="has-error"> {{ order.failed }} </span>
|
||||||
{{ book.stock }} in stock
|
{{ book.stock }} in stock
|
||||||
</label>
|
</label>
|
||||||
<form @submit="submitOrder">
|
<form @submit.prevent="submitOrder">
|
||||||
<input type="number" id="amount" v-model="order.amount" v-bind:class="{ 'has-error': order.failed }">
|
<input type="number" id="amount" v-model="order.amount" v-bind:class="{ 'has-error': order.failed }">
|
||||||
<input type="submit" value="Order:" class="muted-button">
|
<input type="submit" value="Order:" class="muted-button">
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ entity Books : managed {
|
|||||||
author : Association to Authors;
|
author : Association to Authors;
|
||||||
genre : Association to Genres;
|
genre : Association to Genres;
|
||||||
stock : Integer;
|
stock : Integer;
|
||||||
price : Decimal(9,2);
|
price : Decimal;
|
||||||
currency : Currency;
|
currency : Currency;
|
||||||
image : LargeBinary @Core.MediaType : 'image/png';
|
image : LargeBinary @Core.MediaType : 'image/png';
|
||||||
}
|
}
|
||||||
|
|||||||
0
bookshop/sqlite.db
Normal file
0
bookshop/sqlite.db
Normal file
12
bookshop/srv/admin-service.js
Normal file
12
bookshop/srv/admin-service.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const cds = require('@sap/cds')
|
||||||
|
|
||||||
|
module.exports = cds.service.impl (function(){
|
||||||
|
this.before ('NEW','Authors', genid)
|
||||||
|
this.before ('NEW','Books', genid)
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Generate primary keys for target entity in request */
|
||||||
|
async function genid (req) {
|
||||||
|
const {ID} = await cds.tx(req).run (SELECT.one.from(req.target).columns('max(ID) as ID'))
|
||||||
|
req.data.ID = ID - ID % 100 + 100 + 1
|
||||||
|
}
|
||||||
@@ -5,6 +5,10 @@ service CatalogService @(path:'/browse') {
|
|||||||
author.name as author
|
author.name as author
|
||||||
} excluding { createdBy, modifiedBy };
|
} excluding { createdBy, modifiedBy };
|
||||||
|
|
||||||
|
@readonly entity ListOfBooks as SELECT from Books
|
||||||
|
excluding { descr, stock };
|
||||||
|
|
||||||
@requires: 'authenticated-user'
|
@requires: 'authenticated-user'
|
||||||
action submitOrder (book : Books:ID, amount: Integer);
|
action submitOrder ( book: Books:ID, amount: Integer ) returns { stock: Integer };
|
||||||
|
event OrderedBook : { book: Books:ID; amount: Integer; buyer: String };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
const cds = require('@sap/cds')
|
const cds = require('@sap/cds')
|
||||||
module.exports = async function (){
|
const { Books } = cds.entities ('sap.capire.bookshop')
|
||||||
|
|
||||||
const db = await cds.connect.to('db') // connect to database service
|
class CatalogService extends cds.ApplicationService { async init(){
|
||||||
const { Books } = db.entities // get reflected definitions
|
|
||||||
|
|
||||||
// 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,amount} = req.data
|
const {book,amount} = req.data, tx = cds.tx(req)
|
||||||
const n = await UPDATE (Books, book)
|
let {stock} = await tx.read('stock').from(Books,book)
|
||||||
.with ({ stock: {'-=': amount }})
|
if (stock >= amount) {
|
||||||
.where ({ stock: {'>=': amount }})
|
await tx.update (Books,book).with ({ stock: stock -= amount })
|
||||||
n > 0 || req.error (409,`${amount} exceeds stock for book #${book}`)
|
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
|
// Add some discount for overstocked books
|
||||||
@@ -19,4 +21,8 @@ module.exports = async function (){
|
|||||||
each.title += ` -- 11% discount!`
|
each.title += ` -- 11% discount!`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
return super.init()
|
||||||
|
}}
|
||||||
|
|
||||||
|
module.exports = { CatalogService }
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ using CatalogService from '@capire/bookshop';
|
|||||||
annotate CatalogService.Books with @(
|
annotate CatalogService.Books with @(
|
||||||
UI: {
|
UI: {
|
||||||
HeaderInfo: {
|
HeaderInfo: {
|
||||||
|
TypeName: 'Book',
|
||||||
|
TypeNamePlural: 'Books',
|
||||||
Description: {Value: author}
|
Description: {Value: author}
|
||||||
},
|
},
|
||||||
HeaderFacets: [
|
HeaderFacets: [
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
navigationMode: "embedded"
|
navigationMode: "embedded"
|
||||||
},
|
},
|
||||||
"manage-orders": {
|
"manage-orders": {
|
||||||
title: "Order Books",
|
title: "Manage Orders",
|
||||||
description: "... testing FE v42",
|
description: "... testing FE v42",
|
||||||
additionalInformation: "SAPUI5.Component=orders",
|
additionalInformation: "SAPUI5.Component=orders",
|
||||||
applicationType : "URL",
|
applicationType : "URL",
|
||||||
@@ -4,7 +4,9 @@
|
|||||||
|
|
||||||
using from './admin/fiori-service';
|
using from './admin/fiori-service';
|
||||||
using from './browse/fiori-service';
|
using from './browse/fiori-service';
|
||||||
using from './orders/fiori-service';
|
|
||||||
using from './common';
|
using from './common';
|
||||||
|
|
||||||
using from '@capire/common';
|
using from '@capire/common';
|
||||||
|
|
||||||
|
// only works in case of embedded orders service
|
||||||
|
using from '@capire/orders/app/orders/fiori-service';
|
||||||
|
|||||||
1
fiori/app/vue/index.html
Normal file
1
fiori/app/vue/index.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!-- This is just a dummy to be detected by the automatically generated /index.html -->
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Proxy for importing schema from bookshop sample
|
|
||||||
using from '@capire/bookshop';
|
|
||||||
namespace sap.capire.bookshop;
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capire/bookshop": "*",
|
"@capire/bookshop": "*",
|
||||||
|
"@capire/reviews": "*",
|
||||||
"@capire/orders": "*",
|
"@capire/orders": "*",
|
||||||
"@capire/common": "*",
|
"@capire/common": "*",
|
||||||
"@sap/cds": "^4",
|
"@sap/cds": "^4",
|
||||||
@@ -15,6 +16,12 @@
|
|||||||
},
|
},
|
||||||
"cds": {
|
"cds": {
|
||||||
"requires": {
|
"requires": {
|
||||||
|
"ReviewsService": {
|
||||||
|
"kind": "odata", "model": "@capire/reviews"
|
||||||
|
},
|
||||||
|
"OrdersService": {
|
||||||
|
"kind": "odata", "model": "@capire/orders"
|
||||||
|
},
|
||||||
"db": {
|
"db": {
|
||||||
"kind": "sql"
|
"kind": "sql"
|
||||||
}
|
}
|
||||||
|
|||||||
17
fiori/server.js
Normal file
17
fiori/server.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const express = require ('express')
|
||||||
|
const cds = require ('@sap/cds')
|
||||||
|
|
||||||
|
const _imported = (path,file) => express.static(
|
||||||
|
require.resolve(`${path}/${file}`).slice(0,-1-file.length)
|
||||||
|
)
|
||||||
|
|
||||||
|
cds.once('bootstrap',(app)=>{
|
||||||
|
// serving the orders app imported from @capire/orders
|
||||||
|
app.use ('/orders/webapp', _imported('@capire/orders/app/orders/webapp','manifest.json'))
|
||||||
|
// serving the vue.js app imported from @capire/bookshop
|
||||||
|
app.use ('/vue', _imported('@capire/bookshop/app/vue','index.html'))
|
||||||
|
})
|
||||||
|
|
||||||
|
cds.once('served', require('./srv/mashup'))
|
||||||
|
|
||||||
|
module.exports = cds.server
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Proxy for importing services from bookshop sample
|
|
||||||
using from '@capire/bookshop';
|
|
||||||
annotate AdminService with @impl:'srv/admin-service.js';
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
const cds = require('@sap/cds')
|
|
||||||
|
|
||||||
module.exports = cds.service.impl (async function() {
|
|
||||||
const {Books} = cds.entities
|
|
||||||
const {ID} = await SELECT.one.from(Books).columns('max(ID) as ID')
|
|
||||||
let newID = ID - ID % 100 + 100
|
|
||||||
this.before ('NEW','Books', req => req.data.ID = ++newID)
|
|
||||||
})
|
|
||||||
25
fiori/srv/mashup.cds
Normal file
25
fiori/srv/mashup.cds
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Mashing up imported models...
|
||||||
|
//
|
||||||
|
|
||||||
|
using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Extend Books with access to Reviews and average ratings
|
||||||
|
//
|
||||||
|
|
||||||
|
using { ReviewsService.Reviews } from '@capire/reviews';
|
||||||
|
extend Books with {
|
||||||
|
reviews : Composition of many Reviews on reviews.subject = $self.ID;
|
||||||
|
rating : Decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Extend Orders with Books as articles
|
||||||
|
//
|
||||||
|
|
||||||
|
using { sap.capire.orders.OrderItems } from '@capire/orders';
|
||||||
|
extend OrderItems with {
|
||||||
|
book : Association to Books on article = book.ID
|
||||||
|
}
|
||||||
59
fiori/srv/mashup.js
Normal file
59
fiori/srv/mashup.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Mashing up provided and required services...
|
||||||
|
//
|
||||||
|
module.exports = async()=>{ // called by server.js
|
||||||
|
|
||||||
|
const cds = require('@sap/cds')
|
||||||
|
const CatalogService = await cds.connect.to ('CatalogService')
|
||||||
|
const ReviewsService = await cds.connect.to ('ReviewsService')
|
||||||
|
const OrdersService = await cds.connect.to ('OrdersService')
|
||||||
|
const db = await cds.connect.to ('db')
|
||||||
|
|
||||||
|
// reflect entity definitions used below...
|
||||||
|
const { Books } = db.entities ('sap.capire.bookshop')
|
||||||
|
|
||||||
|
//
|
||||||
|
// Delegate requests to read reviews to the ReviewsService
|
||||||
|
// Note: prepend is neccessary to intercept generic default handler
|
||||||
|
//
|
||||||
|
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
|
||||||
|
console.debug ('> delegating request to ReviewsService')
|
||||||
|
const [id] = req.params, { columns, limit } = req.query.SELECT
|
||||||
|
return ReviewsService.tx(req).read ('Reviews',columns).limit(limit).where({subject:String(id)})
|
||||||
|
}))
|
||||||
|
|
||||||
|
//
|
||||||
|
// Create an order with the OrdersService when CatalogService signals a new order
|
||||||
|
//
|
||||||
|
CatalogService.on ('OrderedBook', async (msg) => {
|
||||||
|
const { book, amount, buyer } = msg.data
|
||||||
|
const { title, price } = await db.tx(msg).read (Books, book, b => { b.title, b.price })
|
||||||
|
return OrdersService.tx(msg).create ('Orders').entries({
|
||||||
|
OrderNo: 'Order at '+ (new Date).toLocaleString(),
|
||||||
|
Items: [{ article:`${book}`, title, price, amount }],
|
||||||
|
buyer, createdBy: buyer
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
//
|
||||||
|
// Update Books' average ratings when ReviewsService signals updatd reviews
|
||||||
|
//
|
||||||
|
ReviewsService.on ('reviewed', (msg) => {
|
||||||
|
console.debug ('> received:', msg.event, msg.data)
|
||||||
|
const { subject, rating } = msg.data
|
||||||
|
return UPDATE(Books,subject).with({rating})
|
||||||
|
// ^ Note: the framework will execute this and take care for db.tx
|
||||||
|
})
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reduce stock of ordered books for orders are created from Orders admin UI
|
||||||
|
//
|
||||||
|
OrdersService.on ('OrderChanged', async (msg) => {
|
||||||
|
console.debug ('> received:', msg.event, msg.data)
|
||||||
|
const { article, deltaAmount } = msg.data
|
||||||
|
return UPDATE (Books) .where ('ID =', article)
|
||||||
|
.and ('stock >=', deltaAmount)
|
||||||
|
.set ('stock -=', deltaAmount)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
@me = {{$processEnv USER}}:
|
@me = {{$processEnv USER}}:
|
||||||
@bookshop = http://localhost:4004
|
@bookshop = http://localhost:4004
|
||||||
@reviews-service = {{bookshop}}/reviews
|
@reviews-service = {{bookshop}}/reviews
|
||||||
|
|
||||||
|
# Uncomment this when running separate reviews service
|
||||||
# @reviews-service = http://localhost:5005/reviews
|
# @reviews-service = http://localhost:5005/reviews
|
||||||
|
|
||||||
|
|
||||||
@@ -44,3 +46,9 @@ GET {{bookshop}}/browse/Books(201)?
|
|||||||
&$select=ID,title,rating
|
&$select=ID,title,rating
|
||||||
&$expand=reviews
|
&$expand=reviews
|
||||||
# Note: the $expand only works in case of ReviewsService in same process
|
# Note: the $expand only works in case of ReviewsService in same process
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
GET {{bookshop}}/orders/Orders
|
||||||
2
orders/.env
Normal file
2
orders/.env
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
cds.requires.messaging.kind = file-based-messaging
|
||||||
|
PORT = 4005
|
||||||
39
orders/app/fiori-app.html
Normal file
39
orders/app/fiori-app.html
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Bookshop</title>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window["sap-ushell-config"] = {
|
||||||
|
defaultRenderer: "fiori2",
|
||||||
|
applications: {
|
||||||
|
"manage-orders": {
|
||||||
|
title: "Manage Orders",
|
||||||
|
description: "... testing FE v42",
|
||||||
|
additionalInformation: "SAPUI5.Component=orders",
|
||||||
|
applicationType : "URL",
|
||||||
|
url: "/orders/webapp",
|
||||||
|
navigationMode: "embedded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="sap-ushell-bootstrap" src="https://sapui5.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js"></script>
|
||||||
|
<script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
|
||||||
|
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout"
|
||||||
|
data-sap-ui-compatVersion="edge"
|
||||||
|
data-sap-ui-theme="sap_fiori_3"
|
||||||
|
data-sap-ui-frameOptions="allow"
|
||||||
|
></script>
|
||||||
|
<script>
|
||||||
|
sap.ui.getCore().attachInit(()=> sap.ushell.Container.createRenderer().placeAt("content"))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body class="sapUiBody" id="content"></body>
|
||||||
|
</html>
|
||||||
5
orders/app/index.cds
Normal file
5
orders/app/index.cds
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/*
|
||||||
|
This model controls what gets served to Fiori frontends...
|
||||||
|
*/
|
||||||
|
|
||||||
|
using from './orders/fiori-service';
|
||||||
@@ -1,44 +1,27 @@
|
|||||||
using OrdersService from '@capire/orders/srv/orders-service';
|
|
||||||
|
|
||||||
annotate OrdersService.Books with {
|
|
||||||
price @Common.FieldControl: #ReadOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Common
|
// Note: this is designed for the OrdersService being co-located with
|
||||||
|
// bookshop. It does not work if OrdersService is run as a separate
|
||||||
|
// process, and is not intended to do so.
|
||||||
//
|
//
|
||||||
annotate OrdersService.OrderItems with {
|
////////////////////////////////////////////////////////////////////////////
|
||||||
book @(
|
|
||||||
Common: {
|
|
||||||
Text: book.title,
|
|
||||||
FieldControl: #Mandatory
|
using { OrdersService, sap.capire.orders.OrderItems } from '../../srv/orders-service';
|
||||||
},
|
|
||||||
ValueList.entity:'Books',
|
|
||||||
);
|
|
||||||
amount @(
|
|
||||||
Common.FieldControl: #Mandatory
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@odata.draft.enabled
|
@odata.draft.enabled
|
||||||
annotate OrdersService.Orders with @(
|
annotate OrdersService.Orders with @(
|
||||||
UI: {
|
UI: {
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Lists of Orders
|
|
||||||
//
|
|
||||||
SelectionFields: [ createdAt, createdBy ],
|
SelectionFields: [ createdAt, createdBy ],
|
||||||
LineItem: [
|
LineItem: [
|
||||||
{Value: createdBy, Label:'Customer'},
|
{Value: OrderNo, Label:'OrderNo'},
|
||||||
|
{Value: buyer, Label:'Customer'},
|
||||||
{Value: createdAt, Label:'Date'}
|
{Value: createdAt, Label:'Date'}
|
||||||
],
|
],
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Order Details
|
|
||||||
//
|
|
||||||
HeaderInfo: {
|
HeaderInfo: {
|
||||||
TypeName: 'Order', TypeNamePlural: 'Orders',
|
TypeName: 'Order', TypeNamePlural: 'Orders',
|
||||||
Title: {
|
Title: {
|
||||||
@@ -62,7 +45,7 @@ annotate OrdersService.Orders with @(
|
|||||||
],
|
],
|
||||||
FieldGroup#Details: {
|
FieldGroup#Details: {
|
||||||
Data: [
|
Data: [
|
||||||
{Value: currency_code, Label:'Currency'}
|
{Value: currency.code, Label:'Currency'}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
FieldGroup#Created: {
|
FieldGroup#Created: {
|
||||||
@@ -85,36 +68,25 @@ annotate OrdersService.Orders with @(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
//The enity types name is OrdersService.my_bookshop_OrderItems
|
annotate OrderItems with @(
|
||||||
//The annotations below are not generated in edmx WHY?
|
|
||||||
annotate OrdersService.OrderItems with @(
|
|
||||||
UI: {
|
UI: {
|
||||||
HeaderInfo: {
|
|
||||||
TypeName: 'Order Item', TypeNamePlural: ' ',
|
|
||||||
Title: {
|
|
||||||
Value: book.title
|
|
||||||
},
|
|
||||||
Description: {Value: book.descr}
|
|
||||||
},
|
|
||||||
// There is no filterbar for items so the selctionfileds is not needed
|
|
||||||
SelectionFields: [ book_ID ],
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Lists of OrderItems
|
|
||||||
//
|
|
||||||
LineItem: [
|
LineItem: [
|
||||||
{Value: book_ID, Label:'Book'},
|
{Value: article, Label:'Article ID'},
|
||||||
//The following entry is only used to have the assoication followed in the read event
|
{Value: title, Label:'Article Title'},
|
||||||
{Value: book.price, Label:'Book Price'},
|
{Value: price, Label:'Unit Price'},
|
||||||
{Value: amount, Label:'Quantity'},
|
{Value: amount, Label:'Quantity'},
|
||||||
],
|
],
|
||||||
Identification: [ //Is the main field group
|
Identification: [ //Is the main field group
|
||||||
//{Value: ID, Label:'ID'}, //A guid shouldn't be on the UI
|
|
||||||
{Value: book_ID, Label:'Book'},
|
|
||||||
{Value: amount, Label:'Amount'},
|
{Value: amount, Label:'Amount'},
|
||||||
|
{Value: title, Label:'Article'},
|
||||||
|
{Value: price, Label:'Unit Price'},
|
||||||
],
|
],
|
||||||
Facets: [
|
Facets: [
|
||||||
{$Type: 'UI.ReferenceFacet', Label: '{i18n>OrderItems}', Target: '@UI.Identification'},
|
{$Type: 'UI.ReferenceFacet', Label: '{i18n>OrderItems}', Target: '@UI.Identification'},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
) {
|
||||||
|
amount @(
|
||||||
|
Common.FieldControl: #Mandatory
|
||||||
);
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
ID;amount;parent_ID;book_ID;netAmount
|
|
||||||
58040e66-1dcd-4ffb-ab10-fdce32028b79;1;7e2f2640-6866-4dcf-8f4d-3027aa831cad;201;11.11
|
|
||||||
64e718c9-ff99-47f1-8ca3-950c850777d4;1;7e2f2640-6866-4dcf-8f4d-3027aa831cad;271;15
|
|
||||||
e9641166-e050-4261-bfee-d1e797e6cb7f;2;64e718c9-ff99-47f1-8ca3-950c850777d4;252;28
|
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
ID;modifiedAt;createdAt;createdBy;modifiedBy;OrderNo;currency_code
|
|
||||||
7e2f2640-6866-4dcf-8f4d-3027aa831cad;;2019-01-31;john.doe@test.com;;1;EUR
|
|
||||||
64e718c9-ff99-47f1-8ca3-950c850777d4;;2019-01-30;jane.doe@test.com;;2;EUR
|
|
||||||
|
4
orders/db/data/sap.capire.orders-OrderItems.csv
Normal file
4
orders/db/data/sap.capire.orders-OrderItems.csv
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
ID;order_ID;amount;article;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
|
||||||
|
3
orders/db/data/sap.capire.orders-Orders.csv
Normal file
3
orders/db/data/sap.capire.orders-Orders.csv
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ID;createdAt;createdBy;buyer;OrderNo;currency_code
|
||||||
|
7e2f2640-6866-4dcf-8f4d-3027aa831cad;2019-01-31;john.doe@test.com;john.doe@test.com;1;EUR
|
||||||
|
64e718c9-ff99-47f1-8ca3-950c850777d4;2019-01-30;jane.doe@test.com;jane.doe@test.com;2;EUR
|
||||||
|
@@ -1,16 +1,19 @@
|
|||||||
using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
using { Currency, User, managed, cuid } from '@sap/cds/common';
|
||||||
using { Currency, managed, cuid } from '@sap/cds/common';
|
using from '@capire/common';
|
||||||
namespace sap.capire.bookshop;
|
namespace sap.capire.orders;
|
||||||
|
|
||||||
entity Orders : cuid, managed {
|
entity Orders : cuid, managed {
|
||||||
OrderNo : String @title:'Order Number'; //> readable key
|
OrderNo : String @title:'Order Number'; //> readable key
|
||||||
Items : Composition of many OrderItems on Items.parent = $self;
|
Items : Composition of many OrderItems on Items.order = $self;
|
||||||
|
buyer : User;
|
||||||
currency : Currency;
|
currency : Currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
entity OrderItems : cuid {
|
entity OrderItems {
|
||||||
parent : Association to Orders;
|
key ID : UUID;
|
||||||
book : Association to Books;
|
order : Association to Orders;
|
||||||
amount : Integer;
|
amount : Integer;
|
||||||
netAmount : Decimal(9,2);
|
article : String; //> to allow for arbitrary keys
|
||||||
|
title : String;
|
||||||
|
price : Double;
|
||||||
}
|
}
|
||||||
|
|||||||
6
orders/index.cds
Normal file
6
orders/index.cds
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/*
|
||||||
|
This model controls what gets exposed
|
||||||
|
*/
|
||||||
|
namespace sap.capire.orders;
|
||||||
|
using from './srv/orders-service';
|
||||||
|
using from './db/schema';
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@capire/orders",
|
"name": "@capire/orders",
|
||||||
"version": "1.0.0"
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@capire/common": "*",
|
||||||
|
"@sap/cds": "^4.3.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
using { sap.capire.bookshop as my } from '../db/schema';
|
using { sap.capire.orders as my } from '../db/schema';
|
||||||
|
|
||||||
service OrdersService {
|
service OrdersService {
|
||||||
entity Orders as projection on my.Orders;
|
entity Orders as projection on my.Orders;
|
||||||
entity Books as projection on my.Books;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,37 @@
|
|||||||
const cds = require ('@sap/cds')
|
const cds = require ('@sap/cds')
|
||||||
|
class OrdersService extends cds.ApplicationService {
|
||||||
|
|
||||||
module.exports = cds.service.impl(function() {
|
/** register custom handlers */
|
||||||
|
init(){
|
||||||
|
const { OrderItems } = this.entities
|
||||||
|
|
||||||
const { Books } = cds.entities
|
this.before ('UPDATE', 'Orders', async function(req) {
|
||||||
|
const { ID, Items } = req.data
|
||||||
// Reduce stock of ordered books if available stock suffices
|
if (Items) for (let { article, amount } of Items) {
|
||||||
this.before ('CREATE', 'Orders', (req) => {
|
const { amount:before } = await cds.tx(req).run (
|
||||||
const { Items: items } = req.data
|
SELECT.one.from (OrderItems, oi => oi.amount) .where ({order_ID:ID, article})
|
||||||
return cds.transaction(req) .run (items.map (item =>
|
|
||||||
UPDATE (Books) .where ('ID =', item.book_ID)
|
|
||||||
.and ('stock >=', item.amount)
|
|
||||||
.set ('stock -=', item.amount)
|
|
||||||
)) .then (all => all.forEach ((affectedRows,i) => {
|
|
||||||
if (affectedRows === 0) req.error (409,
|
|
||||||
`${items[i].amount} exceeds stock for book #${items[i].book_ID}`
|
|
||||||
)
|
)
|
||||||
}))
|
if (amount != before) this.orderChanged (article, 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.article, oi.amount }) .where ({order_ID:ID})
|
||||||
|
)
|
||||||
|
if (Items) for (let it of Items) this.orderChanged (it.article, -it.amount)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** order changed -> broadcast event */
|
||||||
|
orderChanged (article, deltaAmount) {
|
||||||
|
// Emit events to inform subscribers about changes in orders
|
||||||
|
console.log ('> emitting:', 'OrderChanged', { article, deltaAmount })
|
||||||
|
return this.emit ('OrderChanged', { article, deltaAmount })
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
module.exports = OrdersService
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
"@capire/hello": "./hello",
|
"@capire/hello": "./hello",
|
||||||
"@capire/media": "./media",
|
"@capire/media": "./media",
|
||||||
"@capire/orders": "./orders",
|
"@capire/orders": "./orders",
|
||||||
"@capire/reviewed": "./reviewed",
|
|
||||||
"@capire/reviews": "./reviews"
|
"@capire/reviews": "./reviews"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
cds.requires.messaging.kind = file-based-messaging
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
//
|
|
||||||
// Extending Books with Reviews
|
|
||||||
//
|
|
||||||
|
|
||||||
using { sap.capire.bookshop.Books } from '@capire/bookshop';
|
|
||||||
using { ReviewsService.Reviews } from '@capire/reviews';
|
|
||||||
|
|
||||||
extend Books with {
|
|
||||||
/** Access to detailed collection of Reviews */
|
|
||||||
reviews : Composition of many Reviews on reviews.subject = $self.ID;
|
|
||||||
/** Average rating */
|
|
||||||
rating : Reviews.rating;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temporary workaround for cap/issues#4112:
|
|
||||||
annotate Reviews with @cds.autoexpose;
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@capire/bookshop-with-reviews",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@capire/bookshop": "../bookshop",
|
|
||||||
"@capire/reviews": "../reviews",
|
|
||||||
"@sap/cds": "^4",
|
|
||||||
"express": "^4.17.1"
|
|
||||||
},
|
|
||||||
"cds": {
|
|
||||||
"requires": {
|
|
||||||
"db": {
|
|
||||||
"kind": "sql"
|
|
||||||
},
|
|
||||||
"ReviewsService": {
|
|
||||||
"kind": "odata", "model": "@capire/reviews"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// This is an example of using a project-local server.js to intercept
|
|
||||||
// the default bootstrapping process.
|
|
||||||
//
|
|
||||||
const cds = require ('@sap/cds')
|
|
||||||
|
|
||||||
// Connect CatalogService and ReviewsService when all are served...
|
|
||||||
cds.once('served', async ({CatalogService}) => {
|
|
||||||
|
|
||||||
const ReviewsService = await cds.connect.to('ReviewsService')
|
|
||||||
|
|
||||||
// reflect entity definitions used below...
|
|
||||||
const { Books } = cds.entities('sap.capire.bookshop')
|
|
||||||
const { Reviews } = ReviewsService.entities
|
|
||||||
|
|
||||||
// prepend the following handler so it overrides the default handler
|
|
||||||
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
|
|
||||||
console.debug ('> delegating request to ReviewsService')
|
|
||||||
const [id] = req.params, { columns, limit } = req.query.SELECT
|
|
||||||
return SELECT(columns).from(Reviews).limit(limit).where({subject:String(id)})
|
|
||||||
}))
|
|
||||||
|
|
||||||
ReviewsService.on ('reviewed', (msg) => {
|
|
||||||
console.debug ('> received:', msg.event, msg.data)
|
|
||||||
const { subject, rating } = msg.data
|
|
||||||
return UPDATE(Books,subject).with({rating})
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
// Delegate bootstrapping to built-in server.js
|
|
||||||
module.exports = cds.server
|
|
||||||
@@ -17,7 +17,7 @@ entity Reviews {
|
|||||||
liked : Integer default 0; // counter for likes as helpful review (count of all _likes belonging to this review)
|
liked : Integer default 0; // counter for likes as helpful review (count of all _likes belonging to this review)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Rating : Decimal(3,2) enum {
|
type Rating : Integer enum {
|
||||||
Best = 5;
|
Best = 5;
|
||||||
Good = 4;
|
Good = 4;
|
||||||
Avg = 3;
|
Avg = 3;
|
||||||
|
|||||||
23
samples.md
23
samples.md
@@ -4,12 +4,12 @@ The list below gives an overview of the samples provided in subdirectories.
|
|||||||
Each sub directory essentially is a individual npm package arranged in an [all-in-one monorepo](all-in-one-monorepo) umbrella setup.
|
Each sub directory essentially is a individual npm package arranged in an [all-in-one monorepo](all-in-one-monorepo) umbrella setup.
|
||||||
|
|
||||||
|
|
||||||
## [hello](hello)
|
## [@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).
|
- 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).
|
||||||
|
|
||||||
|
|
||||||
## [bookshop](bookshop)
|
## [@capire/bookshop](bookshop)
|
||||||
|
|
||||||
- [Getting Started](https://cap.cloud.sap/docs/get-started/in-a-nutshell) with CAP, briefly introducing:
|
- [Getting Started](https://cap.cloud.sap/docs/get-started/in-a-nutshell) with CAP, briefly introducing:
|
||||||
- [Project Setup](https://cap.cloud.sap/docs/get-started/) and [Layouts](https://cap.cloud.sap/docs/get-started/projects)
|
- [Project Setup](https://cap.cloud.sap/docs/get-started/) and [Layouts](https://cap.cloud.sap/docs/get-started/projects)
|
||||||
@@ -20,7 +20,7 @@ Each sub directory essentially is a individual npm package arranged in an [all-i
|
|||||||
- [Using Databases](https://cap.cloud.sap/docs/guides/databases)
|
- [Using Databases](https://cap.cloud.sap/docs/guides/databases)
|
||||||
|
|
||||||
|
|
||||||
## [common](common)
|
## [@capire/common](common)
|
||||||
|
|
||||||
- Showcases how to extend [@sap/cds/common](https://cap.cloud.sap/docs/cds/common) thereby covering...
|
- Showcases how to extend [@sap/cds/common](https://cap.cloud.sap/docs/cds/common) thereby covering...
|
||||||
- Building [extension packages](https://cap.cloud.sap/docs/guides/domain-models#aspects-extensibility)
|
- Building [extension packages](https://cap.cloud.sap/docs/guides/domain-models#aspects-extensibility)
|
||||||
@@ -30,14 +30,14 @@ Each sub directory essentially is a individual npm package arranged in an [all-i
|
|||||||
- Used in the [fiori app sample](#fiori)
|
- Used in the [fiori app sample](#fiori)
|
||||||
|
|
||||||
|
|
||||||
## [orders](orders)
|
## [@capire/orders](orders)
|
||||||
|
|
||||||
- Adds orders to the [bookshop](#bookshop), thereby demonstrating...
|
- A standalone orders mgmt service, demonstrating...
|
||||||
- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with
|
- Using [Compositions](https://cap.cloud.sap/docs/cds/cdl#compositions) in [Domain Models](https://cap.cloud.sap/docs/guides/domain-models), along with
|
||||||
- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)
|
- [Serving deeply nested documents](https://cap.cloud.sap/docs/guides/generic-providers#serving-structured-data)
|
||||||
|
|
||||||
|
|
||||||
## [reviews](reviews)
|
## [@capire/reviews](reviews)
|
||||||
|
|
||||||
- Shows how to implement a modular service to manage product reviews, including...
|
- Shows how to implement a modular service to manage product reviews, including...
|
||||||
- Consuming other services synchronously and asynchronously
|
- Consuming other services synchronously and asynchronously
|
||||||
@@ -50,14 +50,19 @@ Each sub directory essentially is a individual npm package arranged in an [all-i
|
|||||||
- As well as managed data, input validations and authorization
|
- As well as managed data, input validations and authorization
|
||||||
|
|
||||||
|
|
||||||
## [fiori](fiori)
|
## [@capire/fiori](fiori)
|
||||||
|
|
||||||
- [Adds a Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/), introducing to...
|
- 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)
|
||||||
|
- [Adds a Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/) to bookshop, thereby introducing to...
|
||||||
- [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files
|
- [OData Annotations](https://cap.cloud.sap/docs/guides/fiori#adding-odata-annotations) in `.cds` files
|
||||||
- Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)
|
- Support for [Fiori Draft](https://cap.cloud.sap/docs/guides/fiori#draft)
|
||||||
- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)
|
- Support for [Value Helps](https://cap.cloud.sap/docs/guides/fiori#value-help)
|
||||||
- Serving Fiori apps locally
|
- Serving Fiori apps locally
|
||||||
- Combining most of the other samples through [package reuse](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content)
|
- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well
|
||||||
|
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|||||||
Reference in New Issue
Block a user