diff --git a/LICENSES/Apache-2.0.txt b/LICENSE
similarity index 100%
rename from LICENSES/Apache-2.0.txt
rename to LICENSE
diff --git a/bookshop/app/vue/app.js b/bookshop/app/vue/app.js
index 3dd4a2b7..e7aedcaa 100644
--- a/bookshop/app/vue/app.js
+++ b/bookshop/app/vue/app.js
@@ -15,28 +15,30 @@ const books = new Vue ({
methods: {
- search: ({target:{value:v}}) => books.fetch (v && '$search='+v),
+ search: ({target:{value:v}}) => books.fetch(v && '&$search='+v),
- async fetch (_filter='') {
- const columns = 'ID,title,author,price,stock', details = 'genre,currency'
- const {data} = await GET(`/Books?$select=${columns}&$expand=${details}&${_filter}`)
+ async fetch (etc='') {
+ const {data} = await GET(`/ListOfBooks?$expand=genre,currency${etc}`)
books.list = data.value
},
- async inspect () {
- const book = books.book = books.list [event.currentTarget.rowIndex-1]
- book.imageSrc || await GET(`/Books/${book.ID}/image`) .then (({data}) => book.imageSrc = data )
- book.descr || await GET(`/Books/${book.ID}/descr/$value`) .then (({data}) => book.descr = data)
+ async inspect (eve) {
+ 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 = { amount:1 }
setTimeout (()=> $('form > input').focus(), 111)
},
- submitOrder () { event.preventDefault()
+ async submitOrder () {
const {book,order} = books, amount = parseInt (order.amount) || 1
- POST(`/submitOrder`, { amount, book: book.ID })
- .then (()=> books.order = { amount, succeeded: `Successfully orderd ${amount} item(s).` })
- .catch (e=> books.order = { amount, failed: e.response.data.error.message })
- GET(`/Books/${book.ID}/stock/$value`).then (res => book.stock = res.data)
+ try {
+ const res = await POST(`/submitOrder`, { amount, book: book.ID })
+ book.stock = res.data.stock
+ books.order = { amount, succeeded: `Successfully orderd ${amount} item(s).` }
+ } catch (e) {
+ books.order = { amount, failed: e.response.data.error.message }
+ }
}
}
diff --git a/bookshop/app/vue/index.html b/bookshop/app/vue/index.html
index 51408d43..f4753234 100644
--- a/bookshop/app/vue/index.html
+++ b/bookshop/app/vue/index.html
@@ -27,18 +27,20 @@
Book |
Author |
Genre |
+ Rating |
Price |
| {{ book.title }} |
{{ book.author }} |
{{ book.genre.name }} |
+ {{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }} |
{{ book.currency.symbol }} {{ book.price }} |
-
![]()
+
@@ -47,7 +49,7 @@
{{ order.failed }}
{{ book.stock }} in stock
-
diff --git a/bookshop/db/schema.cds b/bookshop/db/schema.cds
index 99fadb5a..ec8b119a 100644
--- a/bookshop/db/schema.cds
+++ b/bookshop/db/schema.cds
@@ -8,7 +8,7 @@ entity Books : managed {
author : Association to Authors;
genre : Association to Genres;
stock : Integer;
- price : Decimal(9,2);
+ price : Decimal;
currency : Currency;
image : LargeBinary @Core.MediaType : 'image/png';
}
diff --git a/bookshop/sqlite.db b/bookshop/sqlite.db
new file mode 100644
index 00000000..e69de29b
diff --git a/bookshop/srv/admin-service.js b/bookshop/srv/admin-service.js
new file mode 100644
index 00000000..0cdae4d8
--- /dev/null
+++ b/bookshop/srv/admin-service.js
@@ -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
+}
diff --git a/bookshop/srv/cat-service.cds b/bookshop/srv/cat-service.cds
index b95c1302..606eb05a 100644
--- a/bookshop/srv/cat-service.cds
+++ b/bookshop/srv/cat-service.cds
@@ -5,6 +5,10 @@ service CatalogService @(path:'/browse') {
author.name as author
} excluding { createdBy, modifiedBy };
+ @readonly entity ListOfBooks as SELECT from Books
+ excluding { descr, stock };
+
@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 };
}
diff --git a/bookshop/srv/cat-service.js b/bookshop/srv/cat-service.js
index 4352c5e7..9a955c5e 100644
--- a/bookshop/srv/cat-service.js
+++ b/bookshop/srv/cat-service.js
@@ -1,16 +1,18 @@
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
- const { Books } = db.entities // get reflected definitions
+class CatalogService extends cds.ApplicationService { async init(){
// Reduce stock of ordered books if available stock suffices
this.on ('submitOrder', async req => {
- const {book,amount} = req.data
- const n = await UPDATE (Books, book)
- .with ({ stock: {'-=': amount }})
- .where ({ stock: {'>=': amount }})
- n > 0 || req.error (409,`${amount} exceeds stock for book #${book}`)
+ const {book,amount} = req.data, tx = cds.tx(req)
+ let {stock} = await tx.read('stock').from(Books,book)
+ if (stock >= amount) {
+ await tx.update (Books,book).with ({ stock: stock -= amount })
+ 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
@@ -19,4 +21,8 @@ module.exports = async function (){
each.title += ` -- 11% discount!`
}
})
-}
+
+ return super.init()
+}}
+
+module.exports = { CatalogService }
diff --git a/fiori/app/browse/fiori-service.cds b/fiori/app/browse/fiori-service.cds
index 4f947fd4..f59a36b4 100644
--- a/fiori/app/browse/fiori-service.cds
+++ b/fiori/app/browse/fiori-service.cds
@@ -7,6 +7,8 @@ using CatalogService from '@capire/bookshop';
annotate CatalogService.Books with @(
UI: {
HeaderInfo: {
+ TypeName: 'Book',
+ TypeNamePlural: 'Books',
Description: {Value: author}
},
HeaderFacets: [
diff --git a/fiori/app/fiori.html b/fiori/app/fiori-apps.html
similarity index 98%
rename from fiori/app/fiori.html
rename to fiori/app/fiori-apps.html
index d011797c..7f7d9a3a 100644
--- a/fiori/app/fiori.html
+++ b/fiori/app/fiori-apps.html
@@ -28,7 +28,7 @@
navigationMode: "embedded"
},
"manage-orders": {
- title: "Order Books",
+ title: "Manage Orders",
description: "... testing FE v42",
additionalInformation: "SAPUI5.Component=orders",
applicationType : "URL",
diff --git a/fiori/app/index.cds b/fiori/app/index.cds
index 379e55ab..595023e9 100644
--- a/fiori/app/index.cds
+++ b/fiori/app/index.cds
@@ -4,7 +4,9 @@
using from './admin/fiori-service';
using from './browse/fiori-service';
-using from './orders/fiori-service';
using from './common';
using from '@capire/common';
+
+// only works in case of embedded orders service
+using from '@capire/orders/app/orders/fiori-service';
diff --git a/fiori/app/vue/index.html b/fiori/app/vue/index.html
new file mode 100644
index 00000000..de7f5a0f
--- /dev/null
+++ b/fiori/app/vue/index.html
@@ -0,0 +1 @@
+
diff --git a/fiori/db/schema.cds b/fiori/db/schema.cds
deleted file mode 100644
index 5d4e2e82..00000000
--- a/fiori/db/schema.cds
+++ /dev/null
@@ -1,3 +0,0 @@
-// Proxy for importing schema from bookshop sample
-using from '@capire/bookshop';
-namespace sap.capire.bookshop;
diff --git a/fiori/package.json b/fiori/package.json
index ca60b42e..a4028d2e 100644
--- a/fiori/package.json
+++ b/fiori/package.json
@@ -3,6 +3,7 @@
"version": "1.0.0",
"dependencies": {
"@capire/bookshop": "*",
+ "@capire/reviews": "*",
"@capire/orders": "*",
"@capire/common": "*",
"@sap/cds": "^4",
@@ -15,6 +16,12 @@
},
"cds": {
"requires": {
+ "ReviewsService": {
+ "kind": "odata", "model": "@capire/reviews"
+ },
+ "OrdersService": {
+ "kind": "odata", "model": "@capire/orders"
+ },
"db": {
"kind": "sql"
}
diff --git a/fiori/server.js b/fiori/server.js
new file mode 100644
index 00000000..c3663e0b
--- /dev/null
+++ b/fiori/server.js
@@ -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
diff --git a/fiori/srv/admin-service.cds b/fiori/srv/admin-service.cds
deleted file mode 100644
index eb518438..00000000
--- a/fiori/srv/admin-service.cds
+++ /dev/null
@@ -1,3 +0,0 @@
-// Proxy for importing services from bookshop sample
-using from '@capire/bookshop';
-annotate AdminService with @impl:'srv/admin-service.js';
diff --git a/fiori/srv/admin-service.js b/fiori/srv/admin-service.js
deleted file mode 100644
index e8853182..00000000
--- a/fiori/srv/admin-service.js
+++ /dev/null
@@ -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)
-})
diff --git a/fiori/srv/mashup.cds b/fiori/srv/mashup.cds
new file mode 100644
index 00000000..e9a41cd3
--- /dev/null
+++ b/fiori/srv/mashup.cds
@@ -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
+}
diff --git a/fiori/srv/mashup.js b/fiori/srv/mashup.js
new file mode 100644
index 00000000..3a72a2c8
--- /dev/null
+++ b/fiori/srv/mashup.js
@@ -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)
+ })
+}
diff --git a/reviewed/test/requests.http b/fiori/test/requests.http
similarity index 88%
rename from reviewed/test/requests.http
rename to fiori/test/requests.http
index 1831c08d..a9c18a97 100644
--- a/reviewed/test/requests.http
+++ b/fiori/test/requests.http
@@ -2,6 +2,8 @@
@me = {{$processEnv USER}}:
@bookshop = http://localhost:4004
@reviews-service = {{bookshop}}/reviews
+
+# Uncomment this when running separate reviews service
# @reviews-service = http://localhost:5005/reviews
@@ -20,7 +22,7 @@ POST {{reviews-service}}/Reviews
Content-Type: application/json;IEEE754Compatible=true
Authorization: Basic {{me}}
-{"subject":"201", "title":"boo"}
+{"subject":"201", "title":"boo" }
@@ -44,3 +46,9 @@ GET {{bookshop}}/browse/Books(201)?
&$select=ID,title,rating
&$expand=reviews
# Note: the $expand only works in case of ReviewsService in same process
+
+
+
+###
+
+GET {{bookshop}}/orders/Orders
diff --git a/orders/.env b/orders/.env
new file mode 100644
index 00000000..b9da5c42
--- /dev/null
+++ b/orders/.env
@@ -0,0 +1,2 @@
+cds.requires.messaging.kind = file-based-messaging
+PORT = 4005
\ No newline at end of file
diff --git a/orders/app/fiori-app.html b/orders/app/fiori-app.html
new file mode 100644
index 00000000..3f1640fe
--- /dev/null
+++ b/orders/app/fiori-app.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
Bookshop
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/orders/app/index.cds b/orders/app/index.cds
new file mode 100644
index 00000000..d64c2905
--- /dev/null
+++ b/orders/app/index.cds
@@ -0,0 +1,5 @@
+/*
+ This model controls what gets served to Fiori frontends...
+*/
+
+using from './orders/fiori-service';
diff --git a/fiori/app/orders/fiori-service.cds b/orders/app/orders/fiori-service.cds
similarity index 54%
rename from fiori/app/orders/fiori-service.cds
rename to orders/app/orders/fiori-service.cds
index cced0972..39a582a6 100644
--- a/fiori/app/orders/fiori-service.cds
+++ b/orders/app/orders/fiori-service.cds
@@ -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
- },
- ValueList.entity:'Books',
- );
- amount @(
- Common.FieldControl: #Mandatory
- );
-}
+////////////////////////////////////////////////////////////////////////////
+
+
+
+using { OrdersService, sap.capire.orders.OrderItems } from '../../srv/orders-service';
@odata.draft.enabled
annotate OrdersService.Orders with @(
UI: {
- ////////////////////////////////////////////////////////////////////////////
- //
- // Lists of Orders
- //
SelectionFields: [ createdAt, createdBy ],
LineItem: [
- {Value: createdBy, Label:'Customer'},
+ {Value: OrderNo, Label:'OrderNo'},
+ {Value: buyer, Label:'Customer'},
{Value: createdAt, Label:'Date'}
],
- ////////////////////////////////////////////////////////////////////////////
- //
- // Order Details
- //
HeaderInfo: {
TypeName: 'Order', TypeNamePlural: 'Orders',
Title: {
@@ -62,7 +45,7 @@ annotate OrdersService.Orders with @(
],
FieldGroup#Details: {
Data: [
- {Value: currency_code, Label:'Currency'}
+ {Value: currency.code, Label:'Currency'}
]
},
FieldGroup#Created: {
@@ -85,36 +68,25 @@ annotate OrdersService.Orders with @(
-//The enity types name is OrdersService.my_bookshop_OrderItems
-//The annotations below are not generated in edmx WHY?
-annotate OrdersService.OrderItems with @(
+annotate OrderItems with @(
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: [
- {Value: book_ID, Label:'Book'},
- //The following entry is only used to have the assoication followed in the read event
- {Value: book.price, Label:'Book Price'},
+ {Value: article, Label:'Article ID'},
+ {Value: title, Label:'Article Title'},
+ {Value: price, Label:'Unit Price'},
{Value: amount, Label:'Quantity'},
],
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: title, Label:'Article'},
+ {Value: price, Label:'Unit Price'},
],
Facets: [
{$Type: 'UI.ReferenceFacet', Label: '{i18n>OrderItems}', Target: '@UI.Identification'},
],
},
-);
\ No newline at end of file
+) {
+ amount @(
+ Common.FieldControl: #Mandatory
+ );
+};
diff --git a/fiori/app/orders/webapp/Component.js b/orders/app/orders/webapp/Component.js
similarity index 100%
rename from fiori/app/orders/webapp/Component.js
rename to orders/app/orders/webapp/Component.js
diff --git a/fiori/app/orders/webapp/i18n/i18n.properties b/orders/app/orders/webapp/i18n/i18n.properties
similarity index 100%
rename from fiori/app/orders/webapp/i18n/i18n.properties
rename to orders/app/orders/webapp/i18n/i18n.properties
diff --git a/fiori/app/orders/webapp/manifest.json b/orders/app/orders/webapp/manifest.json
similarity index 100%
rename from fiori/app/orders/webapp/manifest.json
rename to orders/app/orders/webapp/manifest.json
diff --git a/orders/db/data/sap.capire.bookshop-OrderItems.csv b/orders/db/data/sap.capire.bookshop-OrderItems.csv
deleted file mode 100644
index 25edab7a..00000000
--- a/orders/db/data/sap.capire.bookshop-OrderItems.csv
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/orders/db/data/sap.capire.bookshop-Orders.csv b/orders/db/data/sap.capire.bookshop-Orders.csv
deleted file mode 100644
index 088c1e87..00000000
--- a/orders/db/data/sap.capire.bookshop-Orders.csv
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/orders/db/data/sap.capire.orders-OrderItems.csv b/orders/db/data/sap.capire.orders-OrderItems.csv
new file mode 100644
index 00000000..ebfbcda4
--- /dev/null
+++ b/orders/db/data/sap.capire.orders-OrderItems.csv
@@ -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
\ No newline at end of file
diff --git a/orders/db/data/sap.capire.orders-Orders.csv b/orders/db/data/sap.capire.orders-Orders.csv
new file mode 100644
index 00000000..6ad3d700
--- /dev/null
+++ b/orders/db/data/sap.capire.orders-Orders.csv
@@ -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
\ No newline at end of file
diff --git a/orders/db/schema.cds b/orders/db/schema.cds
index 048aba28..347b502a 100644
--- a/orders/db/schema.cds
+++ b/orders/db/schema.cds
@@ -1,16 +1,19 @@
-using { sap.capire.bookshop.Books } from '@capire/bookshop';
-using { Currency, managed, cuid } from '@sap/cds/common';
-namespace sap.capire.bookshop;
+using { Currency, User, managed, cuid } from '@sap/cds/common';
+using from '@capire/common';
+namespace sap.capire.orders;
entity Orders : cuid, managed {
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;
}
-entity OrderItems : cuid {
- parent : Association to Orders;
- book : Association to Books;
+entity OrderItems {
+ key ID : UUID;
+ order : Association to Orders;
amount : Integer;
- netAmount : Decimal(9,2);
+ article : String; //> to allow for arbitrary keys
+ title : String;
+ price : Double;
}
diff --git a/orders/index.cds b/orders/index.cds
new file mode 100644
index 00000000..a79e9f65
--- /dev/null
+++ b/orders/index.cds
@@ -0,0 +1,6 @@
+/*
+ This model controls what gets exposed
+*/
+namespace sap.capire.orders;
+using from './srv/orders-service';
+using from './db/schema';
diff --git a/orders/package.json b/orders/package.json
index 242aefc4..e1c683af 100644
--- a/orders/package.json
+++ b/orders/package.json
@@ -1,4 +1,8 @@
{
"name": "@capire/orders",
- "version": "1.0.0"
+ "version": "1.0.0",
+ "dependencies": {
+ "@capire/common": "*",
+ "@sap/cds": "^4.3.0"
+ }
}
\ No newline at end of file
diff --git a/orders/srv/orders-service.cds b/orders/srv/orders-service.cds
index 936b5256..119373db 100644
--- a/orders/srv/orders-service.cds
+++ b/orders/srv/orders-service.cds
@@ -1,6 +1,5 @@
-using { sap.capire.bookshop as my } from '../db/schema';
+using { sap.capire.orders as my } from '../db/schema';
service OrdersService {
entity Orders as projection on my.Orders;
- entity Books as projection on my.Books;
}
diff --git a/orders/srv/orders-service.js b/orders/srv/orders-service.js
index bc401fd4..5cfa7c81 100644
--- a/orders/srv/orders-service.js
+++ b/orders/srv/orders-service.js
@@ -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
+ if (Items) for (let { article, amount } of Items) {
+ const { amount:before } = await cds.tx(req).run (
+ SELECT.one.from (OrderItems, oi => oi.amount) .where ({order_ID:ID, article})
+ )
+ if (amount != before) this.orderChanged (article, amount-before)
+ }
+ })
- // Reduce stock of ordered books if available stock suffices
- this.before ('CREATE', 'Orders', (req) => {
- const { Items: items } = req.data
- 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}`
+ 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
diff --git a/package.json b/package.json
index 1914f30e..b783f428 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,6 @@
"@capire/hello": "./hello",
"@capire/media": "./media",
"@capire/orders": "./orders",
- "@capire/reviewed": "./reviewed",
"@capire/reviews": "./reviews"
},
"devDependencies": {
diff --git a/reviewed/.env b/reviewed/.env
deleted file mode 100644
index 24a7ee31..00000000
--- a/reviewed/.env
+++ /dev/null
@@ -1 +0,0 @@
-cds.requires.messaging.kind = file-based-messaging
\ No newline at end of file
diff --git a/reviewed/db/schema.cds b/reviewed/db/schema.cds
deleted file mode 100644
index 7553b3fa..00000000
--- a/reviewed/db/schema.cds
+++ /dev/null
@@ -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;
diff --git a/reviewed/package.json b/reviewed/package.json
deleted file mode 100644
index 363d42e3..00000000
--- a/reviewed/package.json
+++ /dev/null
@@ -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"
- }
- }
- }
-}
diff --git a/reviewed/server.js b/reviewed/server.js
deleted file mode 100644
index 1d59efb9..00000000
--- a/reviewed/server.js
+++ /dev/null
@@ -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
diff --git a/reviews/db/schema.cds b/reviews/db/schema.cds
index e49f90ca..456ef13d 100644
--- a/reviews/db/schema.cds
+++ b/reviews/db/schema.cds
@@ -17,7 +17,7 @@ entity Reviews {
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;
Good = 4;
Avg = 3;
diff --git a/samples.md b/samples.md
index efa74a61..d3441919 100644
--- a/samples.md
+++ b/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.
-## [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).
-## [bookshop](bookshop)
+## [@capire/bookshop](bookshop)
- [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)
@@ -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)
-## [common](common)
+## [@capire/common](common)
- 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)
@@ -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)
-## [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
- [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...
- 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
-## [fiori](fiori)
+## [@capire/fiori](fiori)
-- [Adds a Fiori elements application](https://cap.cloud.sap/docs/guides/fiori/), 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 Fiori apps locally
-- Combining most of the other samples through [package reuse](https://cap.cloud.sap/docs/get-started/projects#sharing-and-reusing-content)
+- 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
+ - 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 Fiori apps locally
+- [The Vue.js app](bookshop/app/vue) imported from bookshop is served as well