From d0d08b1ee1fc4934c6686936a7de5a9e6daf2068 Mon Sep 17 00:00:00 2001 From: Daniel Hutzel Date: Thu, 12 Nov 2020 23:56:04 +0100 Subject: [PATCH 01/25] Enable authorizations w/ dummy-auth (#158) * Enable authorizations w/ dummy-auth * fixed: some tests run in privileged mode * Fixed tests to skip auth * npm test --silent * Added dependency to passport --- bookshop/package.json | 3 ++- bookshop/srv/admin-service.cds | 2 +- bookshop/srv/cat-service.cds | 2 +- bookshop/test/requests.http | 3 ++- fiori/package.json | 3 ++- package.json | 4 ++-- reviewed/test/requests.http | 13 +++++++++---- reviews/srv/reviews-service.cds | 4 ++-- test/custom-handlers.test.js | 2 +- test/localized-data.test.js | 2 +- test/messaging.test.js | 2 +- test/odata.test.js | 2 +- 12 files changed, 25 insertions(+), 17 deletions(-) diff --git a/bookshop/package.json b/bookshop/package.json index 6601ca5c..53be832a 100644 --- a/bookshop/package.json +++ b/bookshop/package.json @@ -5,7 +5,8 @@ "dependencies": { "@capire/common": "*", "@sap/cds": "^4", - "express": "^4.17.1" + "express": "^4.17.1", + "passport": "0.4.1" }, "scripts": { "genres": "cds serve test/genres.cds", diff --git a/bookshop/srv/admin-service.cds b/bookshop/srv/admin-service.cds index 8939262f..ea9b0731 100644 --- a/bookshop/srv/admin-service.cds +++ b/bookshop/srv/admin-service.cds @@ -1,5 +1,5 @@ using { sap.capire.bookshop as my } from '../db/schema'; -service AdminService @(requires_:'admin') { +service AdminService @(requires:'admin') { entity Books as projection on my.Books; entity Authors as projection on my.Authors; } diff --git a/bookshop/srv/cat-service.cds b/bookshop/srv/cat-service.cds index 3cbaaa8f..b95c1302 100644 --- a/bookshop/srv/cat-service.cds +++ b/bookshop/srv/cat-service.cds @@ -5,6 +5,6 @@ service CatalogService @(path:'/browse') { author.name as author } excluding { createdBy, modifiedBy }; - @requires_: 'authenticated-user' + @requires: 'authenticated-user' action submitOrder (book : Books:ID, amount: Integer); } diff --git a/bookshop/test/requests.http b/bookshop/test/requests.http index 6c6428a0..1fbdc0ca 100644 --- a/bookshop/test/requests.http +++ b/bookshop/test/requests.http @@ -36,6 +36,7 @@ Authorization: Basic alice: # Create book POST {{server}}/admin/Books Content-Type: application/json;IEEE754Compatible=true +Authorization: Basic alice: { "ID": 2, @@ -53,6 +54,7 @@ Content-Type: application/json;IEEE754Compatible=true # Put image to books PUT {{server}}/admin/Books(2)/image Content-Type: image/png +Authorization: Basic alice: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANwAAADcCAYAAAAbWs+BAAAGwElEQVR4Ae3cwZFbNxBFUY5rkrDTmKAUk5QT03Aa44U22KC7NHptw+DRikVAXf8fzC3u8Hj4R4AAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAgZzAW26USQT+e4HPx+Mz+RRvj0e0kT+SD2cWAQK1gOBqH6sEogKCi3IaRqAWEFztY5VAVEBwUU7DCNQCgqt9rBKICgguymkYgVpAcLWPVQJRAcFFOQ0jUAsIrvaxSiAqILgop2EEagHB1T5WCUQFBBflNIxALSC42scqgaiA4KKchhGoBQRX+1glEBUQXJTTMAK1gOBqH6sEogKCi3IaRqAWeK+Xb1z9iN558fHxcSPS9p2ezx/ROz4e4TtIHt+3j/61hW9f+2+7/+UXbifjewIDAoIbQDWSwE5AcDsZ3xMYEBDcAKqRBHYCgtvJ+J7AgIDgBlCNJLATENxOxvcEBgQEN4BqJIGdgOB2Mr4nMCAguAFUIwnsBAS3k/E9gQEBwQ2gGklgJyC4nYzvCQwICG4A1UgCOwHB7WR8T2BAQHADqEYS2AkIbifjewIDAoIbQDWSwE5AcDsZ3xMYEEjfTzHwiK91B8npd6Q8n8/oGQ/ckRJ9vvQwv3BpUfMIFAKCK3AsEUgLCC4tah6BQkBwBY4lAmkBwaVFzSNQCAiuwLFEIC0guLSoeQQKAcEVOJYIpAUElxY1j0AhILgCxxKBtIDg0qLmESgEBFfgWCKQFhBcWtQ8AoWA4AocSwTSAoJLi5pHoBAQXIFjiUBaQHBpUfMIFAKCK3AsEUgLCC4tah6BQmDgTpPsHSTFs39p6fQ7Q770UsV/Ov19X+2OFL9wxR+rJQJpAcGlRc0jUAgIrsCxRCAtILi0qHkECgHBFTiWCKQFBJcWNY9AISC4AscSgbSA4NKi5hEoBARX4FgikBYQXFrUPAKFgOAKHEsE0gKCS4uaR6AQEFyBY4lAWkBwaVHzCBQCgitwLBFICwguLWoegUJAcAWOJQJpAcGlRc0jUAgIrsCxRCAt8J4eePq89B0ar3ZnyOnve/rfn1+400/I810lILirjtPLnC4guNNPyPNdJSC4q47Ty5wuILjTT8jzXSUguKuO08ucLiC400/I810lILirjtPLnC4guNNPyPNdJSC4q47Ty5wuILjTT8jzXSUguKuO08ucLiC400/I810lILirjtPLnC4guNNPyPNdJSC4q47Ty5wuILjTT8jzXSUguKuO08ucLiC400/I810l8JZ/m78+szP/zI47fJo7Q37vgJ7PHwN/07/3TOv/9gu3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhAcMPAxhNYBQS3avhMYFhg4P6H9J0maYHXuiMlrXf+vOfA33Turf3C5SxNItAKCK4lsoFATkBwOUuTCLQCgmuJbCCQExBcztIkAq2A4FoiGwjkBASXszSJQCsguJbIBgI5AcHlLE0i0AoIriWygUBOQHA5S5MItAKCa4lsIJATEFzO0iQCrYDgWiIbCOQEBJezNIlAKyC4lsgGAjkBweUsTSLQCgiuJbKBQE5AcDlLkwi0Akff//Dz6U+/I6U1/sUNr3bnytl3kPzi4bXb/cK1RDYQyAkILmdpEoFWQHAtkQ0EcgKCy1maRKAVEFxLZAOBnIDgcpYmEWgFBNcS2UAgJyC4nKVJBFoBwbVENhDICQguZ2kSgVZAcC2RDQRyAoLLWZpEoBUQXEtkA4GcgOByliYRaAUE1xLZQCAnILicpUkEWgHBtUQ2EMgJCC5naRKBVkBwLZENBHIC/4M7TXIv+3PS22d24qvdQfL3C/7N5P5i/MLlLE0i0AoIriWygUBOQHA5S5MItAKCa4lsIJATEFzO0iQCrYDgWiIbCOQEBJezNIlAKyC4lsgGAjkBweUsTSLQCgiuJbKBQE5AcDlLkwi0AoJriWwgkBMQXM7SJAKtgOBaIhsI5AQEl7M0iUArILiWyAYCOQHB5SxNItAKCK4lsoFATkBwOUuTCBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIAAAQIECBAgQIDAvyrwDySEJ2VQgUSoAAAAAElFTkSuQmCC @@ -78,4 +80,3 @@ GET {{server}}/browse/Genres? # &$filter=parent_ID eq null&$select=name # &$expand=children($select=name) {{me}} - diff --git a/fiori/package.json b/fiori/package.json index 1c0eec27..ca60b42e 100644 --- a/fiori/package.json +++ b/fiori/package.json @@ -6,7 +6,8 @@ "@capire/orders": "*", "@capire/common": "*", "@sap/cds": "^4", - "express": "^4.17.1" + "express": "^4.17.1", + "passport": "0.4.1" }, "scripts": { "start": "cds run --in-memory?", diff --git a/package.json b/package.json index f81fc148..1914f30e 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ "fiori": "cds watch fiori", "media": "cds watch media", "mocha": "npx mocha || echo", - "jest": "npx jest --verbose", - "test": "npm run jest -s" + "jest": "npx jest", + "test": "npm run jest --silent" }, "mocha": { "parallel": true diff --git a/reviewed/test/requests.http b/reviewed/test/requests.http index 7a9d711c..1831c08d 100644 --- a/reviewed/test/requests.http +++ b/reviewed/test/requests.http @@ -1,10 +1,16 @@ + +@me = {{$processEnv USER}}: +@bookshop = http://localhost:4004 +@reviews-service = {{bookshop}}/reviews +# @reviews-service = http://localhost:5005/reviews + + + ################################################# # # To ReviewsService # # move the right down: -@reviews-service = http://localhost:4004/reviews -@reviews-service = http://localhost:5005/reviews ### Get all reviews GET {{reviews-service}}/Reviews @@ -12,6 +18,7 @@ GET {{reviews-service}}/Reviews ### Add a new review (with random rating) POST {{reviews-service}}/Reviews Content-Type: application/json;IEEE754Compatible=true +Authorization: Basic {{me}} {"subject":"201", "title":"boo"} @@ -23,8 +30,6 @@ Content-Type: application/json;IEEE754Compatible=true # (both in-process as well as separate one) # -@bookshop = http://localhost:4004 - ### Request to CatalogService > delegated to ReviewsService GET {{bookshop}}/browse/Books(201)/reviews? &$select=rating,date,reviewer,title diff --git a/reviews/srv/reviews-service.cds b/reviews/srv/reviews-service.cds index 44fa83e2..17a46578 100644 --- a/reviews/srv/reviews-service.cds +++ b/reviews/srv/reviews-service.cds @@ -24,14 +24,14 @@ service ReviewsService { // Access control restrictions -annotate ReviewsService.Reviews with @restrict_:[ +annotate ReviewsService.Reviews with @restrict:[ { grant:'READ', to:'any' }, // everybody can read reviews { grant:'CREATE', to:'authenticated-user' }, // users must login to add reviews { grant:'UPDATE', to:'authenticated-user', where:'reviewer=$user' }, { grant:'DELETE', to:'admin' }, ]; -annotate ReviewsService with @restrict_:[ +annotate ReviewsService with @restrict:[ { grant:'like', to:'identified-user' }, { grant:'unlike', to:'identified-user', where:'user=$user' }, ]; diff --git a/test/custom-handlers.test.js b/test/custom-handlers.test.js index 8e798d97..fd38c91f 100644 --- a/test/custom-handlers.test.js +++ b/test/custom-handlers.test.js @@ -1,4 +1,4 @@ -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds/lib'); cds.User = cds.User.Privileged // skip auth const { GET, POST, expect } = cds.test('bookshop').in(__dirname,'..') describe('Custom Handlers', () => { diff --git a/test/localized-data.test.js b/test/localized-data.test.js index ad8256b9..c31fd43a 100644 --- a/test/localized-data.test.js +++ b/test/localized-data.test.js @@ -1,4 +1,4 @@ -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds/lib'); cds.User = cds.User.Privileged // skip auth const { GET, expect } = cds.test ('serve', __dirname+'/localized-data.cds', '--in-memory') describe('Localized Data', () => { diff --git a/test/messaging.test.js b/test/messaging.test.js index a3e0df3e..78866a37 100644 --- a/test/messaging.test.js +++ b/test/messaging.test.js @@ -2,7 +2,7 @@ const cds = require('@sap/cds/lib') const cwd = process.cwd(); process.chdir (__dirname) //> only for internal CI/CD@SAP const {expect} = cds.test const _model = '@capire/reviews' - +cds.User = cds.User.Privileged // hard core monkey patch describe('Messaging', ()=>{ diff --git a/test/odata.test.js b/test/odata.test.js index 4a561a95..db24ea3d 100644 --- a/test/odata.test.js +++ b/test/odata.test.js @@ -1,4 +1,4 @@ -const cds = require('@sap/cds/lib') +const cds = require('@sap/cds/lib'); cds.User = cds.User.Privileged // skip auth const { GET, expect } = cds.test('bookshop').in(__dirname,'..') describe('OData Protocol', () => { From f2c37ec162af0f965fe8e0c8f5ac00952cdf0266 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 16 Nov 2020 22:27:40 +0100 Subject: [PATCH 02/25] 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
From d80084bfb7f87d359ce01b2820f63e4aeeefa0c1 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 17 Nov 2020 17:41:55 +0100 Subject: [PATCH 03/25] Added @capire:registry --- .registry/server.js | 71 +++++++++++++++++++++++++++++++++++++++++++++ README.md | 16 ++++++++++ package.json | 1 + 3 files changed, 88 insertions(+) create mode 100644 .registry/server.js diff --git a/.registry/server.js b/.registry/server.js new file mode 100644 index 00000000..c1461016 --- /dev/null +++ b/.registry/server.js @@ -0,0 +1,71 @@ +const { exec } = require ('child_process') +const express = require ('express') +const fs = require ('fs') +const app = express() + +const { PORT=4444 } = process.env +const [,,port=PORT] = process.argv + +app.use('/-/:tarball', (req,res,next) => { + const url = decodeURIComponent(req.url) + console.debug ('GET', req.params) + try { + const { tarball } = req.params + const [, pkg ] = /^capire-(\w+)/.exec(tarball) + fs.lstat(tarball,(err => { + if (err) exec(`npm pack ../${pkg}`,next) + else next() + })) + } catch (e) { + console.error(e) + res.sendStatus(500) + } +}) + +app.use('/-', express.static(__dirname)) + +app.get('/*', (req,res)=>{ + const url = decodeURIComponent(req.url) + console.debug ('GET',url) + try { + const [, capire, pkg ] = /^\/(@capire)\/(\w+)/.exec(url) + const package = require (`${capire}/${pkg}/package.json`) + const tarball = `capire-${pkg}-${package.version}.tgz` + // https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md + res.json({ + "name": package.name, + "dist-tags": { + "latest": package.version + }, + "versions": { + [package.version]: { + "name": package.name, + "version": package.version, + "dist": { + "tarball": `http://localhost:${port}/-/${tarball}` + }, + } + }, + }) + } catch (e) { + console.error(e) + res.sendStatus(404) + } +}) + +app.listen(port, ()=>{ + console.log (`npm set @capire:registry=http://localhost:${port}`) + console.log (`@capire registry listening on http://localhost:${port}`) + exec(`npm set @capire:registry=http://localhost:${port}`) +}) + +const _exit = ()=>{ + console.log ('\nnpm conf rm @capire:registry') + exec('npm conf rm @capire:registry') + exec('rm *.tgz') + process.exit() +} +process.on ('SIGTERM',_exit) +process.on ('SIGHUP',_exit) +process.on ('SIGINT',_exit) +process.on ('SIGUSR2',_exit) diff --git a/README.md b/README.md index 01d8fdf1..9f7773c4 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,22 @@ npx jest > While mocha is a bit smaller and faster, jest runs tests in parallel and isolation, which allows to run all tests. +### Serve `npm` + +We've simple npm registry mock included which allows you to do an `npm install @capire/` anywhere locally. Use it as follows: + +1. Start the @capire registry: +```sh +npm run registry +``` +> While running this will have `@capire:registry=http://localhost:4444` set with npmrc. + +2. Install one of the @capire packages wherever you like, e.g.: +```sh +npm add @capire/common @capire/bookshop +``` + + ## Get Support Check out the documentation at [https://cap.cloud.sap](https://cap.cloud.sap).
diff --git a/package.json b/package.json index b783f428..21fbea51 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "sqlite3": "^5" }, "scripts": { + "registry": "cd .registry && node server.js", "bookshop": "cds watch bookshop", "fiori": "cds watch fiori", "media": "cds watch media", From 932f56812c9eb5e561253962de533d1edf36fd79 Mon Sep 17 00:00:00 2001 From: Daniel Hutzel Date: Tue, 17 Nov 2020 20:27:10 +0100 Subject: [PATCH 04/25] Update server.js --- fiori/server.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/fiori/server.js b/fiori/server.js index c3663e0b..8f71c3f8 100644 --- a/fiori/server.js +++ b/fiori/server.js @@ -1,15 +1,14 @@ 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)=>{ + const {dirname} = require ('path') // serving the orders app imported from @capire/orders - app.use ('/orders/webapp', _imported('@capire/orders/app/orders/webapp','manifest.json')) + const orders_app = dirname (require.resolve('@capire/orders/app/orders/webapp/manifest.json')) + app.use ('/orders/webapp', express.static(orders_app)) // serving the vue.js app imported from @capire/bookshop - app.use ('/vue', _imported('@capire/bookshop/app/vue','index.html')) + const vue_app = dirname (require.resolve('@capire/bookshop/app/vue/index.html')) + app.use ('/vue', express.static(vue_app)) }) cds.once('served', require('./srv/mashup')) From 8f01bf911e80102c8211d488f3bcd5a4ead9258f Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 20 Nov 2020 12:33:18 +0100 Subject: [PATCH 05/25] Using fake Products entity in @capire/orders --- fiori/srv/mashup.cds | 4 ++-- fiori/srv/mashup.js | 6 +++--- orders/app/orders/fiori-service.cds | 6 +++--- orders/db/data/sap.capire.orders-OrderItems.csv | 2 +- orders/db/schema.cds | 9 ++++++++- orders/srv/orders-service.js | 16 ++++++++-------- 6 files changed, 25 insertions(+), 18 deletions(-) diff --git a/fiori/srv/mashup.cds b/fiori/srv/mashup.cds index e9a41cd3..abcf0547 100644 --- a/fiori/srv/mashup.cds +++ b/fiori/srv/mashup.cds @@ -16,10 +16,10 @@ extend Books with { } // -// Extend Orders with Books as articles +// Extend Orders with Books as Products // using { sap.capire.orders.OrderItems } from '@capire/orders'; extend OrderItems with { - book : Association to Books on article = book.ID + book : Association to Books on product.ID = book.ID } diff --git a/fiori/srv/mashup.js b/fiori/srv/mashup.js index 3a72a2c8..7a46fcc6 100644 --- a/fiori/srv/mashup.js +++ b/fiori/srv/mashup.js @@ -31,7 +31,7 @@ module.exports = async()=>{ // called by server.js 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 }], + Items: [{ product:{ID:`${book}`}, title, price, amount }], buyer, createdBy: buyer }) }) @@ -51,8 +51,8 @@ module.exports = async()=>{ // called by server.js // OrdersService.on ('OrderChanged', async (msg) => { console.debug ('> received:', msg.event, msg.data) - const { article, deltaAmount } = msg.data - return UPDATE (Books) .where ('ID =', article) + const { product, deltaAmount } = msg.data + return UPDATE (Books) .where ('ID =', product) .and ('stock >=', deltaAmount) .set ('stock -=', deltaAmount) }) diff --git a/orders/app/orders/fiori-service.cds b/orders/app/orders/fiori-service.cds index 39a582a6..99356ea9 100644 --- a/orders/app/orders/fiori-service.cds +++ b/orders/app/orders/fiori-service.cds @@ -71,14 +71,14 @@ annotate OrdersService.Orders with @( annotate OrderItems with @( UI: { LineItem: [ - {Value: article, Label:'Article ID'}, - {Value: title, Label:'Article Title'}, + {Value: product_ID, Label:'Product ID'}, + {Value: title, Label:'Product Title'}, {Value: price, Label:'Unit Price'}, {Value: amount, Label:'Quantity'}, ], Identification: [ //Is the main field group {Value: amount, Label:'Amount'}, - {Value: title, Label:'Article'}, + {Value: title, Label:'Product'}, {Value: price, Label:'Unit Price'}, ], Facets: [ diff --git a/orders/db/data/sap.capire.orders-OrderItems.csv b/orders/db/data/sap.capire.orders-OrderItems.csv index ebfbcda4..a3349b28 100644 --- a/orders/db/data/sap.capire.orders-OrderItems.csv +++ b/orders/db/data/sap.capire.orders-OrderItems.csv @@ -1,4 +1,4 @@ -ID;order_ID;amount;article;title;price +ID;order_ID;amount;product_ID;title;price 58040e66-1dcd-4ffb-ab10-fdce32028b79;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;201;Wuthering Heights;11.11 64e718c9-ff99-47f1-8ca3-950c850777d4;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;271;Catweazle;15 e9641166-e050-4261-bfee-d1e797e6cb7f;64e718c9-ff99-47f1-8ca3-950c850777d4;2;252;Eleonora;28 \ No newline at end of file diff --git a/orders/db/schema.cds b/orders/db/schema.cds index 347b502a..1cde2e22 100644 --- a/orders/db/schema.cds +++ b/orders/db/schema.cds @@ -12,8 +12,15 @@ entity Orders : cuid, managed { entity OrderItems { key ID : UUID; order : Association to Orders; + @assert.integrity:false // REVISIT: this is a temporary workaround for a glitch in cds-runtime + product : Association to Products; amount : Integer; - article : String; //> to allow for arbitrary keys title : String; price : Double; } + +/** This is a stand-in for arbitrary ordered Products */ +@cds.persistence.skip:'always' +entity Products { + key ID : String; +} diff --git a/orders/srv/orders-service.js b/orders/srv/orders-service.js index 5cfa7c81..ed42770f 100644 --- a/orders/srv/orders-service.js +++ b/orders/srv/orders-service.js @@ -7,30 +7,30 @@ class OrdersService extends cds.ApplicationService { this.before ('UPDATE', 'Orders', async function(req) { const { ID, Items } = req.data - if (Items) for (let { article, amount } of Items) { + if (Items) for (let { product_ID, amount } of Items) { const { amount:before } = await cds.tx(req).run ( - SELECT.one.from (OrderItems, oi => oi.amount) .where ({order_ID:ID, article}) + SELECT.one.from (OrderItems, oi => oi.amount) .where ({order_ID:ID, product_ID}) ) - if (amount != before) this.orderChanged (article, amount-before) + if (amount != before) this.orderChanged (product_ID, amount-before) } }) this.before ('DELETE', 'Orders', async function(req) { const { ID } = req.data const Items = await cds.tx(req).run ( - SELECT.from (OrderItems, oi => { oi.article, oi.amount }) .where ({order_ID:ID}) + SELECT.from (OrderItems, oi => { oi.product_ID, oi.amount }) .where ({order_ID:ID}) ) - if (Items) for (let it of Items) this.orderChanged (it.article, -it.amount) + if (Items) for (let it of Items) this.orderChanged (it.product_ID, -it.amount) }) return super.init() } /** order changed -> broadcast event */ - orderChanged (article, deltaAmount) { + orderChanged (product, deltaAmount) { // Emit events to inform subscribers about changes in orders - console.log ('> emitting:', 'OrderChanged', { article, deltaAmount }) - return this.emit ('OrderChanged', { article, deltaAmount }) + console.log ('> emitting:', 'OrderChanged', { product, deltaAmount }) + return this.emit ('OrderChanged', { product, deltaAmount }) } } From dae8e96fe1d932b4900af85179a7e83e798a1a5b Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 19 Nov 2020 12:41:11 +0100 Subject: [PATCH 06/25] Adding Vue.js apps for reviews service --- .vscode/launch.json | 9 +++ bookshop/app/vue/app.js | 4 +- bookshop/app/vue/index.html | 38 +++++----- fiori/.env | 2 + fiori/test/requests.http | 36 ++++------ orders/.env | 2 +- reviews/.env | 2 +- reviews/app/vue/app.js | 72 +++++++++++++++++++ reviews/app/vue/index.html | 62 ++++++++++++++++ .../db/data/sap.capire.reviews-Reviews.csv | 5 ++ reviews/index.cds | 1 + reviews/srv/reviews-service.cds | 9 ++- test/messaging.test.js | 1 + 13 files changed, 198 insertions(+), 45 deletions(-) create mode 100644 fiori/.env create mode 100644 reviews/app/vue/app.js create mode 100644 reviews/app/vue/index.html create mode 100644 reviews/db/data/sap.capire.reviews-Reviews.csv diff --git a/.vscode/launch.json b/.vscode/launch.json index d0f0e8eb..ad51dcef 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,15 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "Attach by Process ID", + "processId": "${command:PickProcess}", + "request": "attach", + "skipFiles": [ + "/**" + ], + "type": "pwa-node" + }, { "name": "bookshop", "command": "cds watch bookshop", diff --git a/bookshop/app/vue/app.js b/bookshop/app/vue/app.js index e7aedcaa..78bd342b 100644 --- a/bookshop/app/vue/app.js +++ b/bookshop/app/vue/app.js @@ -9,7 +9,7 @@ const books = new Vue ({ data: { list: [], - book: { descr:'( click on a row to see details... )' }, + book: undefined, order: { amount:1, succeeded:'', failed:'' } }, @@ -31,7 +31,7 @@ const books = new Vue ({ }, async submitOrder () { - const {book,order} = books, amount = parseInt (order.amount) || 1 + const {book,order} = books, amount = parseInt (order.amount) || 1 // REVISIT: Okra should be less strict try { const res = await POST(`/submitOrder`, { amount, book: book.ID }) book.stock = res.data.stock diff --git a/bookshop/app/vue/index.html b/bookshop/app/vue/index.html index f4753234..56c79a07 100644 --- a/bookshop/app/vue/index.html +++ b/bookshop/app/vue/index.html @@ -7,22 +7,21 @@
-

Capire Books

+

{{ document.title }}

- +
@@ -34,29 +33,30 @@ - +
Book Author {{ book.title }} {{ book.author }} {{ book.genre.name }}{{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }} + {{ ('★'.repeat(Math.round(book.rating))+'☆☆☆☆☆').slice(0,5) }} + {{ book.currency.symbol }} {{ book.price }}
-
+
-
- -
-
- + +
+

{{ book.title }}

+

{{ book.descr }}

+
+
+ ( click on a row to see details... )
- -

{{ book.title }}

-

{{ book.descr }}

diff --git a/fiori/.env b/fiori/.env new file mode 100644 index 00000000..36644fa6 --- /dev/null +++ b/fiori/.env @@ -0,0 +1,2 @@ +# cds.requires.messaging.kind = file-based-messaging +PORT = 4004 \ No newline at end of file diff --git a/fiori/test/requests.http b/fiori/test/requests.http index a9c18a97..d759aedf 100644 --- a/fiori/test/requests.http +++ b/fiori/test/requests.http @@ -1,54 +1,48 @@ -@me = {{$processEnv USER}}: @bookshop = http://localhost:4004 @reviews-service = {{bookshop}}/reviews - -# Uncomment this when running separate reviews service -# @reviews-service = http://localhost:5005/reviews +# Uncomment this when running a separate reviews service +@reviews-service = http://localhost:4005/reviews ################################################# # -# To ReviewsService +# Reviews Service # -# move the right down: -### Get all reviews GET {{reviews-service}}/Reviews -### Add a new review (with random rating) -POST {{reviews-service}}/Reviews -Content-Type: application/json;IEEE754Compatible=true -Authorization: Basic {{me}} +### -{"subject":"201", "title":"boo" } +POST {{reviews-service}}/Reviews +Authorization: Basic {{$processEnv USER}}: +Content-Type: application/json + +{"subject":"201", "title":"boo", "rating":3 } ################################################# # -# Bookshop Requests involving reviews -# (both in-process as well as separate one) +# Bookshop Services # -### Request to CatalogService > delegated to ReviewsService -GET {{bookshop}}/browse/Books(201)/reviews? -&$select=rating,date,reviewer,title - -### Alternative OData URL GET {{bookshop}}/browse/Books/201/reviews? &$select=rating,date,title &$top=3 ### + GET {{bookshop}}/browse/Books(201)? &$select=ID,title,rating &$expand=reviews -# Note: the $expand only works in case of ReviewsService in same process -### +################################################# +# +# Bookshop Services +# GET {{bookshop}}/orders/Orders diff --git a/orders/.env b/orders/.env index b9da5c42..616dd8d0 100644 --- a/orders/.env +++ b/orders/.env @@ -1,2 +1,2 @@ cds.requires.messaging.kind = file-based-messaging -PORT = 4005 \ No newline at end of file +PORT = 4006 \ No newline at end of file diff --git a/reviews/.env b/reviews/.env index 8184d48d..b9da5c42 100644 --- a/reviews/.env +++ b/reviews/.env @@ -1,2 +1,2 @@ cds.requires.messaging.kind = file-based-messaging -PORT = 5005 \ No newline at end of file +PORT = 4005 \ No newline at end of file diff --git a/reviews/app/vue/app.js b/reviews/app/vue/app.js new file mode 100644 index 00000000..fb115d68 --- /dev/null +++ b/reviews/app/vue/app.js @@ -0,0 +1,72 @@ +/* global Vue axios */ //> from vue.html +const $ = sel => document.querySelector(sel) +const GET = (url) => axios.get('/reviews'+url) +const PUT = (cmd,data) => axios.patch('/reviews'+cmd,data) +const POST = (cmd,data) => axios.post('/reviews'+cmd,data) + +const reviews = new Vue ({ + + el:'#app', + + data: { + list: [], + review: undefined, + message: {}, + Ratings: Object.entries({ + 5 : '★★★★★', + 4 : '★★★★', + 3 : '★★★', + 2 : '★★', + 1 : '★', + }).reverse() + }, + + methods: { + + search: ({target:{value:v}}) => reviews.fetch(v && '&$search='+v), + + async fetch (etc='') { + const {data} = await GET(`/Reviews?${etc}`) + reviews.list = data.value + }, + + async inspect (eve) { + const review = reviews.review = reviews.list [eve.currentTarget.rowIndex-1] + const res = await GET(`/Reviews/${review.ID}/text/$value`) + review.text = res.data + reviews.message = {} + }, + + async newReview () { + reviews.review = {} + reviews.message = {} + setTimeout (()=> $('form > input').focus(), 111) + }, + + async submitReview () { + const review = reviews.review; review.rating = parseInt (review.rating) // REVISIT: Okra should be less strict + try { + if (!review.ID) { + const res = await POST(`/Reviews`,review) + reviews.ID = res.data.ID + } else { + console.trace() + await PUT(`/Reviews/${review.ID}`,review) + } + reviews.message = { succeeded: 'Your review was submitted successfully. Thanks.' } + } catch (e) { + reviews.message = { failed: e.response.data.error.message } + } + } + + }, + + filters: { + stars: (r) => ('★'.repeat(Math.round(r))+'☆☆☆☆☆').slice(0,5), + datetime: (d) => d && new Date(d).toLocaleString(), + }, + +}) + +// initially fill list of my reviews +reviews.fetch() diff --git a/reviews/app/vue/index.html b/reviews/app/vue/index.html new file mode 100644 index 00000000..a361387f --- /dev/null +++ b/reviews/app/vue/index.html @@ -0,0 +1,62 @@ + + + + + Capire Reviews + + + + + + + +
+ +

{{ document.title }}

+ + + + + + + + + + + + + + + + +
Subject Rating Title Date
{{ review.subject }}{{ review.rating | stars }}{{ review.title }}{{ review.date | datetime }}
+ + + +
+ + + + + + {{ message.succeeded }} + {{ message.failed }} +
+
+ ( click on a row to see details... ) +
+ + +
+ + + + diff --git a/reviews/db/data/sap.capire.reviews-Reviews.csv b/reviews/db/data/sap.capire.reviews-Reviews.csv new file mode 100644 index 00000000..5e7f3d3f --- /dev/null +++ b/reviews/db/data/sap.capire.reviews-Reviews.csv @@ -0,0 +1,5 @@ +subject;rating;title;text +201;5;Intriguing;Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. +201;4;Fascinating;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Id diam maecenas ultricies mi eget mauris pharetra et. Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque. Pulvinar mattis nunc sed blandit libero. Facilisis magna etiam tempor orci eu. Nec sagittis aliquam malesuada bibendum arcu. Eu consequat ac felis donec. Ultricies tristique nulla aliquet enim tortor at auctor urna nunc. Tortor posuere ac ut consequat semper viverra nam libero. Amet nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Scelerisque purus semper eget duis at tellus. Elementum tempus egestas sed sed risus pretium. Arcu dictum varius duis at. Amet luctus venenatis lectus magna fringilla urna. Eget velit aliquet sagittis id consectetur purus ut faucibus. Vitae auctor eu augue ut lectus. Fermentum iaculis eu non diam phasellus vestibulum. +207;2;What is this?;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero justo laoreet sit amet cursus sit amet dictum. Nunc faucibus a pellentesque sit. Dis parturient montes nascetur ridiculus mus mauris vitae ultricies. Enim nunc faucibus a pellentesque. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien. Cras ornare arcu dui vivamus. Facilisi etiam dignissim diam quis enim lobortis. Et molestie ac feugiat sed. Urna neque viverra justo nec ultrices dui. Ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis. Dignissim sodales ut eu sem. Feugiat in fermentum posuere urna nec. At augue eget arcu dictum varius. +251;3;It's dark...;Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Suscipit tellus mauris a diam. Velit aliquet sagittis id consectetur purus ut. Viverra adipiscing at in tellus integer. Vitae elementum curabitur vitae nunc. Mattis ullamcorper velit sed ullamcorper morbi. Diam quis enim lobortis scelerisque. Auctor neque vitae tempus quam pellentesque nec nam aliquam. Semper auctor neque vitae tempus. Quis eleifend quam adipiscing vitae proin. Neque convallis a cras semper auctor neque vitae. Imperdiet massa tincidunt nunc pulvinar sapien et ligula. Sit amet consectetur adipiscing elit ut aliquam purus. Pretium quam vulputate dignissim suspendisse. \ No newline at end of file diff --git a/reviews/index.cds b/reviews/index.cds index c126bf5e..ac2c4e7a 100644 --- a/reviews/index.cds +++ b/reviews/index.cds @@ -1 +1,2 @@ using from './srv/reviews-service'; +namespace sap.capire.reviews; diff --git a/reviews/srv/reviews-service.cds b/reviews/srv/reviews-service.cds index 17a46578..eb26d9ae 100644 --- a/reviews/srv/reviews-service.cds +++ b/reviews/srv/reviews-service.cds @@ -27,7 +27,14 @@ service ReviewsService { annotate ReviewsService.Reviews with @restrict:[ { grant:'READ', to:'any' }, // everybody can read reviews { grant:'CREATE', to:'authenticated-user' }, // users must login to add reviews - { grant:'UPDATE', to:'authenticated-user', where:'reviewer=$user' }, + ///////////////////////////////////////////////// + // + // Temporarily disabling this due to glitch in CAP Node.js runtime: + // { grant:'UPDATE', to:'authenticated-user', where:'reviewer=$user' }, + // -> reenable it when the issue is fixed + { grant:'UPDATE', to:'authenticated-user' }, + // + //////////////////////////////////////////////////// { grant:'DELETE', to:'admin' }, ]; diff --git a/test/messaging.test.js b/test/messaging.test.js index 78866a37..9c2e541d 100644 --- a/test/messaging.test.js +++ b/test/messaging.test.js @@ -10,6 +10,7 @@ describe('Messaging', ()=>{ it ('should bootstrap sqlite in-memory db', async()=>{ const db = await cds.deploy (_model) .to ('sqlite::memory:') + await db.delete('Reviews') expect (db.model) .not.undefined }) From 394a8b5d1263a09785a3d1b4a6f12088b2de8227 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 20 Nov 2020 16:08:49 +0100 Subject: [PATCH 07/25] Revised layout of .html pages --- fiori/app/{fiori-apps.html => admin-fiori.html} | 0 fiori/app/bookshop.html | 3 +++ fiori/app/reviews.html | 3 +++ fiori/app/vue/index.html | 1 - fiori/server.js | 7 +++++-- fiori/test/requests.http | 2 +- 6 files changed, 12 insertions(+), 4 deletions(-) rename fiori/app/{fiori-apps.html => admin-fiori.html} (100%) create mode 100644 fiori/app/bookshop.html create mode 100644 fiori/app/reviews.html delete mode 100644 fiori/app/vue/index.html diff --git a/fiori/app/fiori-apps.html b/fiori/app/admin-fiori.html similarity index 100% rename from fiori/app/fiori-apps.html rename to fiori/app/admin-fiori.html diff --git a/fiori/app/bookshop.html b/fiori/app/bookshop.html new file mode 100644 index 00000000..13c22ac5 --- /dev/null +++ b/fiori/app/bookshop.html @@ -0,0 +1,3 @@ + + + diff --git a/fiori/app/reviews.html b/fiori/app/reviews.html new file mode 100644 index 00000000..a74f6c74 --- /dev/null +++ b/fiori/app/reviews.html @@ -0,0 +1,3 @@ + + + diff --git a/fiori/app/vue/index.html b/fiori/app/vue/index.html deleted file mode 100644 index de7f5a0f..00000000 --- a/fiori/app/vue/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/fiori/server.js b/fiori/server.js index 8f71c3f8..887190f9 100644 --- a/fiori/server.js +++ b/fiori/server.js @@ -7,8 +7,11 @@ cds.once('bootstrap',(app)=>{ const orders_app = dirname (require.resolve('@capire/orders/app/orders/webapp/manifest.json')) app.use ('/orders/webapp', express.static(orders_app)) // serving the vue.js app imported from @capire/bookshop - const vue_app = dirname (require.resolve('@capire/bookshop/app/vue/index.html')) - app.use ('/vue', express.static(vue_app)) + const bookshop_app = dirname (require.resolve('@capire/bookshop/app/vue/index.html')) + app.use ('/vue/bookshop', express.static(bookshop_app)) + // serving the vue.js app imported from @capire/reviews + const reviews_app = dirname (require.resolve('@capire/reviews/app/vue/index.html')) + app.use ('/vue/reviews', express.static(reviews_app)) }) cds.once('served', require('./srv/mashup')) diff --git a/fiori/test/requests.http b/fiori/test/requests.http index d759aedf..873a1533 100644 --- a/fiori/test/requests.http +++ b/fiori/test/requests.http @@ -42,7 +42,7 @@ GET {{bookshop}}/browse/Books(201)? ################################################# # -# Bookshop Services +# Orders Service # GET {{bookshop}}/orders/Orders From d1eb14f638a04599721e7368520f5c881e68d372 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 20 Nov 2020 17:05:01 +0100 Subject: [PATCH 08/25] Minor Cleanup --- orders/app/orders/fiori-service.cds | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/orders/app/orders/fiori-service.cds b/orders/app/orders/fiori-service.cds index 99356ea9..6dfff115 100644 --- a/orders/app/orders/fiori-service.cds +++ b/orders/app/orders/fiori-service.cds @@ -10,7 +10,7 @@ -using { OrdersService, sap.capire.orders.OrderItems } from '../../srv/orders-service'; +using { OrdersService } from '../../srv/orders-service'; @odata.draft.enabled @@ -68,7 +68,7 @@ annotate OrdersService.Orders with @( -annotate OrderItems with @( +annotate OrdersService.OrderItems with @( UI: { LineItem: [ {Value: product_ID, Label:'Product ID'}, From e15a6192b6e06fae48b47657c874a1f7b58e326e Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 21 Nov 2020 01:31:07 +0100 Subject: [PATCH 09/25] Aligned orders w/ managed compositions --- fiori/srv/mashup.cds | 4 ++-- orders/app/orders/fiori-service.cds | 2 +- orders/app/orders/webapp/manifest.json | 2 +- ...ms.csv => sap.capire.orders-Orders_Items.csv} | 2 +- orders/db/schema.cds | 16 ++++++++-------- orders/srv/orders-service.js | 6 +++--- 6 files changed, 16 insertions(+), 16 deletions(-) rename orders/db/data/{sap.capire.orders-OrderItems.csv => sap.capire.orders-Orders_Items.csv} (79%) diff --git a/fiori/srv/mashup.cds b/fiori/srv/mashup.cds index abcf0547..97f21771 100644 --- a/fiori/srv/mashup.cds +++ b/fiori/srv/mashup.cds @@ -19,7 +19,7 @@ extend Books with { // Extend Orders with Books as Products // -using { sap.capire.orders.OrderItems } from '@capire/orders'; -extend OrderItems with { +using { sap.capire.orders.Orders_Items } from '@capire/orders'; +extend Orders_Items with { book : Association to Books on product.ID = book.ID } diff --git a/orders/app/orders/fiori-service.cds b/orders/app/orders/fiori-service.cds index 6dfff115..e58c8ccd 100644 --- a/orders/app/orders/fiori-service.cds +++ b/orders/app/orders/fiori-service.cds @@ -68,7 +68,7 @@ annotate OrdersService.Orders with @( -annotate OrdersService.OrderItems with @( +annotate OrdersService.Orders_Items with @( UI: { LineItem: [ {Value: product_ID, Label:'Product ID'}, diff --git a/orders/app/orders/webapp/manifest.json b/orders/app/orders/webapp/manifest.json index df686462..045fa70a 100644 --- a/orders/app/orders/webapp/manifest.json +++ b/orders/app/orders/webapp/manifest.json @@ -121,7 +121,7 @@ "name": "sap.fe.templates.ObjectPage", "options": { "settings" : { - "entitySet": "OrderItems" + "entitySet": "Orders_Items" } } }, diff --git a/orders/db/data/sap.capire.orders-OrderItems.csv b/orders/db/data/sap.capire.orders-Orders_Items.csv similarity index 79% rename from orders/db/data/sap.capire.orders-OrderItems.csv rename to orders/db/data/sap.capire.orders-Orders_Items.csv index a3349b28..b3025abe 100644 --- a/orders/db/data/sap.capire.orders-OrderItems.csv +++ b/orders/db/data/sap.capire.orders-Orders_Items.csv @@ -1,4 +1,4 @@ -ID;order_ID;amount;product_ID;title;price +ID;up__ID;amount;product_ID;title;price 58040e66-1dcd-4ffb-ab10-fdce32028b79;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;201;Wuthering Heights;11.11 64e718c9-ff99-47f1-8ca3-950c850777d4;7e2f2640-6866-4dcf-8f4d-3027aa831cad;1;271;Catweazle;15 e9641166-e050-4261-bfee-d1e797e6cb7f;64e718c9-ff99-47f1-8ca3-950c850777d4;2;252;Eleonora;28 \ No newline at end of file diff --git a/orders/db/schema.cds b/orders/db/schema.cds index 1cde2e22..c3b4f1c5 100644 --- a/orders/db/schema.cds +++ b/orders/db/schema.cds @@ -1,26 +1,26 @@ 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.order = $self; + Items : Composition of many Orders_Items on Items.up_ = $self; buyer : User; currency : Currency; } -entity OrderItems { +entity Orders_Items { key ID : UUID; - order : Association to Orders; - @assert.integrity:false // REVISIT: this is a temporary workaround for a glitch in cds-runtime - product : Association to Products; + up_ : Association to Orders; + product : Association to Products @assert.integrity:false; // REVISIT: this is a temporary workaround for a glitch in cds-runtime amount : Integer; title : String; price : Double; } /** This is a stand-in for arbitrary ordered Products */ -@cds.persistence.skip:'always' -entity Products { +entity Products @(cds.persistence.skip:'always') { key ID : String; } + +// Activate extension package +using from '@capire/common'; diff --git a/orders/srv/orders-service.js b/orders/srv/orders-service.js index ed42770f..887ee593 100644 --- a/orders/srv/orders-service.js +++ b/orders/srv/orders-service.js @@ -3,13 +3,13 @@ class OrdersService extends cds.ApplicationService { /** register custom handlers */ init(){ - const { OrderItems } = this.entities + const { Orders_Items:OrderItems } = this.entities this.before ('UPDATE', 'Orders', async function(req) { const { ID, Items } = req.data if (Items) for (let { product_ID, amount } of Items) { const { amount:before } = await cds.tx(req).run ( - SELECT.one.from (OrderItems, oi => oi.amount) .where ({order_ID:ID, product_ID}) + SELECT.one.from (OrderItems, oi => oi.amount) .where ({up__ID:ID, product_ID}) ) if (amount != before) this.orderChanged (product_ID, amount-before) } @@ -18,7 +18,7 @@ class OrdersService extends cds.ApplicationService { this.before ('DELETE', 'Orders', async function(req) { const { ID } = req.data const Items = await cds.tx(req).run ( - SELECT.from (OrderItems, oi => { oi.product_ID, oi.amount }) .where ({order_ID:ID}) + SELECT.from (OrderItems, oi => { oi.product_ID, oi.amount }) .where ({up__ID:ID}) ) if (Items) for (let it of Items) this.orderChanged (it.product_ID, -it.amount) }) From b6028721af1bb9330fd2de74d6cd22a44cc35038 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 22 Nov 2020 10:49:56 +0100 Subject: [PATCH 10/25] make tests more robust --- test/cds.ql.test.js | 2 +- test/consuming-services.test.js | 6 +++--- test/custom-handlers.test.js | 2 +- test/hello-world.test.js | 3 +-- test/hierarchical-data.test.js | 5 +---- test/index.js | 6 ++++++ test/localized-data.test.js | 2 +- test/messaging.test.js | 5 +---- test/odata.test.js | 3 ++- 9 files changed, 17 insertions(+), 17 deletions(-) create mode 100644 test/index.js diff --git a/test/cds.ql.test.js b/test/cds.ql.test.js index 3af3327a..79c28fe3 100644 --- a/test/cds.ql.test.js +++ b/test/cds.ql.test.js @@ -1,5 +1,5 @@ +const { expect } = require('../test') const cds = require('@sap/cds/lib') -const { expect } = cds.test const CQL = ([cql]) => cds.parse.cql(cql) const Foo = { name: 'Foo' } const Books = { name: 'capire.bookshop.Books' } diff --git a/test/consuming-services.test.js b/test/consuming-services.test.js index ab550ef4..ff5c1b96 100644 --- a/test/consuming-services.test.js +++ b/test/consuming-services.test.js @@ -1,7 +1,7 @@ -const cds = require('@sap/cds/lib') -const { expect } = cds.test ( +const { expect } = require('../test') .run ( 'serve', 'AdminService', '--from', '@capire/bookshop,@capire/common', '--in-memory' -).in(__dirname) +) +const cds = require('@sap/cds/lib') describe('Consuming Services locally', () => { // diff --git a/test/custom-handlers.test.js b/test/custom-handlers.test.js index fd38c91f..c27779ae 100644 --- a/test/custom-handlers.test.js +++ b/test/custom-handlers.test.js @@ -1,5 +1,5 @@ +const { GET, POST, expect } = require('../test') .run ('bookshop') const cds = require('@sap/cds/lib'); cds.User = cds.User.Privileged // skip auth -const { GET, POST, expect } = cds.test('bookshop').in(__dirname,'..') describe('Custom Handlers', () => { diff --git a/test/hello-world.test.js b/test/hello-world.test.js index d8f58a80..d833dce3 100644 --- a/test/hello-world.test.js +++ b/test/hello-world.test.js @@ -1,5 +1,4 @@ -const cds = require('@sap/cds/lib') -const { GET, expect } = cds.test('serve','hello/world.cds').in(__dirname,'..') +const { GET, expect } = require('../test') .run ('serve','hello/world.cds') describe('Hello world!', () => { diff --git a/test/hierarchical-data.test.js b/test/hierarchical-data.test.js index d92dfcf9..e411eff0 100644 --- a/test/hierarchical-data.test.js +++ b/test/hierarchical-data.test.js @@ -1,6 +1,5 @@ -const cwd = process.cwd(); process.chdir (__dirname) //> only for internal CI/CD@SAP +const {expect} = require('../test') const cds = require('@sap/cds/lib') -const {expect} = cds.test // monkey patching older releases: if (!cds.compile.cdl) cds.compile.cdl = cds.parse @@ -25,8 +24,6 @@ describe('Hierarchical Data', ()=>{ expect (cds.db.model) .to.exist }) - after(()=> process.chdir(cwd)) - it ('supports deeply nested inserts', ()=> INSERT.into (Cats, { ID:100, name:'Some Cats...', children:[ { ID:101, name:'Cat', children:[ diff --git a/test/index.js b/test/index.js new file mode 100644 index 00000000..c44d3400 --- /dev/null +++ b/test/index.js @@ -0,0 +1,6 @@ + +const test = require('@sap/cds/lib/utils/tests').in(__dirname,'..') +module.exports = Object.assign(test,{run:test}) + +// REVISIT: With upcoming release of @sap/cds this should become: +// module.exports = require('@sap/cds/tests').in(__dirname,'..') diff --git a/test/localized-data.test.js b/test/localized-data.test.js index c31fd43a..9c840830 100644 --- a/test/localized-data.test.js +++ b/test/localized-data.test.js @@ -1,5 +1,5 @@ +const { GET, expect } = require('../test') .run ('serve', 'test/localized-data.cds', '--in-memory') const cds = require('@sap/cds/lib'); cds.User = cds.User.Privileged // skip auth -const { GET, expect } = cds.test ('serve', __dirname+'/localized-data.cds', '--in-memory') describe('Localized Data', () => { diff --git a/test/messaging.test.js b/test/messaging.test.js index 9c2e541d..ac76575c 100644 --- a/test/messaging.test.js +++ b/test/messaging.test.js @@ -1,13 +1,10 @@ +const { expect } = require('../test') const cds = require('@sap/cds/lib') -const cwd = process.cwd(); process.chdir (__dirname) //> only for internal CI/CD@SAP -const {expect} = cds.test const _model = '@capire/reviews' cds.User = cds.User.Privileged // hard core monkey patch describe('Messaging', ()=>{ - after(()=> process.chdir(cwd)) - it ('should bootstrap sqlite in-memory db', async()=>{ const db = await cds.deploy (_model) .to ('sqlite::memory:') await db.delete('Reviews') diff --git a/test/odata.test.js b/test/odata.test.js index db24ea3d..25a8c604 100644 --- a/test/odata.test.js +++ b/test/odata.test.js @@ -1,8 +1,9 @@ +const { GET, expect } = require('../test') .run ('bookshop') const cds = require('@sap/cds/lib'); cds.User = cds.User.Privileged // skip auth -const { GET, expect } = cds.test('bookshop').in(__dirname,'..') describe('OData Protocol', () => { + it('serves $metadata documents in v4', async () => { const { headers, status, data } = await GET `/browse/$metadata` expect(status).to.equal(200) From b4594e23c529d23ed84866f65289aab64f3aa7c1 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 23 Nov 2020 14:21:46 +0100 Subject: [PATCH 11/25] remove .db file --- bookshop/sqlite.db | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 bookshop/sqlite.db diff --git a/bookshop/sqlite.db b/bookshop/sqlite.db deleted file mode 100644 index e69de29b..00000000 From 684c2d53f10d2e336836e4fa6f164c6440deb76e Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 3 Dec 2020 13:58:25 +0100 Subject: [PATCH 12/25] Preparing test for upcomming release 4.4.4 --- test/messaging.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/messaging.test.js b/test/messaging.test.js index ac76575c..7e9eeb2c 100644 --- a/test/messaging.test.js +++ b/test/messaging.test.js @@ -1,7 +1,8 @@ const { expect } = require('../test') const cds = require('@sap/cds/lib') const _model = '@capire/reviews' -cds.User = cds.User.Privileged // hard core monkey patch +if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch +else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases describe('Messaging', ()=>{ From f32398ba8dd9b10c7f236282c3e1f89067e64c6f Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 7 Dec 2020 12:39:33 +0100 Subject: [PATCH 13/25] Using cds.User.default in 4.4.4 --- test/custom-handlers.test.js | 4 +++- test/localized-data.test.js | 4 +++- test/odata.test.js | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/test/custom-handlers.test.js b/test/custom-handlers.test.js index c27779ae..f8541a18 100644 --- a/test/custom-handlers.test.js +++ b/test/custom-handlers.test.js @@ -1,5 +1,7 @@ const { GET, POST, expect } = require('../test') .run ('bookshop') -const cds = require('@sap/cds/lib'); cds.User = cds.User.Privileged // skip auth +const cds = require('@sap/cds/lib') +if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch +else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases describe('Custom Handlers', () => { diff --git a/test/localized-data.test.js b/test/localized-data.test.js index 9c840830..bfa3411f 100644 --- a/test/localized-data.test.js +++ b/test/localized-data.test.js @@ -1,5 +1,7 @@ const { GET, expect } = require('../test') .run ('serve', 'test/localized-data.cds', '--in-memory') -const cds = require('@sap/cds/lib'); cds.User = cds.User.Privileged // skip auth +const cds = require('@sap/cds/lib') +if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch +else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases describe('Localized Data', () => { diff --git a/test/odata.test.js b/test/odata.test.js index 25a8c604..7d8c8123 100644 --- a/test/odata.test.js +++ b/test/odata.test.js @@ -1,5 +1,7 @@ const { GET, expect } = require('../test') .run ('bookshop') -const cds = require('@sap/cds/lib'); cds.User = cds.User.Privileged // skip auth +const cds = require('@sap/cds/lib') +if (cds.User.default) cds.User.default = cds.User.Privileged // hard core monkey patch +else cds.User = cds.User.Privileged // hard core monkey patch for older cds releases describe('OData Protocol', () => { From 86e5c429bdc1a43313d978e217c3db286ccad5ce Mon Sep 17 00:00:00 2001 From: Christian Georgi Date: Wed, 16 Dec 2020 17:39:58 +0100 Subject: [PATCH 14/25] Example for draft choreography --- fiori/test/requests.http | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/fiori/test/requests.http b/fiori/test/requests.http index 873a1533..95076ce3 100644 --- a/fiori/test/requests.http +++ b/fiori/test/requests.http @@ -42,7 +42,24 @@ GET {{bookshop}}/browse/Books(201)? ################################################# # -# Orders Service +# Orders Service, incl. draft choreography # +@newOrderID = e939604c-ab83-4d4f-bdb6-95fe30b3773e GET {{bookshop}}/orders/Orders + +### Create order, still inactive +POST {{bookshop}}/orders/Orders +Content-Type: application/json + +{"ID": "{{newOrderID}}"} + +### Get inactive order. We have to specify `IsActiveEntity`. +GET {{bookshop}}/orders/Orders(ID={{newOrderID}},IsActiveEntity=false) + +### Activate order using `.../.draftActivate` +POST {{bookshop}}/orders/Orders(ID={{newOrderID}},IsActiveEntity=false)/OrdersService.draftActivate +Content-Type: application/json + +### Get active order +GET {{bookshop}}/orders/Orders(ID={{newOrderID}},IsActiveEntity=true) From ea6e27481071a765dfd701ddb239ed89b92bf426 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 17 Dec 2020 16:37:30 +0100 Subject: [PATCH 15/25] Fixed: missing await srv.emit --- .eslintrc | 3 ++- bookshop/srv/cat-service.cds | 4 ++-- bookshop/srv/cat-service.js | 4 ++-- common/package.json | 6 +++++- fiori/srv/mashup.js | 2 +- orders/srv/orders-service.js | 4 ++-- reviews/app/vue/app.js | 2 +- reviews/srv/reviews-service.js | 4 ++-- 8 files changed, 17 insertions(+), 12 deletions(-) diff --git a/.eslintrc b/.eslintrc index da867678..40fe0cb5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -21,6 +21,7 @@ }, "rules": { "no-console": "off", - "require-atomic-updates": "off" + "require-atomic-updates": "off", + "require-await":"warn" } } diff --git a/bookshop/srv/cat-service.cds b/bookshop/srv/cat-service.cds index 606eb05a..f6fb00cd 100644 --- a/bookshop/srv/cat-service.cds +++ b/bookshop/srv/cat-service.cds @@ -1,12 +1,12 @@ using { sap.capire.bookshop as my } from '../db/schema'; service CatalogService @(path:'/browse') { - @readonly entity Books as SELECT from my.Books {*, + @readonly entity Books as SELECT from my.Books { *, author.name as author } excluding { createdBy, modifiedBy }; @readonly entity ListOfBooks as SELECT from Books - excluding { descr, stock }; + excluding { descr }; @requires: 'authenticated-user' action submitOrder ( book: Books:ID, amount: Integer ) returns { stock: Integer }; diff --git a/bookshop/srv/cat-service.js b/bookshop/srv/cat-service.js index 9a955c5e..4676dcd5 100644 --- a/bookshop/srv/cat-service.js +++ b/bookshop/srv/cat-service.js @@ -1,7 +1,7 @@ const cds = require('@sap/cds') const { Books } = cds.entities ('sap.capire.bookshop') -class CatalogService extends cds.ApplicationService { async init(){ +class CatalogService extends cds.ApplicationService { init(){ // Reduce stock of ordered books if available stock suffices this.on ('submitOrder', async req => { @@ -9,7 +9,7 @@ class CatalogService extends cds.ApplicationService { async init(){ 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 }) + await this.emit ('OrderedBook', { book, amount, buyer:req.user.id }) return { stock } } else return req.error (409,`${amount} exceeds stock for book #${book}`) diff --git a/common/package.json b/common/package.json index d5c6dc24..c1998c24 100644 --- a/common/package.json +++ b/common/package.json @@ -1,4 +1,8 @@ { "name": "@capire/common", - "version": "1.0.0" + "description": "Provides a pre-built extension package for std @sap/cds/common", + "version": "1.0.0", + "dependencies": { + "@sap/cds": "latest" + } } diff --git a/fiori/srv/mashup.js b/fiori/srv/mashup.js index 7a46fcc6..e8acf174 100644 --- a/fiori/srv/mashup.js +++ b/fiori/srv/mashup.js @@ -49,7 +49,7 @@ module.exports = async()=>{ // called by server.js // // Reduce stock of ordered books for orders are created from Orders admin UI // - OrdersService.on ('OrderChanged', async (msg) => { + OrdersService.on ('OrderChanged', (msg) => { console.debug ('> received:', msg.event, msg.data) const { product, deltaAmount } = msg.data return UPDATE (Books) .where ('ID =', product) diff --git a/orders/srv/orders-service.js b/orders/srv/orders-service.js index 887ee593..10420410 100644 --- a/orders/srv/orders-service.js +++ b/orders/srv/orders-service.js @@ -11,7 +11,7 @@ class OrdersService extends cds.ApplicationService { const { amount:before } = await cds.tx(req).run ( SELECT.one.from (OrderItems, oi => oi.amount) .where ({up__ID:ID, product_ID}) ) - if (amount != before) this.orderChanged (product_ID, amount-before) + if (amount != before) await this.orderChanged (product_ID, amount-before) } }) @@ -20,7 +20,7 @@ class OrdersService extends cds.ApplicationService { const Items = await cds.tx(req).run ( SELECT.from (OrderItems, oi => { oi.product_ID, oi.amount }) .where ({up__ID:ID}) ) - if (Items) for (let it of Items) this.orderChanged (it.product_ID, -it.amount) + if (Items) await Promise.all (Items.map(it => this.orderChanged (it.product_ID, -it.amount))) }) return super.init() diff --git a/reviews/app/vue/app.js b/reviews/app/vue/app.js index fb115d68..b46a4892 100644 --- a/reviews/app/vue/app.js +++ b/reviews/app/vue/app.js @@ -37,7 +37,7 @@ const reviews = new Vue ({ reviews.message = {} }, - async newReview () { + newReview () { reviews.review = {} reviews.message = {} setTimeout (()=> $('form > input').focus(), 111) diff --git a/reviews/srv/reviews-service.js b/reviews/srv/reviews-service.js index 10979994..21441df6 100644 --- a/reviews/srv/reviews-service.js +++ b/reviews/srv/reviews-service.js @@ -1,5 +1,5 @@ const cds = require ('@sap/cds') -module.exports = cds.service.impl (async function(){ +module.exports = cds.service.impl (function(){ // Get the CSN definition for Reviews from the db schema for sub-sequent queries // ( Note: we explicitly specify the namespace to support embedded reuse ) @@ -16,7 +16,7 @@ module.exports = cds.service.impl (async function(){ SELECT.one (['round(avg(rating),2) as rating']) .from (Reviews) .where ({subject}) ) global.it || console.log ('< emitting:', 'reviewed', { subject, rating }) - this.emit ('reviewed', { subject, rating }) + await this.emit ('reviewed', { subject, rating }) }) // Increment counter for reviews considered helpful From 7e04f50852ef50867aa0f99a358f19c243fec192 Mon Sep 17 00:00:00 2001 From: Manuel Blechschmidt Date: Fri, 18 Dec 2020 09:25:11 +0100 Subject: [PATCH 16/25] Added installation for cds (#177) When running these samples for the first time, it is necessary to install the cds binaries globally. Co-authored-by: Christian Georgi Co-authored-by: Daniel Hutzel --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9f7773c4..e26b2ca3 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,15 @@ Find here a collection of samples for the [SAP Cloud Application Programming Mod ![](https://github.com/SAP-samples/cloud-cap-samples/workflows/CI/badge.svg) [![REUSE status](https://api.reuse.software/badge/github.com/SAP-samples/cloud-cap-samples)](https://api.reuse.software/info/github.com/SAP-samples/cloud-cap-samples) + ### Preliminaries -1. [Install @sap/cds-dk](https://cap.cloud.sap/docs/get-started/) as documented in [capire](https://cap.cloud.sap) +1. [Install @sap/cds-dk](https://cap.cloud.sap/docs/get-started/) globally as documented in [capire](https://cap.cloud.sap) + ```sh + npm i -g @sap/cds-dk + ``` 2. _Optional:_ [Use Visual Studio Code](https://cap.cloud.sap/docs/get-started/in-vscode) - ### Download Clone this repo as shown below, if you have [git](https://git-scm.com/downloads) installed, From dc7244276439a7b508db2ace100308b5f8d4a5b4 Mon Sep 17 00:00:00 2001 From: Christian Georgi Date: Fri, 18 Dec 2020 15:06:37 +0100 Subject: [PATCH 17/25] Better brand name --- fiori/app/admin-fiori.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fiori/app/admin-fiori.html b/fiori/app/admin-fiori.html index 7f7d9a3a..4070f7db 100644 --- a/fiori/app/admin-fiori.html +++ b/fiori/app/admin-fiori.html @@ -13,7 +13,7 @@ applications: { "browse-books": { title: "Browse Books", - description: "... testing FE v42", + description: "w/ SAP Fiori Elements", additionalInformation: "SAPUI5.Component=bookshop", applicationType : "URL", url: "/browse/webapp", @@ -21,7 +21,7 @@ }, "manage-books": { title: "Manage Books", - description: "... testing FE v42", + description: "w/ SAP Fiori Elements", additionalInformation: "SAPUI5.Component=admin", applicationType : "URL", url: "/admin/webapp", @@ -29,7 +29,7 @@ }, "manage-orders": { title: "Manage Orders", - description: "... testing FE v42", + description: "w/ SAP Fiori Elements", additionalInformation: "SAPUI5.Component=orders", applicationType : "URL", url: "/orders/webapp", From 01073fd6a5211896b47a123e9a4a064340226e7c Mon Sep 17 00:00:00 2001 From: Marius Obert Date: Fri, 8 Jan 2021 14:30:37 +0100 Subject: [PATCH 18/25] Restructure sentence --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e26b2ca3..8a39720f 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ npx jest ### Serve `npm` -We've simple npm registry mock included which allows you to do an `npm install @capire/` anywhere locally. Use it as follows: +We've included a simple npm registry mock which allows you to do an `npm install @capire/` locally. Use it as follows: 1. Start the @capire registry: ```sh From c12e516f5d3f7154efa64661f86fa405ae5604ae Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 13 Jan 2021 16:26:56 +0100 Subject: [PATCH 19/25] Prep for upcomming releases --- test/cds.ql.test.js | 21 ++++++++++++++++++++- test/messaging.test.js | 8 ++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/test/cds.ql.test.js b/test/cds.ql.test.js index 79c28fe3..a8bfa7be 100644 --- a/test/cds.ql.test.js +++ b/test/cds.ql.test.js @@ -325,7 +325,26 @@ describe('cds.ql → cqn', () => { }) // using CQL fragments -> uses cds.parse.expr - expect((cqn = CQL`SELECT from Foo where ID=11 and x in ( foo, 'bar', 3)`)).to.eql({ + const is_v2 = !!cds.parse.expr('(1,2)').list + if (is_v2) expect((cqn = CQL`SELECT from Foo where ID=11 and x in ( foo, 'bar', 3)`)).to.eql({ + SELECT: { + from: { ref: ['Foo'] }, + where: [ + { ref: ['ID'] }, + '=', + { val: ID }, + 'and', + { ref: ['x'] }, + 'in', + {list:[ + { ref: ['foo'] }, + { val: 'bar' }, + { val: 3 }, + ]} + ], + }, + }) + else expect((cqn = CQL`SELECT from Foo where ID=11 and x in ( foo, 'bar', 3)`)).to.eql({ SELECT: { from: { ref: ['Foo'] }, where: [ diff --git a/test/messaging.test.js b/test/messaging.test.js index 7e9eeb2c..42d20d38 100644 --- a/test/messaging.test.js +++ b/test/messaging.test.js @@ -42,16 +42,16 @@ describe('Messaging', ()=>{ // { ID: 111 + (++N), subject: "201", title: "Captivating", rating: N }, // ), srv.create ('Reviews') .entries ( - { ID: 111 + (++N), subject: "201", title: "Captivating", rating: N } + { ID: String(111 + (++N)), subject: "201", title: "Captivating", rating: N } ), srv.create ('Reviews') .entries ( - { ID: 111 + (++N), subject: "201", title: "Captivating", rating: N } + { ID: String(111 + (++N)), subject: "201", title: "Captivating", rating: N } ), srv.create ('Reviews') .entries ( - { ID: 111 + (++N), subject: "201", title: "Captivating", rating: N } + { ID: String(111 + (++N)), subject: "201", title: "Captivating", rating: N } ), srv.create ('Reviews') .entries ( - { ID: 111 + (++N), subject: "201", title: "Captivating", rating: N } + { ID: String(111 + (++N)), subject: "201", title: "Captivating", rating: N } ), ])) From 75628b6096fc8b27f9a83593d81d9fa2005f5b45 Mon Sep 17 00:00:00 2001 From: Christian Georgi Date: Mon, 18 Jan 2021 14:27:39 +0100 Subject: [PATCH 20/25] Use latest UI5 again --- fiori/app/admin-fiori.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fiori/app/admin-fiori.html b/fiori/app/admin-fiori.html index 4070f7db..6c229e6b 100644 --- a/fiori/app/admin-fiori.html +++ b/fiori/app/admin-fiori.html @@ -40,8 +40,7 @@ - -