From f2c37ec162af0f965fe8e0c8f5ac00952cdf0266 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 16 Nov 2020 22:27:40 +0100 Subject: [PATCH] composites --- LICENSES/Apache-2.0.txt => LICENSE | 0 bookshop/app/vue/app.js | 28 ++++---- bookshop/app/vue/index.html | 6 +- bookshop/db/schema.cds | 2 +- bookshop/sqlite.db | 0 bookshop/srv/admin-service.js | 12 ++++ bookshop/srv/cat-service.cds | 6 +- bookshop/srv/cat-service.js | 24 ++++--- fiori/app/browse/fiori-service.cds | 2 + fiori/app/{fiori.html => fiori-apps.html} | 2 +- fiori/app/index.cds | 4 +- fiori/app/vue/index.html | 1 + fiori/db/schema.cds | 3 - fiori/package.json | 7 ++ fiori/server.js | 17 +++++ fiori/srv/admin-service.cds | 3 - fiori/srv/admin-service.js | 8 --- fiori/srv/mashup.cds | 25 +++++++ fiori/srv/mashup.js | 59 +++++++++++++++ {reviewed => fiori}/test/requests.http | 10 ++- orders/.env | 2 + orders/app/fiori-app.html | 39 ++++++++++ orders/app/index.cds | 5 ++ .../app/orders/fiori-service.cds | 72 ++++++------------- .../app/orders/webapp/Component.js | 0 .../app/orders/webapp/i18n/i18n.properties | 0 .../app/orders/webapp/manifest.json | 0 .../data/sap.capire.bookshop-OrderItems.csv | 4 -- orders/db/data/sap.capire.bookshop-Orders.csv | 3 - .../db/data/sap.capire.orders-OrderItems.csv | 4 ++ orders/db/data/sap.capire.orders-Orders.csv | 3 + orders/db/schema.cds | 19 ++--- orders/index.cds | 6 ++ orders/package.json | 6 +- orders/srv/orders-service.cds | 3 +- orders/srv/orders-service.js | 48 ++++++++----- package.json | 1 - reviewed/.env | 1 - reviewed/db/schema.cds | 16 ----- reviewed/package.json | 20 ------ reviewed/server.js | 33 --------- reviews/db/schema.cds | 2 +- samples.md | 31 ++++---- 43 files changed, 325 insertions(+), 212 deletions(-) rename LICENSES/Apache-2.0.txt => LICENSE (100%) create mode 100644 bookshop/sqlite.db create mode 100644 bookshop/srv/admin-service.js rename fiori/app/{fiori.html => fiori-apps.html} (98%) create mode 100644 fiori/app/vue/index.html delete mode 100644 fiori/db/schema.cds create mode 100644 fiori/server.js delete mode 100644 fiori/srv/admin-service.cds delete mode 100644 fiori/srv/admin-service.js create mode 100644 fiori/srv/mashup.cds create mode 100644 fiori/srv/mashup.js rename {reviewed => fiori}/test/requests.http (88%) create mode 100644 orders/.env create mode 100644 orders/app/fiori-app.html create mode 100644 orders/app/index.cds rename {fiori => orders}/app/orders/fiori-service.cds (54%) rename {fiori => orders}/app/orders/webapp/Component.js (100%) rename {fiori => orders}/app/orders/webapp/i18n/i18n.properties (100%) rename {fiori => orders}/app/orders/webapp/manifest.json (100%) delete mode 100644 orders/db/data/sap.capire.bookshop-OrderItems.csv delete mode 100644 orders/db/data/sap.capire.bookshop-Orders.csv create mode 100644 orders/db/data/sap.capire.orders-OrderItems.csv create mode 100644 orders/db/data/sap.capire.orders-Orders.csv create mode 100644 orders/index.cds delete mode 100644 reviewed/.env delete mode 100644 reviewed/db/schema.cds delete mode 100644 reviewed/package.json delete mode 100644 reviewed/server.js 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