From 309c76caae520258282de8cc24640f62bb7168da Mon Sep 17 00:00:00 2001 From: Wolfgang Koch Date: Tue, 4 May 2021 10:33:38 +0200 Subject: [PATCH] adding fiori --- fiori/.env | 2 + fiori/app/_i18n/i18n.properties | 26 ++ fiori/app/_i18n/i18n_de.properties | 14 + fiori/app/admin-fiori.html | 55 ++++ fiori/app/admin/fiori-service.cds | 93 +++++++ fiori/app/admin/webapp/Component.js | 8 + fiori/app/admin/webapp/i18n/i18n.properties | 11 + fiori/app/admin/webapp/manifest.json | 128 +++++++++ fiori/app/bookshop.html | 3 + fiori/app/browse/fiori-service.cds | 50 ++++ fiori/app/browse/webapp/Component.js | 7 + fiori/app/browse/webapp/i18n/i18n.properties | 11 + fiori/app/browse/webapp/manifest.json | 106 ++++++++ fiori/app/common.cds | 257 +++++++++++++++++++ fiori/app/reviews.html | 3 + fiori/app/services.cds | 12 + fiori/db/hana/index.cds | 10 + fiori/db/schema.cds | 8 + fiori/db/sqlite/index.cds | 10 + fiori/package.json | 44 ++++ fiori/server.js | 18 ++ fiori/srv/mashup.cds | 25 ++ fiori/srv/mashup.js | 59 +++++ fiori/test/requests.http | 77 ++++++ multitenancy/logs-recent_4.txt | Bin 0 -> 276864 bytes 25 files changed, 1037 insertions(+) create mode 100644 fiori/.env create mode 100644 fiori/app/_i18n/i18n.properties create mode 100644 fiori/app/_i18n/i18n_de.properties create mode 100644 fiori/app/admin-fiori.html create mode 100644 fiori/app/admin/fiori-service.cds create mode 100644 fiori/app/admin/webapp/Component.js create mode 100644 fiori/app/admin/webapp/i18n/i18n.properties create mode 100644 fiori/app/admin/webapp/manifest.json create mode 100644 fiori/app/bookshop.html create mode 100644 fiori/app/browse/fiori-service.cds create mode 100644 fiori/app/browse/webapp/Component.js create mode 100644 fiori/app/browse/webapp/i18n/i18n.properties create mode 100644 fiori/app/browse/webapp/manifest.json create mode 100644 fiori/app/common.cds create mode 100644 fiori/app/reviews.html create mode 100644 fiori/app/services.cds create mode 100644 fiori/db/hana/index.cds create mode 100644 fiori/db/schema.cds create mode 100644 fiori/db/sqlite/index.cds create mode 100644 fiori/package.json create mode 100644 fiori/server.js create mode 100644 fiori/srv/mashup.cds create mode 100644 fiori/srv/mashup.js create mode 100644 fiori/test/requests.http create mode 100644 multitenancy/logs-recent_4.txt 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 0000000000000000000000000000000000000000..210d5c21d706ccdb1d4abf06ca9525417bcea772 GIT binary patch literal 276864 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 Y`<359*%sZGUSUzYQ)0?cAH{qBe>&~(MF0Q* literal 0 HcmV?d00001