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/app/_i18n/i18n.properties b/fiori/app/_i18n/i18n.properties new file mode 100644 index 00000000..83681bcf --- /dev/null +++ b/fiori/app/_i18n/i18n.properties @@ -0,0 +1,26 @@ +Books = Books +Book = Book +ID = ID +Title = Title +Author = Author +AuthorID = Author ID +Stock = Stock +Name = Name +AuthorName = Author's Name +DateOfBirth = Date of Birth +DateOfDeath = Date of Death +PlaceOfBirth = Place of Birth +PlaceOfDeath = Place of Death +Age = Age +Authors = Authors +Order = Order +Orders = Orders +Price = Price + +Genre = Genre +Genres = Genres +SubGenres = Sub Genres + +NumCode = Numeric Code +MinorUnit = Minor Unit +Exponent = Exponent \ No newline at end of file diff --git a/fiori/app/_i18n/i18n_de.properties b/fiori/app/_i18n/i18n_de.properties new file mode 100644 index 00000000..7724f685 --- /dev/null +++ b/fiori/app/_i18n/i18n_de.properties @@ -0,0 +1,14 @@ +Books = Bücher +Book = Buch +ID = ID +Title = Titel +Authors = Autoren +Author = Autor +AuthorID = ID des Autors +AuthorName = Name des Autors +Age = Alter +Name = Name +Stock = Bestand +Order = Bestellung +Orders = Bestellungen +Price = Preis diff --git a/fiori/app/admin-fiori.html b/fiori/app/admin-fiori.html new file mode 100644 index 00000000..6c229e6b --- /dev/null +++ b/fiori/app/admin-fiori.html @@ -0,0 +1,55 @@ + + + + + + + + Bookshop + + + + + + + + + + \ No newline at end of file diff --git a/fiori/app/admin/fiori-service.cds b/fiori/app/admin/fiori-service.cds new file mode 100644 index 00000000..8e97fdbe --- /dev/null +++ b/fiori/app/admin/fiori-service.cds @@ -0,0 +1,93 @@ +using { AdminService } from '../../db/schema'; + +//////////////////////////////////////////////////////////////////////////// +// +// Books Object Page +// + +annotate AdminService.Books with @( + UI: { + Facets: [ + {$Type: 'UI.ReferenceFacet', Label: '{i18n>General}', Target: '@UI.FieldGroup#General'}, + {$Type: 'UI.ReferenceFacet', Label: '{i18n>Translations}', Target: 'texts/@UI.LineItem'}, + {$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'}, + {$Type: 'UI.ReferenceFacet', Label: '{i18n>Admin}', Target: '@UI.FieldGroup#Admin'}, + ], + FieldGroup#General: { + Data: [ + {Value: title}, + {Value: author_ID}, + {Value: genre_ID}, + {Value: descr}, + ] + }, + FieldGroup#Details: { + Data: [ + {Value: stock}, + {Value: price}, + {Value: currency_code, Label: '{i18n>Currency}'}, + ] + }, + FieldGroup#Admin: { + Data: [ + {Value: createdBy}, + {Value: createdAt}, + {Value: modifiedBy}, + {Value: modifiedAt} + ] + } + } +); + +annotate AdminService.Authors with @( + UI: { + HeaderInfo: { + Description: {Value: lifetime} + }, + Facets: [ + {$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'}, + {$Type: 'UI.ReferenceFacet', Label: '{i18n>Books}', Target: 'books/@UI.LineItem'}, + ], + FieldGroup#Details: { + Data: [ + {Value: placeOfBirth}, + {Value: placeOfDeath}, + {Value: dateOfBirth}, + {Value: dateOfDeath}, + {Value: age, Label: '{i18n>Age}'}, + ] + }, + } +); + + + +//////////////////////////////////////////////////////////// +// +// Draft for Localized Data +// + +annotate sap.capire.bookshop.Books with @fiori.draft.enabled; +annotate AdminService.Books with @odata.draft.enabled; + +annotate AdminService.Books_texts with @( + UI: { + Identification: [{Value:title}], + SelectionFields: [ locale, title ], + LineItem: [ + {Value: locale, Label: 'Locale'}, + {Value: title, Label: 'Title'}, + {Value: descr, Label: 'Description'}, + ] + } +); + +// Add Value Help for Locales +annotate AdminService.Books_texts { + locale @ValueList:{entity:'Languages',type:#fixed} +} +// In addition we need to expose Languages through AdminService +using { sap } from '@sap/cds/common'; +extend service AdminService { + entity Languages as projection on sap.common.Languages; +} diff --git a/fiori/app/admin/webapp/Component.js b/fiori/app/admin/webapp/Component.js new file mode 100644 index 00000000..c3137017 --- /dev/null +++ b/fiori/app/admin/webapp/Component.js @@ -0,0 +1,8 @@ +sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) { + "use strict"; + return AppComponent.extend("admin.Component", { + metadata: { manifest: "json" } + }); +}); + +/* eslint no-undef:0 */ \ No newline at end of file diff --git a/fiori/app/admin/webapp/i18n/i18n.properties b/fiori/app/admin/webapp/i18n/i18n.properties new file mode 100644 index 00000000..28b03dff --- /dev/null +++ b/fiori/app/admin/webapp/i18n/i18n.properties @@ -0,0 +1,11 @@ +# This is the resource bundle of itelo +# __ldi.translation.uuid=c3431418-9caf-11e8-98d0-529269fb1459 + +# JCI app descriptor contains lower case TITLE +appTitle=Bookshop Sample + +# JCI app descriptor contains lower case DESCRIPTION +appSubTitle=CAP Sample Application + +# JCI app descriptor contains lower case DESCRIPTION +appDescription=CDS Sample Service diff --git a/fiori/app/admin/webapp/manifest.json b/fiori/app/admin/webapp/manifest.json new file mode 100644 index 00000000..25047c29 --- /dev/null +++ b/fiori/app/admin/webapp/manifest.json @@ -0,0 +1,128 @@ +{ + "_version": "1.8.0", + "sap.app": { + "id": "admin", + "type": "application", + "title": "Manage Books", + "description": "Sample Application", + "i18n": "i18n/i18n.properties", + "dataSources": { + "AdminService": { + "uri": "/admin/", + "type": "OData", + "settings": { + "odataVersion": "4.0" + } + } + }, + "-sourceTemplate": { + "id": "ui5template.basicSAPUI5ApplicationProject", + "-id": "ui5template.smartTemplate", + "-version": "1.40.12" + } + }, + "sap.ui5": { + "dependencies": { + "libs": { + "sap.fe.templates": {} + } + }, + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties" + }, + "": { + "dataSource": "AdminService", + "settings": { + "synchronizationMode": "None", + "operationMode": "Server", + "autoExpandSelect" : true, + "earlyRequests": true, + "groupProperties": { + "default": { + "submit": "Auto" + } + } + } + } + }, + "routing": { + "routes": [ + { + "pattern": ":?query:", + "name": "BooksList", + "target": "BooksList" + }, + { + "pattern": "Books({key}):?query:", + "name": "BooksDetails", + "target": "BooksDetails" + }, + { + "pattern": "Books({key}/author({key2}):?query:", + "name": "AuthorsDetails", + "target": "AuthorsDetails" + } + ], + "targets": { + "BooksList": { + "type": "Component", + "id": "BooksList", + "name": "sap.fe.templates.ListReport", + "options": { + "settings" : { + "entitySet" : "Books", + "navigation" : { + "Books" : { + "detail" : { + "route" : "BooksDetails" + } + } + } + } + } + }, + "BooksDetails": { + "type": "Component", + "id": "BooksDetailsList", + "name": "sap.fe.templates.ObjectPage", + "options": { + "settings" : { + "entitySet" : "Books", + "navigation" : { + "Authors" : { + "detail" : { + "route" : "AuthorsDetails" + } + } + } + } + } + }, + "AuthorsDetails": { + "type": "Component", + "id": "AuthorsDetailsList", + "name": "sap.fe.templates.ObjectPage", + "options": { + "settings" : { + "entitySet" : "Authors" + } + } + } + } + }, + "contentDensities": { + "compact": true, + "cozy": true + } + }, + "sap.ui": { + "technology": "UI5", + "fullWidth": false + }, + "sap.fiori": { + "registrationIds": [], + "archeType": "transactional" + } +} \ No newline at end of file diff --git a/fiori/app/bookshop.html b/fiori/app/bookshop.html new file mode 100644 index 00000000..e7c07e25 --- /dev/null +++ b/fiori/app/bookshop.html @@ -0,0 +1,3 @@ + + + diff --git a/fiori/app/browse/fiori-service.cds b/fiori/app/browse/fiori-service.cds new file mode 100644 index 00000000..f59a36b4 --- /dev/null +++ b/fiori/app/browse/fiori-service.cds @@ -0,0 +1,50 @@ +using CatalogService from '@capire/bookshop'; + +//////////////////////////////////////////////////////////////////////////// +// +// Books Object Page +// +annotate CatalogService.Books with @( + UI: { + HeaderInfo: { + TypeName: 'Book', + TypeNamePlural: 'Books', + Description: {Value: author} + }, + HeaderFacets: [ + {$Type: 'UI.ReferenceFacet', Label: '{i18n>Description}', Target: '@UI.FieldGroup#Descr'}, + ], + Facets: [ + {$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Price'}, + ], + FieldGroup#Descr: { + Data: [ + {Value: descr}, + ] + }, + FieldGroup#Price: { + Data: [ + {Value: price}, + {Value: currency.symbol, Label: '{i18n>Currency}'}, + ] + }, + } +); + + +//////////////////////////////////////////////////////////////////////////// +// +// Books Object Page +// +annotate CatalogService.Books with @( + UI: { + SelectionFields: [ ID, price, currency_code ], + LineItem: [ + {Value: title}, + {Value: author, Label:'{i18n>Author}'}, + {Value: genre.name}, + {Value: price}, + {Value: currency.symbol, Label:' '}, + ] + }, +); diff --git a/fiori/app/browse/webapp/Component.js b/fiori/app/browse/webapp/Component.js new file mode 100644 index 00000000..7914d295 --- /dev/null +++ b/fiori/app/browse/webapp/Component.js @@ -0,0 +1,7 @@ +sap.ui.define(["sap/fe/core/AppComponent"], function(AppComponent) { + "use strict"; + return AppComponent.extend("bookshop.Component", { + metadata: { manifest: "json" } + }); +}); +/* eslint no-undef:0 */ \ No newline at end of file diff --git a/fiori/app/browse/webapp/i18n/i18n.properties b/fiori/app/browse/webapp/i18n/i18n.properties new file mode 100644 index 00000000..28b03dff --- /dev/null +++ b/fiori/app/browse/webapp/i18n/i18n.properties @@ -0,0 +1,11 @@ +# This is the resource bundle of itelo +# __ldi.translation.uuid=c3431418-9caf-11e8-98d0-529269fb1459 + +# JCI app descriptor contains lower case TITLE +appTitle=Bookshop Sample + +# JCI app descriptor contains lower case DESCRIPTION +appSubTitle=CAP Sample Application + +# JCI app descriptor contains lower case DESCRIPTION +appDescription=CDS Sample Service diff --git a/fiori/app/browse/webapp/manifest.json b/fiori/app/browse/webapp/manifest.json new file mode 100644 index 00000000..4a2e0a62 --- /dev/null +++ b/fiori/app/browse/webapp/manifest.json @@ -0,0 +1,106 @@ +{ + "_version": "1.8.0", + "sap.app": { + "id": "bookshop", + "type": "application", + "title": "Browse Books", + "description": "Sample Application", + "i18n": "i18n/i18n.properties", + "dataSources": { + "CatalogService": { + "uri": "/browse/", + "type": "OData", + "settings": { + "odataVersion": "4.0" + } + } + }, + "-sourceTemplate": { + "id": "ui5template.basicSAPUI5ApplicationProject", + "-id": "ui5template.smartTemplate", + "-version": "1.40.12" + } + }, + "sap.ui5": { + "dependencies": { + "libs": { + "sap.fe.templates": {} + } + }, + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties" + }, + "": { + "dataSource": "CatalogService", + "settings": { + "synchronizationMode": "None", + "operationMode": "Server", + "autoExpandSelect": true, + "earlyRequests": true, + "groupProperties": { + "default": { + "submit": "Auto" + } + } + } + } + }, + "routing": { + "routes": [ + { + "pattern": ":?query:", + "name": "BooksList", + "target": "BooksList" + }, + { + "pattern": "Books({key}):?query:", + "name": "BooksDetails", + "target": "BooksDetails" + } + ], + "targets": { + "BooksList": { + "type": "Component", + "id": "BooksList", + "name": "sap.fe.templates.ListReport", + "options": { + "settings": { + "entitySet": "Books", + "navigation": { + "Books": { + "detail": { + "route": "BooksDetails" + } + } + } + } + } + }, + "BooksDetails": { + "type": "Component", + "id": "BooksDetailsList", + "name": "sap.fe.templates.ObjectPage", + "options": { + "settings": { + "entitySet": "Books" + } + } + } + } + }, + "contentDensities": { + "compact": true, + "cozy": true + } + }, + "sap.ui": { + "technology": "UI5", + "fullWidth": false + }, + "sap.fiori": { + "registrationIds": [], + "archeType": "transactional" + } +} diff --git a/fiori/app/common.cds b/fiori/app/common.cds new file mode 100644 index 00000000..614f03b3 --- /dev/null +++ b/fiori/app/common.cds @@ -0,0 +1,257 @@ +/* + Common Annotations shared by all apps +*/ + +using { sap.capire.bookshop as my } from '@capire/bookshop'; +using { sap.common } from '@capire/common'; + +//////////////////////////////////////////////////////////////////////////// +// +// Books Lists +// +annotate my.Books with @( + Common.SemanticKey: [title], + UI: { + Identification: [{Value:title}], + SelectionFields: [ ID, author_ID, price, currency_code ], + LineItem: [ + {Value: ID}, + {Value: title}, + {Value: author.name, Label:'{i18n>Author}'}, + {Value: genre.name}, + {Value: stock}, + {Value: price}, + {Value: currency.symbol, Label:' '}, + ] + } +) { + author @ValueList.entity:'Authors'; +}; + +//////////////////////////////////////////////////////////////////////////// +// +// Books Details +// +annotate my.Books with @( + UI: { + HeaderInfo: { + TypeName: '{i18n>Book}', + TypeNamePlural: '{i18n>Books}', + Title: {Value: title}, + Description: {Value: author.name} + }, + } +); + + + +//////////////////////////////////////////////////////////////////////////// +// +// Books Elements +// +annotate my.Books with { + ID @title:'{i18n>ID}' @UI.HiddenFilter; + title @title:'{i18n>Title}'; + genre @title:'{i18n>Genre}' @Common: { Text: genre.name, TextArrangement: #TextOnly }; + author @title:'{i18n>Author}' @Common: { Text: author.name, TextArrangement: #TextOnly }; + price @title:'{i18n>Price}'; + stock @title:'{i18n>Stock}'; + descr @UI.MultiLineText; +} + +//////////////////////////////////////////////////////////////////////////// +// +// Genres List +// +annotate my.Genres with @( + Common.SemanticKey: [name], + UI: { + SelectionFields: [ name ], + LineItem:[ + {Value: name}, + {Value: parent.name, Label: 'Main Genre'}, + ], + } +); + +//////////////////////////////////////////////////////////////////////////// +// +// Genre Details +// +annotate my.Genres with @( + UI: { + Identification: [{Value:name}], + HeaderInfo: { + TypeName: '{i18n>Genre}', + TypeNamePlural: '{i18n>Genres}', + Title: {Value: name}, + Description: {Value: ID} + }, + Facets: [ + {$Type: 'UI.ReferenceFacet', Label: '{i18n>SubGenres}', Target: 'children/@UI.LineItem'}, + ], + } +); + +//////////////////////////////////////////////////////////////////////////// +// +// Genres Elements +// +annotate my.Genres with { + ID @title: '{i18n>ID}'; + name @title: '{i18n>Genre}'; +} + +//////////////////////////////////////////////////////////////////////////// +// +// Authors List +// +annotate my.Authors with @( + Common.SemanticKey: [name], + UI: { + Identification: [{Value:name}], + SelectionFields: [ name ], + LineItem:[ + {Value: ID}, + {Value: name}, + {Value: dateOfBirth}, + {Value: dateOfDeath}, + {Value: placeOfBirth}, + {Value: placeOfDeath}, + ], + } +); + +//////////////////////////////////////////////////////////////////////////// +// +// Author Details +// +annotate my.Authors with @( + UI: { + HeaderInfo: { + TypeName: '{i18n>Author}', + TypeNamePlural: '{i18n>Authors}', + Title: {Value: name}, + Description: {Value: dateOfBirth} + }, + Facets: [ + {$Type: 'UI.ReferenceFacet', Target: 'books/@UI.LineItem'}, + ], + } +); + + +//////////////////////////////////////////////////////////////////////////// +// +// Authors Elements +// +annotate my.Authors with { + ID @title:'{i18n>ID}' @UI.HiddenFilter; + name @title:'{i18n>Name}'; + dateOfBirth @title:'{i18n>DateOfBirth}'; + dateOfDeath @title:'{i18n>DateOfDeath}'; + placeOfBirth @title:'{i18n>PlaceOfBirth}'; + placeOfDeath @title:'{i18n>PlaceOfDeath}'; +} + +//////////////////////////////////////////////////////////////////////////// +// +// Languages List +// +annotate common.Languages with @( + Common.SemanticKey: [code], + Identification: [{Value:code}], + UI: { + SelectionFields: [ name, descr ], + LineItem:[ + {Value: code}, + {Value: name}, + ], + } +); + +//////////////////////////////////////////////////////////////////////////// +// +// Language Details +// +annotate common.Languages with @( + UI: { + HeaderInfo: { + TypeName: '{i18n>Language}', + TypeNamePlural: '{i18n>Languages}', + Title: {Value: name}, + Description: {Value: descr} + }, + Facets: [ + {$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'}, + ], + FieldGroup#Details: { + Data: [ + {Value: code}, + {Value: name}, + {Value: descr} + ] + }, + } +); + +//////////////////////////////////////////////////////////////////////////// +// +// Currencies List +// +annotate common.Currencies with @( + Common.SemanticKey: [code], + Identification: [{Value:code}], + UI: { + SelectionFields: [ name, descr ], + LineItem:[ + {Value: descr}, + {Value: symbol}, + {Value: code}, + ], + } +); + +//////////////////////////////////////////////////////////////////////////// +// +// Currency Details +// +annotate common.Currencies with @( + UI: { + HeaderInfo: { + TypeName: '{i18n>Currency}', + TypeNamePlural: '{i18n>Currencies}', + Title: {Value: descr}, + Description: {Value: code} + }, + Facets: [ + {$Type: 'UI.ReferenceFacet', Label: '{i18n>Details}', Target: '@UI.FieldGroup#Details'}, + {$Type: 'UI.ReferenceFacet', Label: '{i18n>Extended}', Target: '@UI.FieldGroup#Extended'}, + ], + FieldGroup#Details: { + Data: [ + {Value: name}, + {Value: symbol}, + {Value: code}, + {Value: descr} + ] + }, + FieldGroup#Extended: { + Data: [ + {Value: numcode}, + {Value: minor}, + {Value: exponent} + ] + }, + } +); + +//////////////////////////////////////////////////////////////////////////// +// +// Currencies Elements +// +annotate common.Currencies with { + numcode @title:'{i18n>NumCode}'; + minor @title:'{i18n>MinorUnit}'; + exponent @title:'{i18n>Exponent}'; +} diff --git a/fiori/app/reviews.html b/fiori/app/reviews.html new file mode 100644 index 00000000..75af8860 --- /dev/null +++ b/fiori/app/reviews.html @@ -0,0 +1,3 @@ + + + diff --git a/fiori/app/services.cds b/fiori/app/services.cds new file mode 100644 index 00000000..595023e9 --- /dev/null +++ b/fiori/app/services.cds @@ -0,0 +1,12 @@ +/* + This model controls what gets served to Fiori frontends... +*/ + +using from './admin/fiori-service'; +using from './browse/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/db/hana/index.cds b/fiori/db/hana/index.cds new file mode 100644 index 00000000..04822ad0 --- /dev/null +++ b/fiori/db/hana/index.cds @@ -0,0 +1,10 @@ +// +// Add Author.age and .lifetime with a DB-specific function +// + +using { AdminService } from '../schema'; + +extend projection AdminService.Authors with { + YEARS_BETWEEN(dateOfBirth, dateOfDeath) as age: Integer, + YEAR(dateOfBirth) || ' – ' || YEAR(dateOfDeath) as lifetime : String +} diff --git a/fiori/db/schema.cds b/fiori/db/schema.cds new file mode 100644 index 00000000..479fdbfb --- /dev/null +++ b/fiori/db/schema.cds @@ -0,0 +1,8 @@ +using { sap.capire.bookshop } from '@capire/bookshop'; + +// Forward-declare calculated fields to be filled in database-specific ways +// TODO find a better way to have 'default' fields that still can be overwritten. +extend bookshop.Authors with { + virtual age: Integer; + virtual lifetime: String; +} diff --git a/fiori/db/sqlite/index.cds b/fiori/db/sqlite/index.cds new file mode 100644 index 00000000..019335ef --- /dev/null +++ b/fiori/db/sqlite/index.cds @@ -0,0 +1,10 @@ +// +// Add Author.age and .lifetime with a DB-specific function +// + +using { AdminService } from '../schema'; + +extend projection AdminService.Authors with { + strftime('%Y',dateOfDeath)-strftime('%Y',dateOfBirth) as age: Integer, + strftime('%Y',dateOfBirth) || ' – ' || strftime('%Y',dateOfDeath) as lifetime : String +} diff --git a/fiori/package.json b/fiori/package.json new file mode 100644 index 00000000..414541d3 --- /dev/null +++ b/fiori/package.json @@ -0,0 +1,44 @@ +{ + "name": "@capire/fiori", + "version": "1.0.0", + "dependencies": { + "@capire/bookshop": "*", + "@capire/reviews": "*", + "@capire/orders": "*", + "@capire/common": "*", + "@sap/cds": ">=4", + "express": "^4.17.1", + "passport": "^0.4.1" + }, + "scripts": { + "start": "cds run --in-memory?", + "watch": "cds watch" + }, + "cds": { + "hana": { + "deploy-format": "hdbtable" + }, + "requires": { + "auth": { + "strategy": "dummy" + }, + "ReviewsService": { + "kind": "odata", + "model": "@capire/reviews" + }, + "OrdersService": { + "kind": "odata", + "model": "@capire/orders" + }, + "db": { + "kind": "sql", + "[development]": { + "model": "db/sqlite" + }, + "[production]": { + "model": "db/hana" + } + } + } + } +} diff --git a/fiori/server.js b/fiori/server.js new file mode 100644 index 00000000..a8dc4298 --- /dev/null +++ b/fiori/server.js @@ -0,0 +1,18 @@ +const cds = require ('@sap/cds') + +cds.once('bootstrap',(app)=>{ + app.use ('/orders/webapp', _from('@capire/orders/app/orders/webapp/manifest.json')) + app.use ('/bookshop', _from('@capire/bookshop/app/vue/index.html')) + app.use ('/reviews', _from('@capire/reviews/app/vue/index.html')) +}) + +cds.once('served', require('./srv/mashup')) + +module.exports = cds.server + + +// ----------------------------------------------------------------------- +// Helper for serving static content from npm-installed packages +const {static} = require('express') +const {dirname} = require('path') +const _from = target => static (dirname (require.resolve(target))) diff --git a/fiori/srv/mashup.cds b/fiori/srv/mashup.cds new file mode 100644 index 00000000..97f21771 --- /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 Products +// + +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/fiori/srv/mashup.js b/fiori/srv/mashup.js new file mode 100644 index 00000000..e8acf174 --- /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: [{ product:{ID:`${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', (msg) => { + console.debug ('> received:', msg.event, msg.data) + const { product, deltaAmount } = msg.data + return UPDATE (Books) .where ('ID =', product) + .and ('stock >=', deltaAmount) + .set ('stock -=', deltaAmount) + }) +} diff --git a/fiori/test/requests.http b/fiori/test/requests.http new file mode 100644 index 00000000..badacbfe --- /dev/null +++ b/fiori/test/requests.http @@ -0,0 +1,77 @@ + +@bookshop = http://localhost:4004 +@reviews-service = {{bookshop}}/reviews +# Uncomment this when running a separate reviews service +@reviews-service = http://localhost:4005/reviews + + + +################################################# +# +# Reviews Service +# + +GET {{reviews-service}}/Reviews + +### + +POST {{reviews-service}}/Reviews +Authorization: Basic {{$processEnv USER}}: +Content-Type: application/json + +{"subject":"201", "title":"boo", "rating":3 } + + + +################################################# +# +# Bookshop Services +# + +GET {{bookshop}}/browse/Books/201/reviews? +&$select=rating,date,title +&$top=3 + +### + +GET {{bookshop}}/browse/Books(201)? +&$select=ID,title,rating +&$expand=reviews + + + +################################################# +# +# 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) + +### Create author +POST {{bookshop}}/admin/Authors +Content-Type: application/json +Authorization: Basic alice: + +{ + "ID": 200, + "name": "William Shakespeare", + "dateOfBirth": "1564-04-26", + "dateOfDeath": "1616-04-23" +} diff --git a/multitenancy/logs-recent_4.txt b/multitenancy/logs-recent_4.txt new file mode 100644 index 00000000..210d5c21 Binary files /dev/null and b/multitenancy/logs-recent_4.txt differ