From 16c919d5edadd2e744f8291725e0ce57fe3295f6 Mon Sep 17 00:00:00 2001 From: Olena Date: Wed, 13 Nov 2024 17:25:20 +0100 Subject: [PATCH 01/26] WIP: Demonstrate Hierarchy Functions (#388) Provides an example of an hierarchy function as `ListReport` and `ValueHelp` - currently only READ - only on HANA Changes: - `LineItem parent.name` is removed from `app/common.cds`, because it leads to OData requests with `$expand` for an hierarchy function which are not supported yet - `sap.common.CodeList` aspect was removed from `Genres` entity because it provides `ValueHelp` dialog implicitly through annotation [@cds.odata.valuelist](https://pages.github.tools.sap/cap/docs/advanced/fiori#convenience-option-cds-odata-valuelist). It also uses the annotation `@cds.autoexpose`, because of this entity `Genres` should be exposed explicitly in `CatalogService` and `ReviewService` after `sap.common.CodeList` is removed. - renaming in the view `GenreHierarchy` is temporal and will be solved by CAP Java team in the future ``` node as node_id, parent_node as parent_id ``` - to [enable tree tables](https://ui5.sap.com//#/topic/7cf7a31fd1ee490ab816ecd941bd2f1f) as `ListReport` add to `manifest.json`: ``` "tableSettings": { "type": "TreeTable", "hierarchyQualifier": "NodesHierarchy", ... } ``` - to enable tree table for ValueHelp hierarchical entity should be annotated with `PresentationVariant` and root entity with `PresentationVariantQualifier` --------- Co-authored-by: D070615 --- app/admin/fiori-service.cds | 27 ++++ app/appconfig/fioriSandboxConfig.json | 21 +++ app/common.cds | 13 +- app/genres/fiori-service.cds | 23 ++++ app/genres/package.json | 12 ++ app/genres/webapp/Component.js | 3 + app/genres/webapp/i18n/i18n.properties | 2 + app/genres/webapp/i18n/i18n_de.properties | 2 + app/genres/webapp/index.html | 35 +++++ app/genres/webapp/manifest.json | 129 +++++++++++++++++++ app/index.cds | 1 + app/xs-app.json | 149 +++++++++++----------- db/books.cds | 13 +- db/data/my.bookshop-Genres.csv | 32 ++--- srv/admin-service.cds | 15 ++- srv/cat-service.cds | 3 + srv/hierarchy.cds | 29 +++++ srv/review-service.cds | 3 + 18 files changed, 405 insertions(+), 107 deletions(-) create mode 100644 app/genres/fiori-service.cds create mode 100644 app/genres/package.json create mode 100644 app/genres/webapp/Component.js create mode 100644 app/genres/webapp/i18n/i18n.properties create mode 100644 app/genres/webapp/i18n/i18n_de.properties create mode 100644 app/genres/webapp/index.html create mode 100644 app/genres/webapp/manifest.json create mode 100644 srv/hierarchy.cds diff --git a/app/admin/fiori-service.cds b/app/admin/fiori-service.cds index 2480f4cc..8acab159 100644 --- a/app/admin/fiori-service.cds +++ b/app/admin/fiori-service.cds @@ -54,6 +54,33 @@ annotate AdminService.Books with @(UI : { ]} }); +// Add Value Help for Tree Table +annotate AdminService.Books with { + genre @(Common: { + Label : 'Genre', + ValueList: { + CollectionPath : 'GenreHierarchy', + Parameters : [ + { + $Type : 'Common.ValueListParameterDisplayOnly', + ValueListProperty: 'name', + }], + PresentationVariantQualifier: 'VH', + } + }); +} + +annotate AdminService.GenreHierarchy with @UI: { + PresentationVariant #VH: { + $Type : 'UI.PresentationVariantType', + Visualizations : ['@UI.LineItem'], + RecursiveHierarchyQualifier: 'GenreHierarchy' + }, + LineItem : [{ + $Type: 'UI.DataField', + Value: name, + }] +}; //////////////////////////////////////////////////////////// // diff --git a/app/appconfig/fioriSandboxConfig.json b/app/appconfig/fioriSandboxConfig.json index 1dc45ef8..48b2ef6a 100644 --- a/app/appconfig/fioriSandboxConfig.json +++ b/app/appconfig/fioriSandboxConfig.json @@ -20,6 +20,14 @@ "title": "Browse Books", "description": "Find your favorite book" } + }, { + "id": "browse-genres", + "tileType": "sap.ushell.ui.tile.StaticTile", + "properties": { + "targetURL": "#Genres-display", + "title": "Browse Genres", + "description": "Find your favorite genre" + } } ] }, @@ -112,6 +120,19 @@ "url": "/browse/webapp" } }, + "browse-genres": { + "semanticObject": "Genres", + "action": "display", + "signature": { + "parameters": {}, + "additionalParameters": "allowed" + }, + "resolutionResult": { + "applicationType": "SAPUI5", + "additionalInformation": "SAPUI5.Component=genres", + "url": "/genres/webapp" + } + }, "manage-books": { "semanticObject": "Books", "action": "manage", diff --git a/app/common.cds b/app/common.cds index a8607057..ba638052 100644 --- a/app/common.cds +++ b/app/common.cds @@ -177,10 +177,8 @@ annotate my.Genres with UI : { SelectionFields : [name], LineItem : [ - {Value : name}, - { - Value : parent.name, - Label : 'Main Genre' + {Value : name, + Label : '{i18n>Name}', }, ], } @@ -199,12 +197,7 @@ annotate my.Genres with TypeNamePlural : '{i18n>Genres}', Title : {Value : name}, Description : {Value : ID} - }, - Facets : [{ - $Type : 'UI.ReferenceFacet', - Label : '{i18n>SubGenres}', - Target : 'children/@UI.LineItem' - }, ], + } }); diff --git a/app/genres/fiori-service.cds b/app/genres/fiori-service.cds new file mode 100644 index 00000000..d195d935 --- /dev/null +++ b/app/genres/fiori-service.cds @@ -0,0 +1,23 @@ +/* + UI annotations for the Browse GenreHierarchy App +*/ + +using AdminService from '../../srv/admin-service'; + + +annotate AdminService.GenreHierarchy with @Aggregation.RecursiveHierarchy#GenreHierarchy: { + $Type: 'Aggregation.RecursiveHierarchyType', + NodeProperty: node_id, // identifies a node + ParentNavigationProperty: parent // navigates to a node's parent + }; + + annotate AdminService.GenreHierarchy with @Hierarchy.RecursiveHierarchy#GenreHierarchy: { + $Type: 'Hierarchy.RecursiveHierarchyType', + // ExternalKey : null, + LimitedDescendantCount: LimitedDescendantCount, + DistanceFromRoot: DistanceFromRoot, + DrillState: DrillState, + Matched: Matched, + MatchedDescendantCount: MatchedDescendantCount, + LimitedRank: LimitedRank +}; \ No newline at end of file diff --git a/app/genres/package.json b/app/genres/package.json new file mode 100644 index 00000000..9b81674d --- /dev/null +++ b/app/genres/package.json @@ -0,0 +1,12 @@ +{ + "name": "genres", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/app/genres/webapp/Component.js b/app/genres/webapp/Component.js new file mode 100644 index 00000000..6b352417 --- /dev/null +++ b/app/genres/webapp/Component.js @@ -0,0 +1,3 @@ +sap.ui.define(["sap/fe/core/AppComponent"], ac => ac.extend("genres.Component", { + metadata:{ manifest:'json' } +})) diff --git a/app/genres/webapp/i18n/i18n.properties b/app/genres/webapp/i18n/i18n.properties new file mode 100644 index 00000000..d2792ee9 --- /dev/null +++ b/app/genres/webapp/i18n/i18n.properties @@ -0,0 +1,2 @@ +appTitle=Browse Genres +appDescription=Genres as Tree View diff --git a/app/genres/webapp/i18n/i18n_de.properties b/app/genres/webapp/i18n/i18n_de.properties new file mode 100644 index 00000000..e8714e92 --- /dev/null +++ b/app/genres/webapp/i18n/i18n_de.properties @@ -0,0 +1,2 @@ +appTitle=Zeige Genres +appDescription=Genres als Baumansicht diff --git a/app/genres/webapp/index.html b/app/genres/webapp/index.html new file mode 100644 index 00000000..d2192773 --- /dev/null +++ b/app/genres/webapp/index.html @@ -0,0 +1,35 @@ + + + + + + + Browse Genres + + + + +
+ + diff --git a/app/genres/webapp/manifest.json b/app/genres/webapp/manifest.json new file mode 100644 index 00000000..ab4a7d46 --- /dev/null +++ b/app/genres/webapp/manifest.json @@ -0,0 +1,129 @@ +{ + "_version": "1.8.0", + "sap.app": { + "id": "genres", + "type": "application", + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "applicationVersion": { + "version": "1.0.0" + }, + "dataSources": { + "AdminService": { + "uri": "/api/admin/", + "type": "OData", + "settings": { + "odataVersion": "4.0" + } + } + }, + "-sourceTemplate": { + "id": "ui5template.basicSAPUI5ApplicationProject", + "-id": "ui5template.smartTemplate", + "-version": "1.40.12" + }, + "crossNavigation": { + "inbounds": { + "Genres-show": { + "signature": { + "parameters": {}, + "additionalParameters": "allowed" + }, + "semanticObject": "GenreHierarchy", + "action": "show" + } + } + } + }, + "sap.ui5": { + "dependencies": { + "minUI5Version": "1.122.0", + "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": "GenreHierarchyList", + "target": "GenreHierarchyList" + }, + { + "pattern": "GenreHierarchy({key}):?query:", + "name": "GenreHierarchyDetails", + "target": "GenreHierarchyDetails" + } + ], + "targets": { + "GenreHierarchyList": { + "type": "Component", + "id": "GenreHierarchyList", + "name": "sap.fe.templates.ListReport", + "options": { + "settings" : { + "entitySet" : "GenreHierarchy", + "navigation" : { + "GenreHierarchy" : { + "detail" : { + "route" : "GenreHierarchyDetails" + } + } + }, + "controlConfiguration": { + "@com.sap.vocabularies.UI.v1.LineItem": { + "tableSettings": { + "hierarchyQualifier": "GenreHierarchy", + "type": "TreeTable" + } + } + } + } + } + }, + "GenreHierarchyDetails": { + "type": "Component", + "id": "GenreHierarchyDetails", + "name": "sap.fe.templates.ObjectPage", + "options": { + "settings" : { + "entitySet": "GenreHierarchy" + } + } + } + } + }, + "contentDensities": { + "compact": true, + "cozy": true + } + }, + "sap.ui": { + "technology": "UI5", + "fullWidth": false + }, + "sap.fiori": { + "registrationIds": [], + "archeType": "transactional" + } +} diff --git a/app/index.cds b/app/index.cds index dcbf40e5..0c07c888 100644 --- a/app/index.cds +++ b/app/index.cds @@ -8,4 +8,5 @@ using from './orders/fiori-service'; using from './reviews/fiori-service'; using from './notes/fiori-service'; using from './addresses/fiori-service'; +using from './genres/fiori-service'; using from './common'; diff --git a/app/xs-app.json b/app/xs-app.json index f6adf5a4..4facc67d 100644 --- a/app/xs-app.json +++ b/app/xs-app.json @@ -2,77 +2,82 @@ "welcomeFile": "/app/fiori.html", "authenticationMethod": "route", "routes": [ - { - "source": "^/app/(.*)$", - "cacheControl": "no-cache, no-store, must-revalidate", - "target": "$1", - "localDir": "./", - "authenticationType": "xsuaa" - }, - { - "source": "^/appconfig/(.*)$", - "localDir": "./", - "authenticationType": "xsuaa" - }, - { - "source": "^/browse/webapp/(.*)$", - "localDir": "./", - "authenticationType": "xsuaa" - }, - { - "source": "^/admin/webapp/(.*)$", - "localDir": "./", - "authenticationType": "xsuaa" - }, - { - "source": "^/orders/webapp/(.*)$", - "localDir": "./", - "authenticationType": "xsuaa" - }, - { - "source": "^/reviews/webapp/(.*)$", - "localDir": "./", - "authenticationType": "xsuaa" - }, - { - "source": "^/notes/webapp/(.*)$", - "localDir": "./", - "authenticationType": "xsuaa" - }, - { - "source": "^/addresses/webapp/(.*)$", - "localDir": "./", - "authenticationType": "xsuaa" - }, - { - "source": "^/vue/(.*)$", - "localDir": "./", - "authenticationType": "xsuaa" - }, - { - "source": "^/api/admin/(.*)", - "authenticationType": "xsuaa", - "destination": "backend" - }, - { - "source": "^/api/browse/(.*)", - "authenticationType": "xsuaa", - "destination": "backend" - }, - { - "source": "^/api/review/(.*)", - "authenticationType": "xsuaa", - "destination": "backend" - }, - { - "source": "^/api/notes/(.*)", - "authenticationType": "xsuaa", - "destination": "backend" - }, - { - "source": "^/api/(.*)$", - "authenticationType": "none", - "destination": "backend" - } + { + "source": "^/app/(.*)$", + "cacheControl": "no-cache, no-store, must-revalidate", + "target": "$1", + "localDir": "./", + "authenticationType": "xsuaa" + }, + { + "source": "^/appconfig/(.*)$", + "localDir": "./", + "authenticationType": "xsuaa" + }, + { + "source": "^/browse/webapp/(.*)$", + "localDir": "./", + "authenticationType": "xsuaa" + }, + { + "source": "^/admin/webapp/(.*)$", + "localDir": "./", + "authenticationType": "xsuaa" + }, + { + "source": "^/orders/webapp/(.*)$", + "localDir": "./", + "authenticationType": "xsuaa" + }, + { + "source": "^/genres/webapp/(.*)$", + "localDir": "./", + "authenticationType": "xsuaa" + }, + { + "source": "^/reviews/webapp/(.*)$", + "localDir": "./", + "authenticationType": "xsuaa" + }, + { + "source": "^/notes/webapp/(.*)$", + "localDir": "./", + "authenticationType": "xsuaa" + }, + { + "source": "^/addresses/webapp/(.*)$", + "localDir": "./", + "authenticationType": "xsuaa" + }, + { + "source": "^/vue/(.*)$", + "localDir": "./", + "authenticationType": "xsuaa" + }, + { + "source": "^/api/admin/(.*)", + "authenticationType": "xsuaa", + "destination": "backend" + }, + { + "source": "^/api/browse/(.*)", + "authenticationType": "xsuaa", + "destination": "backend" + }, + { + "source": "^/api/review/(.*)", + "authenticationType": "xsuaa", + "destination": "backend" + }, + { + "source": "^/api/notes/(.*)", + "authenticationType": "xsuaa", + "destination": "backend" + }, + { + "source": "^/api/(.*)$", + "authenticationType": "none", + "destination": "backend" + } ] } diff --git a/db/books.cds b/db/books.cds index fe2e5e0a..a4d8ba44 100644 --- a/db/books.cds +++ b/db/books.cds @@ -2,7 +2,6 @@ namespace my.bookshop; using { Currency, - sap, managed, cuid } from '@sap/cds/common'; @@ -46,9 +45,11 @@ annotate Authors with /** * Hierarchically organized Code List for Genres */ -entity Genres : sap.common.CodeList { - key ID : Integer; - parent : Association to Genres; - children : Composition of many Genres - on children.parent = $self; +entity Genres { + key ID : Integer; + node : Integer not null; + parent_node : Integer default 0; + name : localized String(255); + descr : localized String(1000); + parent : Association to one Genres on parent.node = parent_node; } diff --git a/db/data/my.bookshop-Genres.csv b/db/data/my.bookshop-Genres.csv index 88e73bdd..044b2b47 100644 --- a/db/data/my.bookshop-Genres.csv +++ b/db/data/my.bookshop-Genres.csv @@ -1,16 +1,16 @@ -ID;parent_ID;name -10;;Fiction -11;10;Drama -12;10;Poetry -13;10;Fantasy -14;10;Science Fiction -15;10;Romance -16;10;Mystery -17;10;Thriller -18;10;Dystopia -19;10;Fairy Tale -20;;Non-Fiction -21;20;Biography -22;21;Autobiography -23;20;Essay -24;20;Speech \ No newline at end of file +ID;parent_node;name;node +10;;Fiction;10 +11;10;Drama;11 +12;10;Poetry;12 +13;10;Fantasy;13 +14;10;Science Fiction;14 +15;10;Romance;15 +16;10;Mystery;16 +17;10;Thriller;17 +18;10;Dystopia;18 +19;10;Fairy Tale;19 +20;;Non-Fiction;20 +21;20;Biography;21 +22;21;Autobiography;22 +23;20;Essay;23 +24;20;Speech;24 \ No newline at end of file diff --git a/srv/admin-service.cds b/srv/admin-service.cds index 0f0e1a9b..7709ebd0 100644 --- a/srv/admin-service.cds +++ b/srv/admin-service.cds @@ -1,10 +1,12 @@ using {sap.common.Languages as CommonLanguages} from '@sap/cds/common'; using {my.bookshop as my} from '../db/index'; using {sap.changelog as changelog} from 'com.sap.cds/change-tracking'; +using {my.common.Hierarchy as Hierarchy} from './hierarchy'; extend my.Orders with changelog.changeTracked; @path : 'admin' +@odata.apply.transformations service AdminService @(requires : 'admin') { entity Books as projection on my.Books excluding { reviews } actions { action addToOrder(order_ID : UUID, quantity : Integer) returns Orders; @@ -12,6 +14,13 @@ service AdminService @(requires : 'admin') { entity Authors as projection on my.Authors; entity Orders as select from my.Orders; + extend my.Genres with Hierarchy; + + entity GenreHierarchy as projection on my.Genres { + node as node_id, + parent_node as parent_id, + * + } excluding { node, parent_node } @cds.persistence.skip entity Upload @odata.singleton { @@ -60,6 +69,6 @@ annotate AdminService.OrderItems { // Assign identifiers to the tracked entities annotate AdminService.Orders with @changelog: [OrderNo]; annotate AdminService.OrderItems with @changelog: [ - parent.OrderNo, - book.title, - ]; \ No newline at end of file + parent.OrderNo, + book.title, +]; diff --git a/srv/cat-service.cds b/srv/cat-service.cds index e887271f..c83eb432 100644 --- a/srv/cat-service.cds +++ b/srv/cat-service.cds @@ -13,6 +13,9 @@ service CatalogService @(requires: 'any') { @readonly entity Authors as projection on my.Authors; + @readonly + entity Genres as projection on my.Genres; + @readonly entity Reviews as projection on my.Reviews; diff --git a/srv/hierarchy.cds b/srv/hierarchy.cds new file mode 100644 index 00000000..f6619d98 --- /dev/null +++ b/srv/hierarchy.cds @@ -0,0 +1,29 @@ +namespace my.common; + +aspect Hierarchy { + virtual LimitedDescendantCount : Integer64; + virtual DistanceFromRoot : Integer64; + virtual DrillState : String; + virtual Matched : Boolean; + virtual MatchedDescendantCount : Integer64; + virtual LimitedRank : Integer64; +} + + +annotate Hierarchy with @Capabilities.FilterRestrictions.NonFilterableProperties: [ + 'LimitedDescendantCount', + 'DistanceFromRoot', + 'DrillState', + 'Matched', + 'MatchedDescendantCount', + 'LimitedRank' +]; + +annotate Hierarchy with @Capabilities.SortRestrictions.NonSortableProperties: [ + 'LimitedDescendantCount', + 'DistanceFromRoot', + 'DrillState', + 'Matched', + 'MatchedDescendantCount', + 'LimitedRank' +]; diff --git a/srv/review-service.cds b/srv/review-service.cds index d87950f9..599a2d79 100644 --- a/srv/review-service.cds +++ b/srv/review-service.cds @@ -13,6 +13,9 @@ service ReviewService { @readonly entity Authors as projection on my.Authors; + @readonly + entity Genres as projection on my.Genres; + // access control restrictions annotate Reviews with @restrict : [ { From cb392f71e14d9dd8563c892dc8f54863c1dd6ba0 Mon Sep 17 00:00:00 2001 From: Olena Date: Tue, 26 Nov 2024 08:49:24 +0100 Subject: [PATCH 02/26] Fix: ValueHelp Dialog (#397) Co-authored-by: D070615 --- app/admin/fiori-service.cds | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/admin/fiori-service.cds b/app/admin/fiori-service.cds index 8acab159..e0b60016 100644 --- a/app/admin/fiori-service.cds +++ b/app/admin/fiori-service.cds @@ -61,10 +61,16 @@ annotate AdminService.Books with { ValueList: { CollectionPath : 'GenreHierarchy', Parameters : [ + { + $Type : 'Common.ValueListParameterInOut', + LocalDataProperty: genre_ID, + ValueListProperty: 'ID', + }, { $Type : 'Common.ValueListParameterDisplayOnly', ValueListProperty: 'name', - }], + } + ], PresentationVariantQualifier: 'VH', } }); From 2d8f6f83d5c9e2ce9b527e3ee74d0c0b3b63bfb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20G=C3=B6rler?= Date: Tue, 26 Nov 2024 09:17:15 +0100 Subject: [PATCH 03/26] Fix value help (#398) Co-authored-by: D070615 --- app/admin/fiori-service.cds | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/admin/fiori-service.cds b/app/admin/fiori-service.cds index e0b60016..4ff0d0f7 100644 --- a/app/admin/fiori-service.cds +++ b/app/admin/fiori-service.cds @@ -69,7 +69,12 @@ annotate AdminService.Books with { { $Type : 'Common.ValueListParameterDisplayOnly', ValueListProperty: 'name', - } + }, + { + $Type : 'Common.ValueListParameterInOut', + LocalDataProperty: genre_ID, + ValueListProperty: 'ID', + } ], PresentationVariantQualifier: 'VH', } From 1a037e3bee7565c19b93bf5a2849a5bcef94298a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20G=C3=B6rler?= Date: Tue, 26 Nov 2024 09:49:01 +0100 Subject: [PATCH 04/26] Update fiori-service.cds --- app/admin/fiori-service.cds | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/admin/fiori-service.cds b/app/admin/fiori-service.cds index 4ff0d0f7..1a119dfe 100644 --- a/app/admin/fiori-service.cds +++ b/app/admin/fiori-service.cds @@ -61,11 +61,6 @@ annotate AdminService.Books with { ValueList: { CollectionPath : 'GenreHierarchy', Parameters : [ - { - $Type : 'Common.ValueListParameterInOut', - LocalDataProperty: genre_ID, - ValueListProperty: 'ID', - }, { $Type : 'Common.ValueListParameterDisplayOnly', ValueListProperty: 'name', From 357611e6b58d4a9c82124d052f41efdf71917e1c Mon Sep 17 00:00:00 2001 From: Olena Date: Tue, 26 Nov 2024 12:36:32 +0100 Subject: [PATCH 05/26] UI: Hide ID in ValueHelp (#399) Co-authored-by: D070615 --- app/admin/fiori-service.cds | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/admin/fiori-service.cds b/app/admin/fiori-service.cds index 1a119dfe..67020cf2 100644 --- a/app/admin/fiori-service.cds +++ b/app/admin/fiori-service.cds @@ -76,6 +76,11 @@ annotate AdminService.Books with { }); } +// Hide ID because of the ValueHelp +annotate AdminService.GenreHierarchy with { + ID @UI.Hidden; +}; + annotate AdminService.GenreHierarchy with @UI: { PresentationVariant #VH: { $Type : 'UI.PresentationVariantType', From 7b6179ea90595bab245948940fe0e2281a3d6d94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20G=C3=B6rler?= Date: Tue, 26 Nov 2024 14:50:09 +0100 Subject: [PATCH 06/26] Update to 3.5.0 & avoid parent_id/node_id projection --- app/genres/fiori-service.cds | 4 ++-- db/books.cds | 6 ++--- db/data/my.bookshop-Genres.csv | 32 ++++++++++++------------- pom.xml | 4 ++-- srv/admin-service.cds | 6 +---- srv/src/main/resources/application.yaml | 3 ++- 6 files changed, 26 insertions(+), 29 deletions(-) diff --git a/app/genres/fiori-service.cds b/app/genres/fiori-service.cds index d195d935..77b132a5 100644 --- a/app/genres/fiori-service.cds +++ b/app/genres/fiori-service.cds @@ -7,8 +7,8 @@ using AdminService from '../../srv/admin-service'; annotate AdminService.GenreHierarchy with @Aggregation.RecursiveHierarchy#GenreHierarchy: { $Type: 'Aggregation.RecursiveHierarchyType', - NodeProperty: node_id, // identifies a node - ParentNavigationProperty: parent // navigates to a node's parent + NodeProperty: ID, // identifies a node + ParentNavigationProperty: parnt // navigates to a node's parent }; annotate AdminService.GenreHierarchy with @Hierarchy.RecursiveHierarchy#GenreHierarchy: { diff --git a/db/books.cds b/db/books.cds index a4d8ba44..67f9fd88 100644 --- a/db/books.cds +++ b/db/books.cds @@ -47,9 +47,9 @@ annotate Authors with */ entity Genres { key ID : Integer; - node : Integer not null; - parent_node : Integer default 0; name : localized String(255); descr : localized String(1000); - parent : Association to one Genres on parent.node = parent_node; + parnt : Association to Genres; + children : Composition of many Genres + on children.parnt = $self; } diff --git a/db/data/my.bookshop-Genres.csv b/db/data/my.bookshop-Genres.csv index 044b2b47..f498be2d 100644 --- a/db/data/my.bookshop-Genres.csv +++ b/db/data/my.bookshop-Genres.csv @@ -1,16 +1,16 @@ -ID;parent_node;name;node -10;;Fiction;10 -11;10;Drama;11 -12;10;Poetry;12 -13;10;Fantasy;13 -14;10;Science Fiction;14 -15;10;Romance;15 -16;10;Mystery;16 -17;10;Thriller;17 -18;10;Dystopia;18 -19;10;Fairy Tale;19 -20;;Non-Fiction;20 -21;20;Biography;21 -22;21;Autobiography;22 -23;20;Essay;23 -24;20;Speech;24 \ No newline at end of file +ID;parnt_ID;name +10;;Fiction +11;10;Drama +12;10;Poetry +13;10;Fantasy +14;10;Science Fiction +15;10;Romance +16;10;Mystery +17;10;Thriller +18;10;Dystopia +19;10;Fairy Tale +20;;Non-Fiction +21;20;Biography +22;21;Autobiography +23;20;Essay +24;20;Speech \ No newline at end of file diff --git a/pom.xml b/pom.xml index 98a241b6..9dac44cf 100644 --- a/pom.xml +++ b/pom.xml @@ -23,11 +23,11 @@ 21 - 3.4.0 + 3.5.0 5.13.0 3.5.3 3.8.4 - 8.4.1 + 8.5.0 diff --git a/srv/admin-service.cds b/srv/admin-service.cds index 7709ebd0..32fb80e8 100644 --- a/srv/admin-service.cds +++ b/srv/admin-service.cds @@ -16,11 +16,7 @@ service AdminService @(requires : 'admin') { entity Orders as select from my.Orders; extend my.Genres with Hierarchy; - entity GenreHierarchy as projection on my.Genres { - node as node_id, - parent_node as parent_id, - * - } excluding { node, parent_node } + entity GenreHierarchy as projection on my.Genres; @cds.persistence.skip entity Upload @odata.singleton { diff --git a/srv/src/main/resources/application.yaml b/srv/src/main/resources/application.yaml index dad875b3..f28a0738 100644 --- a/srv/src/main/resources/application.yaml +++ b/srv/src/main/resources/application.yaml @@ -1,7 +1,8 @@ --- logging: level: - '[com.sap.cds.auditlog]': DEBUG + com.sap.cds.auditlog: DEBUG + com.sap.cds.persistence.sql: DEBUG spring: jmx: enabled: true From c7f50a84acda34729addf7ce1682fcb278bc656c Mon Sep 17 00:00:00 2001 From: Olena Date: Wed, 27 Nov 2024 14:34:31 +0100 Subject: [PATCH 07/26] Demonstrate custom logic for Tree Tables (#393) --- .../bookshop/handlers/HierarchyHandler.java | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java diff --git a/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java b/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java new file mode 100644 index 00000000..65c12aad --- /dev/null +++ b/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java @@ -0,0 +1,254 @@ +package my.bookshop.handlers; + +import java.util.ArrayDeque; +import java.util.Comparator; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import com.sap.cds.Row; +import com.sap.cds.ql.CQL; +import com.sap.cds.ql.Select; +import com.sap.cds.ql.cqn.CqnElementRef; +import com.sap.cds.ql.cqn.CqnPredicate; +import com.sap.cds.ql.cqn.CqnSelect; +import com.sap.cds.ql.cqn.CqnValue; +import com.sap.cds.ql.cqn.Modifier; +import com.sap.cds.ql.cqn.transformation.CqnTopLevelsTransformation; +import com.sap.cds.ql.cqn.transformation.CqnAncestorsTransformation; +import com.sap.cds.ql.cqn.transformation.CqnDescendantsTransformation; +import com.sap.cds.ql.cqn.transformation.CqnFilterTransformation; +import com.sap.cds.ql.cqn.transformation.CqnSearchTransformation; +import com.sap.cds.ql.cqn.transformation.CqnOrderByTransformation; +import com.sap.cds.ql.cqn.transformation.CqnTransformation; +import com.sap.cds.services.cds.CdsReadEventContext; +import com.sap.cds.services.cds.CqnService; +import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.handler.annotations.Before; +import com.sap.cds.services.handler.annotations.ServiceName; +import com.sap.cds.services.persistence.PersistenceService; + +import cds.gen.adminservice.AdminService_; +import cds.gen.adminservice.GenreHierarchy; +import cds.gen.adminservice.GenreHierarchy_; + +@Component +@Profile("default") +@ServiceName(AdminService_.CDS_NAME) +public class HierarchyHandler implements EventHandler { + + private final PersistenceService db; + + HierarchyHandler(PersistenceService db) { + this.db = db; + } + + @Before(event = CqnService.EVENT_READ, entity = GenreHierarchy_.CDS_NAME) + public void readGenreHierarchy(CdsReadEventContext event) { + List trafos = event.getCqn().transformations(); + List result = null; + + if (trafos.size() < 1) { + return; + } + + if (getTopLevels(trafos) instanceof CqnTopLevelsTransformation topLevels) { + result = topLevels(topLevels, CQL.TRUE); + } else if (trafos.get(0) instanceof CqnDescendantsTransformation descendants) { + result = handleDescendants(descendants); + } else if (trafos.get(0) instanceof CqnAncestorsTransformation ancestors) { + if (trafos.size() == 2 && trafos.get(1) instanceof CqnTopLevelsTransformation topLevels) { + result = handleAncestors(ancestors, topLevels); + } else if (trafos.size() == 3 && trafos.get(2) instanceof CqnTopLevelsTransformation topLevels) { + result = handleAncestors(ancestors, topLevels); + } + } + + setResult(event, result); + } + + private CqnTopLevelsTransformation getTopLevels(List trafos) { + if (trafos.get(0) instanceof CqnTopLevelsTransformation topLevels) { + return topLevels; + } else if (trafos.size() == 2 && trafos.get(0) instanceof CqnOrderByTransformation && trafos.get(1) instanceof CqnTopLevelsTransformation topLevels) { + return topLevels; + } + return null; + } + + private void setResult(CdsReadEventContext event, List result) { + if (!result.isEmpty()) { + addDrillState(result); + } + + event.setResult(result); + } + + private void addDrillState(List ghs) { + List ids = ghs.stream().map(gh -> gh.getNodeId()).toList(); + Set parents = ghs.stream().map(gh -> gh.getParentId()).filter(p -> p != 0).collect(Collectors.toSet()); + CqnSelect q = Select.from(AdminService_.GENRE_HIERARCHY, gh -> gh.parent()).columns(gh -> gh.node_id()) + .where(gh -> gh.node_id().in(ids)); + Set nonLeafs = db + .run(q) + .stream().map(r -> r.get(GenreHierarchy.NODE_ID)).collect(Collectors.toSet()); + + for (GenreHierarchy gh : ghs) { + Integer id = gh.getNodeId(); + if (nonLeafs.contains(id)) { + if (parents.contains(id)) { + gh.setDrillState("expanded"); + } else { + gh.setDrillState("collapsed"); + } + } else { + gh.setDrillState("leaf"); + } + } + } + + private List handleDescendants(CqnDescendantsTransformation descendants) { + Map lookup = new HashMap<>(); + CqnFilterTransformation filter = (CqnFilterTransformation) descendants.transformations().get(0); + CqnSelect getRoot = Select.from(AdminService_.GENRE_HIERARCHY).where(filter.filter()); + GenreHierarchy root = db.run(getRoot).single(GenreHierarchy.class); + lookup.put(root.getNodeId(), root); + + CqnPredicate parentFilter = CQL.copy(filter.filter(), new Modifier() { + @Override + public CqnValue ref(CqnElementRef ref) { + return CQL.get(GenreHierarchy.PARENT_ID); + } + }); + + CqnSelect childrenCQN = Select.from(AdminService_.GENRE_HIERARCHY).where(parentFilter); + List children = db.run(childrenCQN).listOf(GenreHierarchy.class); + children.forEach(gh -> lookup.put(gh.getNodeId(), gh)); + children.forEach(gh -> gh.setParent(lookup.get(gh.getParentId()))); + + return children.stream().sorted(new Sorter()).toList(); + } + + private List handleAncestors(CqnAncestorsTransformation ancestors, CqnTopLevelsTransformation topLevels) { + CqnTransformation trafo = ancestors.transformations().get(0); + Select inner = Select.from(AdminService_.GENRE_HIERARCHY).columns(gh -> gh.node_id()); + if (trafo instanceof CqnFilterTransformation filter) { + inner.where(filter.filter()); + } else if (trafo instanceof CqnSearchTransformation search) { + inner.search(search.search()); + } + Select outer = Select.from(AdminService_.GENRE_HIERARCHY).columns(gh -> gh.node_id().as("i0"), gh -> gh.parent().node_id().as("i1"), + gh -> gh.parent().parent().node_id().as("i2"), gh -> gh.parent().parent().parent().node_id().as("i3"), + gh -> gh.parent().parent().parent().parent().node_id().as("i4")).where(gh -> gh.node_id().in(inner)); + + Set ancestorIds = new HashSet<>(); + db.run(outer).stream().forEach(r -> { + addIfNotNull(ancestorIds, r, "i0"); + addIfNotNull(ancestorIds, r, "i1"); + addIfNotNull(ancestorIds, r, "i2"); + addIfNotNull(ancestorIds, r, "i3"); + addIfNotNull(ancestorIds, r, "i4"); + }); + + CqnPredicate filter = CQL.get("node_id").in(ancestorIds.stream().toList()); + return topLevels(topLevels, filter); + } + + private void addIfNotNull(Set ancestorIds, Row r, String key) { + Integer id = (Integer) r.get(key); + if (id != null) { + ancestorIds.add(id); + } + } + + private List topLevels(CqnTopLevelsTransformation topLevels, CqnPredicate filter) { + return topLevels.levels() < 0 || !(topLevels.expandLevels().isEmpty()) ? topLevelsAll(filter) : topLevelsLimit(topLevels.levels(), filter); + } + + private List topLevelsLimit(long limit, CqnPredicate filter) { + Map lookup = new HashMap<>(); + + CqnSelect getRoots = Select.from(AdminService_.GENRE_HIERARCHY).where(gh -> gh.parent_id().eq(0).and(filter)); + List roots = db.run(getRoots).listOf(GenreHierarchy.class); + roots.forEach(root -> { + root.setDistanceFromRoot(0l); + lookup.put(root.getNodeId(), root); + List parents = List.of(root.getNodeId()); + for (long i = 1; i < limit; i++) { + List ps = parents; + CqnSelect getChildren = Select.from(AdminService_.GENRE_HIERARCHY).where(gh -> gh.parent_id().in(ps).and(filter)); + List children = db.run(getChildren).listOf(GenreHierarchy.class); + if (children.isEmpty()) { + break; + } + long dfr = i; + parents = children.stream().peek(gh -> { + gh.setParent(lookup.get(gh.getParentId())); + gh.setDistanceFromRoot(dfr); + lookup.put(gh.getNodeId(), gh); + }).map(GenreHierarchy::getNodeId).toList(); + } + }); + + return lookup.values().stream().sorted(new Sorter()).toList(); + } + + private List topLevelsAll(CqnPredicate filter) { + Map lookup = new HashMap<>(); + + CqnSelect allCqn = Select.from(AdminService_.GENRE_HIERARCHY).where(filter); + var all = db.run(allCqn).listOf(GenreHierarchy.class); + all.forEach(gh -> lookup.put(gh.getNodeId(), gh)); + all.forEach(gh -> gh.setParent(lookup.get(gh.getParentId()))); + all.forEach(gh -> gh.setDistanceFromRoot(distanceFromRoot(gh))); + + return all.stream().sorted(new Sorter()).toList(); + } + + private static long distanceFromRoot(GenreHierarchy gh) { + long dfr = 0; + while (gh.getParent() != null) { + dfr++; + gh = gh.getParent(); + } + + return dfr; + } + + private class Sorter implements Comparator { + + @Override + public int compare(GenreHierarchy gh1, GenreHierarchy gh2) { + Deque path1 = getPath(gh1); + Deque path2 = getPath(gh2); + int res = 0; + + while (!path1.isEmpty() && !path2.isEmpty()) { + String last1 = path1.pop(); + String last2 = path2.pop(); + res = last1.compareTo(last2); + if (res != 0) { + return res; + } + } + return res; + } + + Deque getPath(GenreHierarchy gh){ + Deque path = new ArrayDeque<>(); + do { + path.push(gh.getName()); + gh = gh.getParent(); + } while (gh != null); + + return path; + } + } +} From 45e6b03d7f14a8fdbab5e0eae1cd2a16083faaa2 Mon Sep 17 00:00:00 2001 From: Olena Date: Thu, 28 Nov 2024 15:17:14 +0100 Subject: [PATCH 08/26] Support of ExpandLevels (#403) Support of ExpandLevels in custom code --------- Co-authored-by: D070615 --- .../bookshop/handlers/HierarchyHandler.java | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java b/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java index 65c12aad..6780d8d3 100644 --- a/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java +++ b/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java @@ -169,11 +169,13 @@ private void addIfNotNull(Set ancestorIds, Row r, String key) { } private List topLevels(CqnTopLevelsTransformation topLevels, CqnPredicate filter) { - return topLevels.levels() < 0 || !(topLevels.expandLevels().isEmpty()) ? topLevelsAll(filter) : topLevelsLimit(topLevels.levels(), filter); + return topLevels.levels() < 0 ? topLevelsAll(filter) : topLevelsLimit(topLevels, filter); } - private List topLevelsLimit(long limit, CqnPredicate filter) { + private List topLevelsLimit(CqnTopLevelsTransformation topLevels, CqnPredicate filter) { + long limit = topLevels.levels(); Map lookup = new HashMap<>(); + Map expandLevels = topLevels.expandLevels(); CqnSelect getRoots = Select.from(AdminService_.GENRE_HIERARCHY).where(gh -> gh.parent_id().eq(0).and(filter)); List roots = db.run(getRoots).listOf(GenreHierarchy.class); @@ -197,6 +199,23 @@ private List topLevelsLimit(long limit, CqnPredicate filter) { } }); + if (!expandLevels.isEmpty()) { + List expandedIds = expandLevels.keySet().stream().map(key -> (Integer) key).toList(); + CqnSelect expandedCQN = Select.from(AdminService_.GENRE_HIERARCHY).where(gh -> + CQL.and(filter, + CQL.or(gh.node_id().in(expandedIds), gh.parent_id().in(expandedIds)))); + + List expanded = db.run(expandedCQN).listOf(GenreHierarchy.class); + expanded.forEach(gh -> { + if (!lookup.keySet().contains(gh.getNodeId())) { + gh.setParent(lookup.get(gh.getParentId())); + gh.setDistanceFromRoot(distanceFromRoot(gh)); + lookup.put(gh.getNodeId(), gh); + } + }); + + } + return lookup.values().stream().sorted(new Sorter()).toList(); } From 7bf5397eab255f4254350d2f4c040cef6569e0ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20G=C3=B6rler?= Date: Thu, 28 Nov 2024 15:22:13 +0100 Subject: [PATCH 09/26] cleanup --- .../bookshop/handlers/HierarchyHandler.java | 111 +++++++++++------- 1 file changed, 69 insertions(+), 42 deletions(-) diff --git a/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java b/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java index c675af84..306508e5 100644 --- a/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java +++ b/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java @@ -39,6 +39,8 @@ import cds.gen.adminservice.GenreHierarchy; import cds.gen.adminservice.GenreHierarchy_; +import static cds.gen.adminservice.AdminService_.GENRE_HIERARCHY; + @Component @Profile("default") @ServiceName(AdminService_.CDS_NAME) @@ -47,8 +49,8 @@ public class HierarchyHandler implements EventHandler { private final PersistenceService db; HierarchyHandler(PersistenceService db) { - this.db = db; - } + this.db = db; + } @Before(event = CqnService.EVENT_READ, entity = GenreHierarchy_.CDS_NAME) public void readGenreHierarchy(CdsReadEventContext event) { @@ -58,12 +60,12 @@ public void readGenreHierarchy(CdsReadEventContext event) { if (trafos.size() < 1) { return; } - + if (getTopLevels(trafos) instanceof CqnTopLevelsTransformation topLevels) { result = topLevels(topLevels, CQL.TRUE); } else if (trafos.get(0) instanceof CqnDescendantsTransformation descendants) { result = handleDescendants(descendants); - } else if (trafos.get(0) instanceof CqnAncestorsTransformation ancestors) { + } else if (trafos.get(0) instanceof CqnAncestorsTransformation ancestors) { if (trafos.size() == 2 && trafos.get(1) instanceof CqnTopLevelsTransformation topLevels) { result = handleAncestors(ancestors, topLevels); } else if (trafos.size() == 3 && trafos.get(2) instanceof CqnTopLevelsTransformation topLevels) { @@ -77,7 +79,8 @@ public void readGenreHierarchy(CdsReadEventContext event) { private CqnTopLevelsTransformation getTopLevels(List trafos) { if (trafos.get(0) instanceof CqnTopLevelsTransformation topLevels) { return topLevels; - } else if (trafos.size() == 2 && trafos.get(0) instanceof CqnOrderByTransformation && trafos.get(1) instanceof CqnTopLevelsTransformation topLevels) { + } else if (trafos.size() == 2 && trafos.get(0) instanceof CqnOrderByTransformation + && trafos.get(1) instanceof CqnTopLevelsTransformation topLevels) { return topLevels; } return null; @@ -93,8 +96,9 @@ private void setResult(CdsReadEventContext event, List result) { private void addDrillState(List ghs) { List ids = ghs.stream().map(gh -> gh.getId()).toList(); - Set parents = ghs.stream().map(gh -> gh.getParntId()).filter(p -> p != null).collect(Collectors.toSet()); - CqnSelect q = Select.from(AdminService_.GENRE_HIERARCHY).columns(gh -> gh.parnt_ID().as("id")) + Set parents = ghs.stream().map(gh -> gh.getParntId()).filter(p -> p != null) + .collect(Collectors.toSet()); + CqnSelect q = Select.from(GENRE_HIERARCHY).columns(gh -> gh.parnt_ID().as("id")) .where(gh -> gh.parnt_ID().in(ids)); Set nonLeafs = db .run(q) @@ -111,42 +115,43 @@ private void addDrillState(List ghs) { } else { gh.setDrillState("leaf"); } - } + } } - - private List handleDescendants(CqnDescendantsTransformation descendants) { - Map lookup = new HashMap<>(); - CqnFilterTransformation filter = (CqnFilterTransformation) descendants.transformations().get(0); - CqnSelect getRoot = Select.from(AdminService_.GENRE_HIERARCHY).where(filter.filter()); - GenreHierarchy root = db.run(getRoot).single(GenreHierarchy.class); - lookup.put(root.getId(), root); - CqnPredicate parentFilter = CQL.copy(filter.filter(), new Modifier() { + private CqnPredicate descendantsFilter(CqnDescendantsTransformation descendants) { + CqnTransformation trafo = descendants.transformations().get(0); + CqnPredicate start = ((CqnFilterTransformation) trafo).filter(); + CqnPredicate result = CQL.FALSE; + if (descendants.keepStart()) { + result = CQL.or(result, start); + } + CqnPredicate children = CQL.copy(start, new Modifier() { @Override public CqnValue ref(CqnElementRef ref) { return CQL.get(GenreHierarchy.PARNT_ID); } }); + result = CQL.or(result, children); - CqnSelect childrenCQN = Select.from(AdminService_.GENRE_HIERARCHY).where(parentFilter); - List children = db.run(childrenCQN).listOf(GenreHierarchy.class); - children.forEach(gh -> lookup.put(gh.getId(), gh)); - children.forEach(gh -> gh.setParnt(lookup.get(gh.getParntId()))); - - return children.stream().sorted(new Sorter()).toList(); + return result; } - private List handleAncestors(CqnAncestorsTransformation ancestors, CqnTopLevelsTransformation topLevels) { + private CqnPredicate ancestorsFilter(CqnAncestorsTransformation ancestors) { CqnTransformation trafo = ancestors.transformations().get(0); - Select inner = Select.from(AdminService_.GENRE_HIERARCHY).columns(gh -> gh.ID()); + Select inner = Select.from(GENRE_HIERARCHY).columns(gh -> gh.ID()); if (trafo instanceof CqnFilterTransformation filter) { inner.where(filter.filter()); } else if (trafo instanceof CqnSearchTransformation search) { inner.search(search.search()); } - Select outer = Select.from(AdminService_.GENRE_HIERARCHY).columns(gh -> gh.ID().as("i0"), gh -> gh.parnt().ID().as("i1"), - gh -> gh.parnt().parnt().ID().as("i2"), gh -> gh.parnt().parnt().parnt().ID().as("i3"), - gh -> gh.parnt().parnt().parnt().parnt().ID().as("i4")).where(gh -> gh.ID().in(inner)); + + Select outer = Select.from(GENRE_HIERARCHY) + .columns(gh -> gh.ID().as("i0"), + gh -> gh.parnt().ID().as("i1"), + gh -> gh.parnt().parnt().ID().as("i2"), + gh -> gh.parnt().parnt().parnt().ID().as("i3"), + gh -> gh.parnt().parnt().parnt().parnt().ID().as("i4")) + .where(gh -> gh.ID().in(inner)); Set ancestorIds = new HashSet<>(); db.run(outer).stream().forEach(r -> { @@ -157,7 +162,30 @@ private List handleAncestors(CqnAncestorsTransformation ancestor addIfNotNull(ancestorIds, r, "i4"); }); - CqnPredicate filter = CQL.get(GenreHierarchy_.ID).in(ancestorIds.stream().toList()); + return CQL.get(GenreHierarchy_.ID).in(ancestorIds.stream().toList()); + } + + private List handleDescendants(CqnDescendantsTransformation descendants) { + CqnPredicate filter = descendantsFilter(descendants); + CqnSelect childrenCQN = Select.from(GENRE_HIERARCHY).where(filter); + List nodes = db.run(childrenCQN).listOf(GenreHierarchy.class); + + connect(nodes); + + return nodes.stream().sorted(new Sorter()).toList(); + } + + private static void connect(List nodes) { + Map lookup = new HashMap<>(); + nodes.forEach(gh -> lookup.put(gh.getId(), gh)); + nodes.forEach(gh -> gh.setParnt(lookup.get(gh.getParntId()))); + nodes.forEach(gh -> gh.setDistanceFromRoot(distanceFromRoot(gh))); + } + + private List handleAncestors(CqnAncestorsTransformation ancestors, + CqnTopLevelsTransformation topLevels) { + CqnPredicate filter = ancestorsFilter(ancestors); + return topLevels(topLevels, filter); } @@ -166,16 +194,17 @@ private void addIfNotNull(Set ancestorIds, Row r, String key) { if (id != null) { ancestorIds.add(id); } - } + } private List topLevels(CqnTopLevelsTransformation topLevels, CqnPredicate filter) { - return topLevels.levels() < 0 || !(topLevels.expandLevels().isEmpty()) ? topLevelsAll(filter) : topLevelsLimit(topLevels.levels(), filter); + return topLevels.levels() < 0 || !(topLevels.expandLevels().isEmpty()) ? topLevelsAll(filter) + : topLevelsLimit(topLevels.levels(), filter); } private List topLevelsLimit(long limit, CqnPredicate filter) { - Map lookup = new HashMap<>(); + Map lookup = new HashMap<>(); - CqnSelect getRoots = Select.from(AdminService_.GENRE_HIERARCHY).where(gh -> gh.parnt_ID().isNull().and(filter)); + CqnSelect getRoots = Select.from(GENRE_HIERARCHY).where(gh -> gh.parnt_ID().isNull().and(filter)); List roots = db.run(getRoots).listOf(GenreHierarchy.class); roots.forEach(root -> { root.setDistanceFromRoot(0l); @@ -183,7 +212,8 @@ private List topLevelsLimit(long limit, CqnPredicate filter) { List parents = List.of(root.getId()); for (long i = 1; i < limit; i++) { List ps = parents; - CqnSelect getChildren = Select.from(AdminService_.GENRE_HIERARCHY).where(gh -> gh.parnt_ID().in(ps).and(filter)); + CqnSelect getChildren = Select.from(GENRE_HIERARCHY) + .where(gh -> gh.parnt_ID().in(ps).and(filter)); List children = db.run(getChildren).listOf(GenreHierarchy.class); if (children.isEmpty()) { break; @@ -201,13 +231,10 @@ private List topLevelsLimit(long limit, CqnPredicate filter) { } private List topLevelsAll(CqnPredicate filter) { - Map lookup = new HashMap<>(); - - CqnSelect allCqn = Select.from(AdminService_.GENRE_HIERARCHY).where(filter); + CqnSelect allCqn = Select.from(GENRE_HIERARCHY).where(filter); var all = db.run(allCqn).listOf(GenreHierarchy.class); - all.forEach(gh -> lookup.put(gh.getId(), gh)); - all.forEach(gh -> gh.setParnt(lookup.get(gh.getParntId()))); - all.forEach(gh -> gh.setDistanceFromRoot(distanceFromRoot(gh))); + + connect(all); return all.stream().sorted(new Sorter()).toList(); } @@ -241,14 +268,14 @@ public int compare(GenreHierarchy gh1, GenreHierarchy gh2) { return res; } - Deque getPath(GenreHierarchy gh){ + Deque getPath(GenreHierarchy gh) { Deque path = new ArrayDeque<>(); do { path.push(gh.getName()); gh = gh.getParnt(); - } while (gh != null); + } while (gh != null); return path; - } + } } } From cd01357667a09d3e15431965c7aa75ff6f3ff419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20G=C3=B6rler?= Date: Thu, 28 Nov 2024 15:45:59 +0100 Subject: [PATCH 10/26] No projection (#401) --- app/genres/fiori-service.cds | 4 +- db/books.cds | 6 +- db/data/my.bookshop-Genres.csv | 32 ++-- pom.xml | 4 +- srv/admin-service.cds | 6 +- .../bookshop/handlers/HierarchyHandler.java | 140 +++++++++++------- srv/src/main/resources/application.yaml | 3 +- 7 files changed, 109 insertions(+), 86 deletions(-) diff --git a/app/genres/fiori-service.cds b/app/genres/fiori-service.cds index d195d935..77b132a5 100644 --- a/app/genres/fiori-service.cds +++ b/app/genres/fiori-service.cds @@ -7,8 +7,8 @@ using AdminService from '../../srv/admin-service'; annotate AdminService.GenreHierarchy with @Aggregation.RecursiveHierarchy#GenreHierarchy: { $Type: 'Aggregation.RecursiveHierarchyType', - NodeProperty: node_id, // identifies a node - ParentNavigationProperty: parent // navigates to a node's parent + NodeProperty: ID, // identifies a node + ParentNavigationProperty: parnt // navigates to a node's parent }; annotate AdminService.GenreHierarchy with @Hierarchy.RecursiveHierarchy#GenreHierarchy: { diff --git a/db/books.cds b/db/books.cds index a4d8ba44..67f9fd88 100644 --- a/db/books.cds +++ b/db/books.cds @@ -47,9 +47,9 @@ annotate Authors with */ entity Genres { key ID : Integer; - node : Integer not null; - parent_node : Integer default 0; name : localized String(255); descr : localized String(1000); - parent : Association to one Genres on parent.node = parent_node; + parnt : Association to Genres; + children : Composition of many Genres + on children.parnt = $self; } diff --git a/db/data/my.bookshop-Genres.csv b/db/data/my.bookshop-Genres.csv index 044b2b47..f498be2d 100644 --- a/db/data/my.bookshop-Genres.csv +++ b/db/data/my.bookshop-Genres.csv @@ -1,16 +1,16 @@ -ID;parent_node;name;node -10;;Fiction;10 -11;10;Drama;11 -12;10;Poetry;12 -13;10;Fantasy;13 -14;10;Science Fiction;14 -15;10;Romance;15 -16;10;Mystery;16 -17;10;Thriller;17 -18;10;Dystopia;18 -19;10;Fairy Tale;19 -20;;Non-Fiction;20 -21;20;Biography;21 -22;21;Autobiography;22 -23;20;Essay;23 -24;20;Speech;24 \ No newline at end of file +ID;parnt_ID;name +10;;Fiction +11;10;Drama +12;10;Poetry +13;10;Fantasy +14;10;Science Fiction +15;10;Romance +16;10;Mystery +17;10;Thriller +18;10;Dystopia +19;10;Fairy Tale +20;;Non-Fiction +21;20;Biography +22;21;Autobiography +23;20;Essay +24;20;Speech \ No newline at end of file diff --git a/pom.xml b/pom.xml index 98a241b6..9dac44cf 100644 --- a/pom.xml +++ b/pom.xml @@ -23,11 +23,11 @@ 21 - 3.4.0 + 3.5.0 5.13.0 3.5.3 3.8.4 - 8.4.1 + 8.5.0 diff --git a/srv/admin-service.cds b/srv/admin-service.cds index 7709ebd0..32fb80e8 100644 --- a/srv/admin-service.cds +++ b/srv/admin-service.cds @@ -16,11 +16,7 @@ service AdminService @(requires : 'admin') { entity Orders as select from my.Orders; extend my.Genres with Hierarchy; - entity GenreHierarchy as projection on my.Genres { - node as node_id, - parent_node as parent_id, - * - } excluding { node, parent_node } + entity GenreHierarchy as projection on my.Genres; @cds.persistence.skip entity Upload @odata.singleton { diff --git a/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java b/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java index 6780d8d3..d9ea0fa4 100644 --- a/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java +++ b/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java @@ -39,6 +39,8 @@ import cds.gen.adminservice.GenreHierarchy; import cds.gen.adminservice.GenreHierarchy_; +import static cds.gen.adminservice.AdminService_.GENRE_HIERARCHY; + @Component @Profile("default") @ServiceName(AdminService_.CDS_NAME) @@ -47,8 +49,8 @@ public class HierarchyHandler implements EventHandler { private final PersistenceService db; HierarchyHandler(PersistenceService db) { - this.db = db; - } + this.db = db; + } @Before(event = CqnService.EVENT_READ, entity = GenreHierarchy_.CDS_NAME) public void readGenreHierarchy(CdsReadEventContext event) { @@ -58,12 +60,12 @@ public void readGenreHierarchy(CdsReadEventContext event) { if (trafos.size() < 1) { return; } - + if (getTopLevels(trafos) instanceof CqnTopLevelsTransformation topLevels) { result = topLevels(topLevels, CQL.TRUE); } else if (trafos.get(0) instanceof CqnDescendantsTransformation descendants) { result = handleDescendants(descendants); - } else if (trafos.get(0) instanceof CqnAncestorsTransformation ancestors) { + } else if (trafos.get(0) instanceof CqnAncestorsTransformation ancestors) { if (trafos.size() == 2 && trafos.get(1) instanceof CqnTopLevelsTransformation topLevels) { result = handleAncestors(ancestors, topLevels); } else if (trafos.size() == 3 && trafos.get(2) instanceof CqnTopLevelsTransformation topLevels) { @@ -77,7 +79,8 @@ public void readGenreHierarchy(CdsReadEventContext event) { private CqnTopLevelsTransformation getTopLevels(List trafos) { if (trafos.get(0) instanceof CqnTopLevelsTransformation topLevels) { return topLevels; - } else if (trafos.size() == 2 && trafos.get(0) instanceof CqnOrderByTransformation && trafos.get(1) instanceof CqnTopLevelsTransformation topLevels) { + } else if (trafos.size() == 2 && trafos.get(0) instanceof CqnOrderByTransformation + && trafos.get(1) instanceof CqnTopLevelsTransformation topLevels) { return topLevels; } return null; @@ -92,16 +95,17 @@ private void setResult(CdsReadEventContext event, List result) { } private void addDrillState(List ghs) { - List ids = ghs.stream().map(gh -> gh.getNodeId()).toList(); - Set parents = ghs.stream().map(gh -> gh.getParentId()).filter(p -> p != 0).collect(Collectors.toSet()); - CqnSelect q = Select.from(AdminService_.GENRE_HIERARCHY, gh -> gh.parent()).columns(gh -> gh.node_id()) - .where(gh -> gh.node_id().in(ids)); + List ids = ghs.stream().map(gh -> gh.getId()).toList(); + Set parents = ghs.stream().map(gh -> gh.getParntId()).filter(p -> p != null) + .collect(Collectors.toSet()); + CqnSelect q = Select.from(GENRE_HIERARCHY).columns(gh -> gh.parnt_ID().as("id")) + .where(gh -> gh.parnt_ID().in(ids)); Set nonLeafs = db .run(q) - .stream().map(r -> r.get(GenreHierarchy.NODE_ID)).collect(Collectors.toSet()); + .stream().map(r -> r.get("id")).collect(Collectors.toSet()); for (GenreHierarchy gh : ghs) { - Integer id = gh.getNodeId(); + Integer id = gh.getId(); if (nonLeafs.contains(id)) { if (parents.contains(id)) { gh.setDrillState("expanded"); @@ -111,42 +115,43 @@ private void addDrillState(List ghs) { } else { gh.setDrillState("leaf"); } - } + } } - - private List handleDescendants(CqnDescendantsTransformation descendants) { - Map lookup = new HashMap<>(); - CqnFilterTransformation filter = (CqnFilterTransformation) descendants.transformations().get(0); - CqnSelect getRoot = Select.from(AdminService_.GENRE_HIERARCHY).where(filter.filter()); - GenreHierarchy root = db.run(getRoot).single(GenreHierarchy.class); - lookup.put(root.getNodeId(), root); - CqnPredicate parentFilter = CQL.copy(filter.filter(), new Modifier() { + private CqnPredicate descendantsFilter(CqnDescendantsTransformation descendants) { + CqnTransformation trafo = descendants.transformations().get(0); + CqnPredicate start = ((CqnFilterTransformation) trafo).filter(); + CqnPredicate result = CQL.FALSE; + if (descendants.keepStart()) { + result = CQL.or(result, start); + } + CqnPredicate children = CQL.copy(start, new Modifier() { @Override public CqnValue ref(CqnElementRef ref) { - return CQL.get(GenreHierarchy.PARENT_ID); + return CQL.get(GenreHierarchy.PARNT_ID); } }); + result = CQL.or(result, children); - CqnSelect childrenCQN = Select.from(AdminService_.GENRE_HIERARCHY).where(parentFilter); - List children = db.run(childrenCQN).listOf(GenreHierarchy.class); - children.forEach(gh -> lookup.put(gh.getNodeId(), gh)); - children.forEach(gh -> gh.setParent(lookup.get(gh.getParentId()))); - - return children.stream().sorted(new Sorter()).toList(); + return result; } - private List handleAncestors(CqnAncestorsTransformation ancestors, CqnTopLevelsTransformation topLevels) { + private CqnPredicate ancestorsFilter(CqnAncestorsTransformation ancestors) { CqnTransformation trafo = ancestors.transformations().get(0); - Select inner = Select.from(AdminService_.GENRE_HIERARCHY).columns(gh -> gh.node_id()); + Select inner = Select.from(GENRE_HIERARCHY).columns(gh -> gh.ID()); if (trafo instanceof CqnFilterTransformation filter) { inner.where(filter.filter()); } else if (trafo instanceof CqnSearchTransformation search) { inner.search(search.search()); } - Select outer = Select.from(AdminService_.GENRE_HIERARCHY).columns(gh -> gh.node_id().as("i0"), gh -> gh.parent().node_id().as("i1"), - gh -> gh.parent().parent().node_id().as("i2"), gh -> gh.parent().parent().parent().node_id().as("i3"), - gh -> gh.parent().parent().parent().parent().node_id().as("i4")).where(gh -> gh.node_id().in(inner)); + + Select outer = Select.from(GENRE_HIERARCHY) + .columns(gh -> gh.ID().as("i0"), + gh -> gh.parnt().ID().as("i1"), + gh -> gh.parnt().parnt().ID().as("i2"), + gh -> gh.parnt().parnt().parnt().ID().as("i3"), + gh -> gh.parnt().parnt().parnt().parnt().ID().as("i4")) + .where(gh -> gh.ID().in(inner)); Set ancestorIds = new HashSet<>(); db.run(outer).stream().forEach(r -> { @@ -157,7 +162,30 @@ private List handleAncestors(CqnAncestorsTransformation ancestor addIfNotNull(ancestorIds, r, "i4"); }); - CqnPredicate filter = CQL.get("node_id").in(ancestorIds.stream().toList()); + return CQL.get(GenreHierarchy_.ID).in(ancestorIds.stream().toList()); + } + + private List handleDescendants(CqnDescendantsTransformation descendants) { + CqnPredicate filter = descendantsFilter(descendants); + CqnSelect childrenCQN = Select.from(GENRE_HIERARCHY).where(filter); + List nodes = db.run(childrenCQN).listOf(GenreHierarchy.class); + + connect(nodes); + + return nodes.stream().sorted(new Sorter()).toList(); + } + + private static void connect(List nodes) { + Map lookup = new HashMap<>(); + nodes.forEach(gh -> lookup.put(gh.getId(), gh)); + nodes.forEach(gh -> gh.setParnt(lookup.get(gh.getParntId()))); + nodes.forEach(gh -> gh.setDistanceFromRoot(distanceFromRoot(gh))); + } + + private List handleAncestors(CqnAncestorsTransformation ancestors, + CqnTopLevelsTransformation topLevels) { + CqnPredicate filter = ancestorsFilter(ancestors); + return topLevels(topLevels, filter); } @@ -166,7 +194,7 @@ private void addIfNotNull(Set ancestorIds, Row r, String key) { if (id != null) { ancestorIds.add(id); } - } + } private List topLevels(CqnTopLevelsTransformation topLevels, CqnPredicate filter) { return topLevels.levels() < 0 ? topLevelsAll(filter) : topLevelsLimit(topLevels, filter); @@ -177,25 +205,26 @@ private List topLevelsLimit(CqnTopLevelsTransformation topLevels Map lookup = new HashMap<>(); Map expandLevels = topLevels.expandLevels(); - CqnSelect getRoots = Select.from(AdminService_.GENRE_HIERARCHY).where(gh -> gh.parent_id().eq(0).and(filter)); + CqnSelect getRoots = Select.from(GENRE_HIERARCHY).where(gh -> gh.parnt_ID().isNull().and(filter)); List roots = db.run(getRoots).listOf(GenreHierarchy.class); roots.forEach(root -> { root.setDistanceFromRoot(0l); - lookup.put(root.getNodeId(), root); - List parents = List.of(root.getNodeId()); + lookup.put(root.getId(), root); + List parents = List.of(root.getId()); for (long i = 1; i < limit; i++) { List ps = parents; - CqnSelect getChildren = Select.from(AdminService_.GENRE_HIERARCHY).where(gh -> gh.parent_id().in(ps).and(filter)); + CqnSelect getChildren = Select.from(GENRE_HIERARCHY) + .where(gh -> gh.parnt_ID().in(ps).and(filter)); List children = db.run(getChildren).listOf(GenreHierarchy.class); if (children.isEmpty()) { break; } long dfr = i; parents = children.stream().peek(gh -> { - gh.setParent(lookup.get(gh.getParentId())); + gh.setParnt(lookup.get(gh.getParntId())); gh.setDistanceFromRoot(dfr); - lookup.put(gh.getNodeId(), gh); - }).map(GenreHierarchy::getNodeId).toList(); + lookup.put(gh.getId(), gh); + }).map(GenreHierarchy::getId).toList(); } }); @@ -203,14 +232,14 @@ private List topLevelsLimit(CqnTopLevelsTransformation topLevels List expandedIds = expandLevels.keySet().stream().map(key -> (Integer) key).toList(); CqnSelect expandedCQN = Select.from(AdminService_.GENRE_HIERARCHY).where(gh -> CQL.and(filter, - CQL.or(gh.node_id().in(expandedIds), gh.parent_id().in(expandedIds)))); + CQL.or(gh.ID().in(expandedIds), gh.parnt_ID().in(expandedIds)))); List expanded = db.run(expandedCQN).listOf(GenreHierarchy.class); expanded.forEach(gh -> { - if (!lookup.keySet().contains(gh.getNodeId())) { - gh.setParent(lookup.get(gh.getParentId())); + if (!lookup.keySet().contains(gh.getId())) { + gh.setParnt(lookup.get(gh.getParntId())); gh.setDistanceFromRoot(distanceFromRoot(gh)); - lookup.put(gh.getNodeId(), gh); + lookup.put(gh.getId(), gh); } }); @@ -220,22 +249,19 @@ private List topLevelsLimit(CqnTopLevelsTransformation topLevels } private List topLevelsAll(CqnPredicate filter) { - Map lookup = new HashMap<>(); - - CqnSelect allCqn = Select.from(AdminService_.GENRE_HIERARCHY).where(filter); + CqnSelect allCqn = Select.from(GENRE_HIERARCHY).where(filter); var all = db.run(allCqn).listOf(GenreHierarchy.class); - all.forEach(gh -> lookup.put(gh.getNodeId(), gh)); - all.forEach(gh -> gh.setParent(lookup.get(gh.getParentId()))); - all.forEach(gh -> gh.setDistanceFromRoot(distanceFromRoot(gh))); + + connect(all); return all.stream().sorted(new Sorter()).toList(); } private static long distanceFromRoot(GenreHierarchy gh) { long dfr = 0; - while (gh.getParent() != null) { + while (gh.getParnt() != null) { dfr++; - gh = gh.getParent(); + gh = gh.getParnt(); } return dfr; @@ -260,14 +286,14 @@ public int compare(GenreHierarchy gh1, GenreHierarchy gh2) { return res; } - Deque getPath(GenreHierarchy gh){ + Deque getPath(GenreHierarchy gh) { Deque path = new ArrayDeque<>(); do { path.push(gh.getName()); - gh = gh.getParent(); - } while (gh != null); + gh = gh.getParnt(); + } while (gh != null); return path; - } + } } } diff --git a/srv/src/main/resources/application.yaml b/srv/src/main/resources/application.yaml index dad875b3..f28a0738 100644 --- a/srv/src/main/resources/application.yaml +++ b/srv/src/main/resources/application.yaml @@ -1,7 +1,8 @@ --- logging: level: - '[com.sap.cds.auditlog]': DEBUG + com.sap.cds.auditlog: DEBUG + com.sap.cds.persistence.sql: DEBUG spring: jmx: enabled: true From 3cd209acba0d7bdac92d143f4ffdcf6827151f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20G=C3=B6rler?= Date: Thu, 28 Nov 2024 16:05:12 +0100 Subject: [PATCH 11/26] fix sorter --- .../main/java/my/bookshop/handlers/HierarchyHandler.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java b/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java index d9ea0fa4..4d826094 100644 --- a/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java +++ b/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java @@ -275,7 +275,13 @@ public int compare(GenreHierarchy gh1, GenreHierarchy gh2) { Deque path2 = getPath(gh2); int res = 0; - while (!path1.isEmpty() && !path2.isEmpty()) { + while (true) { + if (path1.isEmpty()) { + return path2.isEmpty() ? 0 : -1; + } + if (path2.isEmpty()) { + return +1; + } String last1 = path1.pop(); String last2 = path2.pop(); res = last1.compareTo(last2); @@ -283,7 +289,6 @@ public int compare(GenreHierarchy gh1, GenreHierarchy gh2) { return res; } } - return res; } Deque getPath(GenreHierarchy gh) { From 7e2153ef97fe65d41310fdf705f22c4f5d6dd999 Mon Sep 17 00:00:00 2001 From: D070615 Date: Fri, 29 Nov 2024 17:01:05 +0100 Subject: [PATCH 12/26] added tests for GenreHierarchy --- .../java/my/bookshop/GenreHierarchyTest.java | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 srv/src/test/java/my/bookshop/GenreHierarchyTest.java diff --git a/srv/src/test/java/my/bookshop/GenreHierarchyTest.java b/srv/src/test/java/my/bookshop/GenreHierarchyTest.java new file mode 100644 index 00000000..8f7dd583 --- /dev/null +++ b/srv/src/test/java/my/bookshop/GenreHierarchyTest.java @@ -0,0 +1,118 @@ +package my.bookshop; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles({"hybrid","default"}) +public class GenreHierarchyTest { + + @Autowired + private MockMvc client; + + private static final String genresURI = "/api/admin/GenreHierarchy"; + + @Test + @WithMockUser(username = "admin") + void testGetAll() throws Exception { + client.perform(get(genresURI)).andExpect(status().isOk()); + } + + @Test + @WithMockUser(username = "admin") + void testCountAll() throws Exception { + client.perform(get(genresURI + "/$count")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").value(15)); + } + + @Test + @WithMockUser(username = "admin") + void testStartOneLevel() throws Exception { + client.perform(get(genresURI + + "?$select=DrillState,ID,name" + + "&$apply=com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID',Levels=1)" + + "&$count=true&$skip=218&$top=20")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.value[0].ID").value(10)) + .andExpect(jsonPath("$.value[0].name").value("Fiction")) + .andExpect(jsonPath("$.value[0].DistanceFromRoot").value(0)) + .andExpect(jsonPath("$.value[0].DrillState").value("collapsed")) + .andExpect(jsonPath("$.value[0].parnt_ID").isEmpty()) + .andExpect(jsonPath("$.value[1].ID").value(20)) + .andExpect(jsonPath("$.value[1].name").value("Non-Fiction")) + .andExpect(jsonPath("$.value[1].DistanceFromRoot").value(0)) + .andExpect(jsonPath("$.value[1].DrillState").value("collapsed")) + .andExpect(jsonPath("$.value[1].parnt_ID").isEmpty()) + .andExpect(jsonPath("$.value[2]").doesNotExist()); + + } + + @Test + @WithMockUser(username = "admin") + void testExpandNonFiction() throws Exception { + client.perform(get(genresURI + + "?$select=DrillState,ID,name" + + "&$apply=descendants($root/GenreHierarchy,GenreHierarchy,ID,filter(ID eq 20),1)" + + "&$count=true&$skip=0&$top=218")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.value[0].ID").value(21)) + .andExpect(jsonPath("$.value[0].name").value("Biography")) + // DistanceFromRoot: fix me? + .andExpect(jsonPath("$.value[0].DistanceFromRoot").value(0)) + .andExpect(jsonPath("$.value[0].DrillState").value("collapsed")) + .andExpect(jsonPath("$.value[0].parnt_ID").value(20)) + .andExpect(jsonPath("$.value[1].ID").value(23)) + .andExpect(jsonPath("$.value[1].name").value("Essay")) + .andExpect(jsonPath("$.value[1].DistanceFromRoot").value(0)) + .andExpect(jsonPath("$.value[1].DrillState").value("leaf")) + .andExpect(jsonPath("$.value[1].parnt_ID").value(20)) + .andExpect(jsonPath("$.value[2].ID").value(24)) + .andExpect(jsonPath("$.value[2].name").value("Speech")) + .andExpect(jsonPath("$.value[2].DistanceFromRoot").value(0)) + .andExpect(jsonPath("$.value[2].DrillState").value("leaf")) + .andExpect(jsonPath("$.value[2].parnt_ID").value(20)) + .andExpect(jsonPath("$.value[3]").doesNotExist()); + } + + @Test + @WithMockUser(username = "admin") + void testCollapseAll() throws Exception { + client.perform(get(genresURI + + "?$select=DrillState,ID,name" + + "&$apply=com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID',Levels=1)" + + "&$count=true&$skip=0&$top=238")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.value[0].name").value("Fiction")) + .andExpect(jsonPath("$.value[0].DrillState").value("collapsed")) + .andExpect(jsonPath("$.value[1].name").value("Non-Fiction")) + .andExpect(jsonPath("$.value[1].DrillState").value("collapsed")) + .andExpect(jsonPath("$.value[2]").doesNotExist()); + } + + @Test + @WithMockUser(username = "admin") + void testExpandAll() throws Exception { + client.perform(get(genresURI + + "?$select=DistanceFromRoot,DrillState,ID,LimitedDescendantCount,name" + + "&$apply=com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID')" + + "&$count=true&$skip=0&$top=238")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.value[0].name").value("Fiction")) + .andExpect(jsonPath("$.value[0].DrillState").value("expanded")) + .andExpect(jsonPath("$.value[14].name").value("Speech")) + .andExpect(jsonPath("$.value[14].DrillState").value("leaf")) + .andExpect(jsonPath("$.value[15]").doesNotExist()); + } +} From ae813c2e44f7d1b84605459c641de20651d6c21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20G=C3=B6rler?= Date: Mon, 2 Dec 2024 13:50:02 +0100 Subject: [PATCH 13/26] test in hybrid mode --- README.md | 9 +++++++ .../java/my/bookshop/GenreHierarchyTest.java | 24 +++++++------------ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 00df451e..587b31d6 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ User Interface related Features: - [Model Localization](https://cap.cloud.sap/docs/guides/i18n) for [English](app/_i18n/i18n.properties) and [German](app/_i18n/i18n_de.properties) language for static texts - [Custom File Upload extension](app/admin/webapp/extension/Upload.js) which provides a button for uploading `CSV` files - A simple Swagger UI for the CatalogService API at +- UI5 Tree Table CDS Maven Plugin Features: @@ -138,6 +139,14 @@ are defined for local development: - User: `user`, password: `user` to browse books - User: `admin`, password: `admin` to manage books and orders +### Testing in hybrid mode + +You can test the `GenreHierarchyTest` on H2 using the profile `default` as well as on HANA using the profile `hybrid` + +``` +cds bind --exec -- mvn clean install -Dspring.profiles.active=hybrid +``` + ## Using VS Code VS Code supports the project out-of-the-box, when using the [Extension Pack for Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack). diff --git a/srv/src/test/java/my/bookshop/GenreHierarchyTest.java b/srv/src/test/java/my/bookshop/GenreHierarchyTest.java index 8f7dd583..c16dd0c6 100644 --- a/srv/src/test/java/my/bookshop/GenreHierarchyTest.java +++ b/srv/src/test/java/my/bookshop/GenreHierarchyTest.java @@ -10,12 +10,11 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; @SpringBootTest @AutoConfigureMockMvc -@ActiveProfiles({"hybrid","default"}) +//@ActiveProfiles({"hybrid"}) public class GenreHierarchyTest { @Autowired @@ -41,20 +40,19 @@ void testCountAll() throws Exception { @WithMockUser(username = "admin") void testStartOneLevel() throws Exception { client.perform(get(genresURI - + "?$select=DrillState,ID,name" - + "&$apply=com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID',Levels=1)" - + "&$count=true&$skip=218&$top=20")) + + "?$select=DrillState,ID,name,DistanceFromRoot" + + "&$apply=orderby(name)/" + + "com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID',Levels=1)" + + "&$count=true")) .andExpect(status().isOk()) .andExpect(jsonPath("$.value[0].ID").value(10)) .andExpect(jsonPath("$.value[0].name").value("Fiction")) .andExpect(jsonPath("$.value[0].DistanceFromRoot").value(0)) .andExpect(jsonPath("$.value[0].DrillState").value("collapsed")) - .andExpect(jsonPath("$.value[0].parnt_ID").isEmpty()) .andExpect(jsonPath("$.value[1].ID").value(20)) .andExpect(jsonPath("$.value[1].name").value("Non-Fiction")) .andExpect(jsonPath("$.value[1].DistanceFromRoot").value(0)) .andExpect(jsonPath("$.value[1].DrillState").value("collapsed")) - .andExpect(jsonPath("$.value[1].parnt_ID").isEmpty()) .andExpect(jsonPath("$.value[2]").doesNotExist()); } @@ -65,24 +63,17 @@ void testExpandNonFiction() throws Exception { client.perform(get(genresURI + "?$select=DrillState,ID,name" + "&$apply=descendants($root/GenreHierarchy,GenreHierarchy,ID,filter(ID eq 20),1)" - + "&$count=true&$skip=0&$top=218")) + + "/orderby(ID)")) .andExpect(status().isOk()) .andExpect(jsonPath("$.value[0].ID").value(21)) .andExpect(jsonPath("$.value[0].name").value("Biography")) - // DistanceFromRoot: fix me? - .andExpect(jsonPath("$.value[0].DistanceFromRoot").value(0)) .andExpect(jsonPath("$.value[0].DrillState").value("collapsed")) - .andExpect(jsonPath("$.value[0].parnt_ID").value(20)) .andExpect(jsonPath("$.value[1].ID").value(23)) .andExpect(jsonPath("$.value[1].name").value("Essay")) - .andExpect(jsonPath("$.value[1].DistanceFromRoot").value(0)) .andExpect(jsonPath("$.value[1].DrillState").value("leaf")) - .andExpect(jsonPath("$.value[1].parnt_ID").value(20)) .andExpect(jsonPath("$.value[2].ID").value(24)) .andExpect(jsonPath("$.value[2].name").value("Speech")) - .andExpect(jsonPath("$.value[2].DistanceFromRoot").value(0)) .andExpect(jsonPath("$.value[2].DrillState").value("leaf")) - .andExpect(jsonPath("$.value[2].parnt_ID").value(20)) .andExpect(jsonPath("$.value[3]").doesNotExist()); } @@ -109,8 +100,11 @@ void testExpandAll() throws Exception { + "&$apply=com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID')" + "&$count=true&$skip=0&$top=238")) .andExpect(status().isOk()) + .andExpect(jsonPath("$.value[0].ID").value(10)) .andExpect(jsonPath("$.value[0].name").value("Fiction")) .andExpect(jsonPath("$.value[0].DrillState").value("expanded")) + .andExpect(jsonPath("$.value[0].DistanceFromRoot").value(0)) + .andExpect(jsonPath("$.value[0].LimitedDescendantCount").value(9)) .andExpect(jsonPath("$.value[14].name").value("Speech")) .andExpect(jsonPath("$.value[14].DrillState").value("leaf")) .andExpect(jsonPath("$.value[15]").doesNotExist()); From b8d82854307d8ef717043d137dbf5d9b6bd09214 Mon Sep 17 00:00:00 2001 From: Olena Date: Mon, 2 Dec 2024 14:27:08 +0100 Subject: [PATCH 14/26] Test: added tests for GenreHierarchy (#404) --- README.md | 9 ++ .../java/my/bookshop/GenreHierarchyTest.java | 112 ++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 srv/src/test/java/my/bookshop/GenreHierarchyTest.java diff --git a/README.md b/README.md index 00df451e..587b31d6 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ User Interface related Features: - [Model Localization](https://cap.cloud.sap/docs/guides/i18n) for [English](app/_i18n/i18n.properties) and [German](app/_i18n/i18n_de.properties) language for static texts - [Custom File Upload extension](app/admin/webapp/extension/Upload.js) which provides a button for uploading `CSV` files - A simple Swagger UI for the CatalogService API at +- UI5 Tree Table CDS Maven Plugin Features: @@ -138,6 +139,14 @@ are defined for local development: - User: `user`, password: `user` to browse books - User: `admin`, password: `admin` to manage books and orders +### Testing in hybrid mode + +You can test the `GenreHierarchyTest` on H2 using the profile `default` as well as on HANA using the profile `hybrid` + +``` +cds bind --exec -- mvn clean install -Dspring.profiles.active=hybrid +``` + ## Using VS Code VS Code supports the project out-of-the-box, when using the [Extension Pack for Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack). diff --git a/srv/src/test/java/my/bookshop/GenreHierarchyTest.java b/srv/src/test/java/my/bookshop/GenreHierarchyTest.java new file mode 100644 index 00000000..c16dd0c6 --- /dev/null +++ b/srv/src/test/java/my/bookshop/GenreHierarchyTest.java @@ -0,0 +1,112 @@ +package my.bookshop; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest +@AutoConfigureMockMvc +//@ActiveProfiles({"hybrid"}) +public class GenreHierarchyTest { + + @Autowired + private MockMvc client; + + private static final String genresURI = "/api/admin/GenreHierarchy"; + + @Test + @WithMockUser(username = "admin") + void testGetAll() throws Exception { + client.perform(get(genresURI)).andExpect(status().isOk()); + } + + @Test + @WithMockUser(username = "admin") + void testCountAll() throws Exception { + client.perform(get(genresURI + "/$count")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").value(15)); + } + + @Test + @WithMockUser(username = "admin") + void testStartOneLevel() throws Exception { + client.perform(get(genresURI + + "?$select=DrillState,ID,name,DistanceFromRoot" + + "&$apply=orderby(name)/" + + "com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID',Levels=1)" + + "&$count=true")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.value[0].ID").value(10)) + .andExpect(jsonPath("$.value[0].name").value("Fiction")) + .andExpect(jsonPath("$.value[0].DistanceFromRoot").value(0)) + .andExpect(jsonPath("$.value[0].DrillState").value("collapsed")) + .andExpect(jsonPath("$.value[1].ID").value(20)) + .andExpect(jsonPath("$.value[1].name").value("Non-Fiction")) + .andExpect(jsonPath("$.value[1].DistanceFromRoot").value(0)) + .andExpect(jsonPath("$.value[1].DrillState").value("collapsed")) + .andExpect(jsonPath("$.value[2]").doesNotExist()); + + } + + @Test + @WithMockUser(username = "admin") + void testExpandNonFiction() throws Exception { + client.perform(get(genresURI + + "?$select=DrillState,ID,name" + + "&$apply=descendants($root/GenreHierarchy,GenreHierarchy,ID,filter(ID eq 20),1)" + + "/orderby(ID)")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.value[0].ID").value(21)) + .andExpect(jsonPath("$.value[0].name").value("Biography")) + .andExpect(jsonPath("$.value[0].DrillState").value("collapsed")) + .andExpect(jsonPath("$.value[1].ID").value(23)) + .andExpect(jsonPath("$.value[1].name").value("Essay")) + .andExpect(jsonPath("$.value[1].DrillState").value("leaf")) + .andExpect(jsonPath("$.value[2].ID").value(24)) + .andExpect(jsonPath("$.value[2].name").value("Speech")) + .andExpect(jsonPath("$.value[2].DrillState").value("leaf")) + .andExpect(jsonPath("$.value[3]").doesNotExist()); + } + + @Test + @WithMockUser(username = "admin") + void testCollapseAll() throws Exception { + client.perform(get(genresURI + + "?$select=DrillState,ID,name" + + "&$apply=com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID',Levels=1)" + + "&$count=true&$skip=0&$top=238")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.value[0].name").value("Fiction")) + .andExpect(jsonPath("$.value[0].DrillState").value("collapsed")) + .andExpect(jsonPath("$.value[1].name").value("Non-Fiction")) + .andExpect(jsonPath("$.value[1].DrillState").value("collapsed")) + .andExpect(jsonPath("$.value[2]").doesNotExist()); + } + + @Test + @WithMockUser(username = "admin") + void testExpandAll() throws Exception { + client.perform(get(genresURI + + "?$select=DistanceFromRoot,DrillState,ID,LimitedDescendantCount,name" + + "&$apply=com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID')" + + "&$count=true&$skip=0&$top=238")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.value[0].ID").value(10)) + .andExpect(jsonPath("$.value[0].name").value("Fiction")) + .andExpect(jsonPath("$.value[0].DrillState").value("expanded")) + .andExpect(jsonPath("$.value[0].DistanceFromRoot").value(0)) + .andExpect(jsonPath("$.value[0].LimitedDescendantCount").value(9)) + .andExpect(jsonPath("$.value[14].name").value("Speech")) + .andExpect(jsonPath("$.value[14].DrillState").value("leaf")) + .andExpect(jsonPath("$.value[15]").doesNotExist()); + } +} From 39020760e9a4a7e91b1e281563df836c151ee1ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20G=C3=B6rler?= Date: Mon, 2 Dec 2024 14:45:09 +0100 Subject: [PATCH 15/26] update test --- srv/src/test/java/my/bookshop/GenreHierarchyTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/srv/src/test/java/my/bookshop/GenreHierarchyTest.java b/srv/src/test/java/my/bookshop/GenreHierarchyTest.java index c16dd0c6..11589301 100644 --- a/srv/src/test/java/my/bookshop/GenreHierarchyTest.java +++ b/srv/src/test/java/my/bookshop/GenreHierarchyTest.java @@ -14,7 +14,6 @@ @SpringBootTest @AutoConfigureMockMvc -//@ActiveProfiles({"hybrid"}) public class GenreHierarchyTest { @Autowired From 903852e5eb66b7bed7745b757a3c32af13656b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20G=C3=B6rler?= Date: Mon, 16 Dec 2024 12:28:29 +0100 Subject: [PATCH 16/26] parnt -> parent --- db/books.cds | 4 +-- db/data/my.bookshop-Genres.csv | 2 +- pom.xml | 2 +- .../bookshop/handlers/HierarchyHandler.java | 34 +++++++++---------- .../java/my/bookshop/GenreHierarchyTest.java | 13 +++++-- 5 files changed, 32 insertions(+), 23 deletions(-) diff --git a/db/books.cds b/db/books.cds index 67f9fd88..9799d0a6 100644 --- a/db/books.cds +++ b/db/books.cds @@ -49,7 +49,7 @@ entity Genres { key ID : Integer; name : localized String(255); descr : localized String(1000); - parnt : Association to Genres; + parent : Association to Genres; children : Composition of many Genres - on children.parnt = $self; + on children.parent = $self; } diff --git a/db/data/my.bookshop-Genres.csv b/db/data/my.bookshop-Genres.csv index f498be2d..88e73bdd 100644 --- a/db/data/my.bookshop-Genres.csv +++ b/db/data/my.bookshop-Genres.csv @@ -1,4 +1,4 @@ -ID;parnt_ID;name +ID;parent_ID;name 10;;Fiction 11;10;Drama 12;10;Poetry diff --git a/pom.xml b/pom.xml index 69cb4eec..9b32362e 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ 21 - 3.5.0 + 3.6.0-m2450 5.13.0 3.5.3 3.8.4 diff --git a/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java b/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java index 4d826094..ad124963 100644 --- a/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java +++ b/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java @@ -96,10 +96,10 @@ private void setResult(CdsReadEventContext event, List result) { private void addDrillState(List ghs) { List ids = ghs.stream().map(gh -> gh.getId()).toList(); - Set parents = ghs.stream().map(gh -> gh.getParntId()).filter(p -> p != null) + Set parents = ghs.stream().map(gh -> gh.getParentId()).filter(p -> p != null) .collect(Collectors.toSet()); - CqnSelect q = Select.from(GENRE_HIERARCHY).columns(gh -> gh.parnt_ID().as("id")) - .where(gh -> gh.parnt_ID().in(ids)); + CqnSelect q = Select.from(GENRE_HIERARCHY).columns(gh -> gh.parent_ID().as("id")) + .where(gh -> gh.parent_ID().in(ids)); Set nonLeafs = db .run(q) .stream().map(r -> r.get("id")).collect(Collectors.toSet()); @@ -128,7 +128,7 @@ private CqnPredicate descendantsFilter(CqnDescendantsTransformation descendants) CqnPredicate children = CQL.copy(start, new Modifier() { @Override public CqnValue ref(CqnElementRef ref) { - return CQL.get(GenreHierarchy.PARNT_ID); + return CQL.get(GenreHierarchy.PARENT_ID); } }); result = CQL.or(result, children); @@ -147,10 +147,10 @@ private CqnPredicate ancestorsFilter(CqnAncestorsTransformation ancestors) { Select outer = Select.from(GENRE_HIERARCHY) .columns(gh -> gh.ID().as("i0"), - gh -> gh.parnt().ID().as("i1"), - gh -> gh.parnt().parnt().ID().as("i2"), - gh -> gh.parnt().parnt().parnt().ID().as("i3"), - gh -> gh.parnt().parnt().parnt().parnt().ID().as("i4")) + gh -> gh.parent().ID().as("i1"), + gh -> gh.parent().parent().ID().as("i2"), + gh -> gh.parent().parent().parent().ID().as("i3"), + gh -> gh.parent().parent().parent().parent().ID().as("i4")) .where(gh -> gh.ID().in(inner)); Set ancestorIds = new HashSet<>(); @@ -178,7 +178,7 @@ private List handleDescendants(CqnDescendantsTransformation desc private static void connect(List nodes) { Map lookup = new HashMap<>(); nodes.forEach(gh -> lookup.put(gh.getId(), gh)); - nodes.forEach(gh -> gh.setParnt(lookup.get(gh.getParntId()))); + nodes.forEach(gh -> gh.setParent(lookup.get(gh.getParentId()))); nodes.forEach(gh -> gh.setDistanceFromRoot(distanceFromRoot(gh))); } @@ -205,7 +205,7 @@ private List topLevelsLimit(CqnTopLevelsTransformation topLevels Map lookup = new HashMap<>(); Map expandLevels = topLevels.expandLevels(); - CqnSelect getRoots = Select.from(GENRE_HIERARCHY).where(gh -> gh.parnt_ID().isNull().and(filter)); + CqnSelect getRoots = Select.from(GENRE_HIERARCHY).where(gh -> gh.parent_ID().isNull().and(filter)); List roots = db.run(getRoots).listOf(GenreHierarchy.class); roots.forEach(root -> { root.setDistanceFromRoot(0l); @@ -214,14 +214,14 @@ private List topLevelsLimit(CqnTopLevelsTransformation topLevels for (long i = 1; i < limit; i++) { List ps = parents; CqnSelect getChildren = Select.from(GENRE_HIERARCHY) - .where(gh -> gh.parnt_ID().in(ps).and(filter)); + .where(gh -> gh.parent_ID().in(ps).and(filter)); List children = db.run(getChildren).listOf(GenreHierarchy.class); if (children.isEmpty()) { break; } long dfr = i; parents = children.stream().peek(gh -> { - gh.setParnt(lookup.get(gh.getParntId())); + gh.setParent(lookup.get(gh.getParentId())); gh.setDistanceFromRoot(dfr); lookup.put(gh.getId(), gh); }).map(GenreHierarchy::getId).toList(); @@ -232,12 +232,12 @@ private List topLevelsLimit(CqnTopLevelsTransformation topLevels List expandedIds = expandLevels.keySet().stream().map(key -> (Integer) key).toList(); CqnSelect expandedCQN = Select.from(AdminService_.GENRE_HIERARCHY).where(gh -> CQL.and(filter, - CQL.or(gh.ID().in(expandedIds), gh.parnt_ID().in(expandedIds)))); + CQL.or(gh.ID().in(expandedIds), gh.parent_ID().in(expandedIds)))); List expanded = db.run(expandedCQN).listOf(GenreHierarchy.class); expanded.forEach(gh -> { if (!lookup.keySet().contains(gh.getId())) { - gh.setParnt(lookup.get(gh.getParntId())); + gh.setParent(lookup.get(gh.getParentId())); gh.setDistanceFromRoot(distanceFromRoot(gh)); lookup.put(gh.getId(), gh); } @@ -259,9 +259,9 @@ private List topLevelsAll(CqnPredicate filter) { private static long distanceFromRoot(GenreHierarchy gh) { long dfr = 0; - while (gh.getParnt() != null) { + while (gh.getParent() != null) { dfr++; - gh = gh.getParnt(); + gh = gh.getParent(); } return dfr; @@ -295,7 +295,7 @@ Deque getPath(GenreHierarchy gh) { Deque path = new ArrayDeque<>(); do { path.push(gh.getName()); - gh = gh.getParnt(); + gh = gh.getParent(); } while (gh != null); return path; diff --git a/srv/src/test/java/my/bookshop/GenreHierarchyTest.java b/srv/src/test/java/my/bookshop/GenreHierarchyTest.java index 11589301..1778200d 100644 --- a/srv/src/test/java/my/bookshop/GenreHierarchyTest.java +++ b/srv/src/test/java/my/bookshop/GenreHierarchyTest.java @@ -9,8 +9,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.env.Environment; +import org.springframework.core.env.Profiles; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; @SpringBootTest @AutoConfigureMockMvc @@ -18,6 +21,9 @@ public class GenreHierarchyTest { @Autowired private MockMvc client; + + @Autowired + Environment env; private static final String genresURI = "/api/admin/GenreHierarchy"; @@ -94,7 +100,8 @@ void testCollapseAll() throws Exception { @Test @WithMockUser(username = "admin") void testExpandAll() throws Exception { - client.perform(get(genresURI + ResultActions expectactions = + client.perform(get(genresURI + "?$select=DistanceFromRoot,DrillState,ID,LimitedDescendantCount,name" + "&$apply=com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID')" + "&$count=true&$skip=0&$top=238")) @@ -103,9 +110,11 @@ void testExpandAll() throws Exception { .andExpect(jsonPath("$.value[0].name").value("Fiction")) .andExpect(jsonPath("$.value[0].DrillState").value("expanded")) .andExpect(jsonPath("$.value[0].DistanceFromRoot").value(0)) - .andExpect(jsonPath("$.value[0].LimitedDescendantCount").value(9)) .andExpect(jsonPath("$.value[14].name").value("Speech")) .andExpect(jsonPath("$.value[14].DrillState").value("leaf")) .andExpect(jsonPath("$.value[15]").doesNotExist()); + if (env.acceptsProfiles(Profiles.of("hybrid"))) { + expectactions.andExpect(jsonPath("$.value[0].LimitedDescendantCount").value(9)); + } } } From 17354c932d8af07e096b35bb861c5e1ff7409957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20G=C3=B6rler?= Date: Mon, 16 Dec 2024 12:50:07 +0100 Subject: [PATCH 17/26] fix annotation --- app/genres/fiori-service.cds | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/genres/fiori-service.cds b/app/genres/fiori-service.cds index 77b132a5..fd4a556a 100644 --- a/app/genres/fiori-service.cds +++ b/app/genres/fiori-service.cds @@ -8,7 +8,7 @@ using AdminService from '../../srv/admin-service'; annotate AdminService.GenreHierarchy with @Aggregation.RecursiveHierarchy#GenreHierarchy: { $Type: 'Aggregation.RecursiveHierarchyType', NodeProperty: ID, // identifies a node - ParentNavigationProperty: parnt // navigates to a node's parent + ParentNavigationProperty: parent // navigates to a node's parent }; annotate AdminService.GenreHierarchy with @Hierarchy.RecursiveHierarchy#GenreHierarchy: { @@ -20,4 +20,4 @@ annotate AdminService.GenreHierarchy with @Aggregation.RecursiveHierarchy#GenreH Matched: Matched, MatchedDescendantCount: MatchedDescendantCount, LimitedRank: LimitedRank -}; \ No newline at end of file +}; From ee8b025f9685e004f69208fcbde841b78393bb90 Mon Sep 17 00:00:00 2001 From: Olena Date: Tue, 17 Dec 2024 10:44:51 +0100 Subject: [PATCH 18/26] Added Tree Tables to Readme (#414) Co-authored-by: D070615 --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 284d4e90..d59b0bb6 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,9 @@ User Interface related Features: - [Model Localization](https://cap.cloud.sap/docs/guides/i18n) for [English](app/_i18n/i18n.properties) and [German](app/_i18n/i18n_de.properties) language for static texts - [Custom File Upload extension](app/admin/webapp/extension/Upload.js) which provides a button for uploading `CSV` files - A simple Swagger UI for the CatalogService API at -- UI5 Tree Table +- UI5 [Tree Table](app/genres/webapp/manifest.json) with Value Help for [GenreHierarchy](app/admin/fiori-service.cds) +- [Custom event handlers](https://cap.cloud.sap/docs/java/provisioning-api) for Tree Table such as the [Custom business logic for GenreHierarchy](srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java). + Please note, that Tree Tables must be used with HANA. Custom event handler in this case provides a limited support ment for local testing. CDS Maven Plugin Features: From 81bd62e0f3c2a23a9cea959c90b64621edf19573 Mon Sep 17 00:00:00 2001 From: Olena Date: Tue, 17 Dec 2024 14:58:43 +0100 Subject: [PATCH 19/26] Test: added additional tests for GenreHierarchy (#413) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: D070615 Co-authored-by: Evgeny Andreev Co-authored-by: Adrian Görler Co-authored-by: Evgeny Andreev --- .../bookshop/handlers/HierarchyHandler.java | 2 +- .../java/my/bookshop/GenreHierarchyTest.java | 148 ++++++++++++++++-- .../handlers/HierarchyHandlerSorterTest.java | 70 +++++++++ 3 files changed, 209 insertions(+), 11 deletions(-) create mode 100644 srv/src/test/java/my/bookshop/handlers/HierarchyHandlerSorterTest.java diff --git a/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java b/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java index ad124963..40e762d5 100644 --- a/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java +++ b/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java @@ -267,7 +267,7 @@ private static long distanceFromRoot(GenreHierarchy gh) { return dfr; } - private class Sorter implements Comparator { + static class Sorter implements Comparator { @Override public int compare(GenreHierarchy gh1, GenreHierarchy gh2) { diff --git a/srv/src/test/java/my/bookshop/GenreHierarchyTest.java b/srv/src/test/java/my/bookshop/GenreHierarchyTest.java index 1778200d..28f641a4 100644 --- a/srv/src/test/java/my/bookshop/GenreHierarchyTest.java +++ b/srv/src/test/java/my/bookshop/GenreHierarchyTest.java @@ -1,9 +1,11 @@ package my.bookshop; +import static org.assertj.core.api.Assumptions.assumeThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.net.URI; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -14,6 +16,7 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.web.util.UriComponentsBuilder; @SpringBootTest @AutoConfigureMockMvc @@ -24,7 +27,7 @@ public class GenreHierarchyTest { @Autowired Environment env; - + private static final String genresURI = "/api/admin/GenreHierarchy"; @Test @@ -45,7 +48,7 @@ void testCountAll() throws Exception { @WithMockUser(username = "admin") void testStartOneLevel() throws Exception { client.perform(get(genresURI - + "?$select=DrillState,ID,name,DistanceFromRoot" + + "?$select=DrillState,ID,name,DistanceFromRoot" + "&$apply=orderby(name)/" + "com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID',Levels=1)" + "&$count=true")) @@ -59,7 +62,30 @@ void testStartOneLevel() throws Exception { .andExpect(jsonPath("$.value[1].DistanceFromRoot").value(0)) .andExpect(jsonPath("$.value[1].DrillState").value("collapsed")) .andExpect(jsonPath("$.value[2]").doesNotExist()); + } + @Test + @WithMockUser(username = "admin") + void testStartTwoLevels() throws Exception { + client.perform(get(genresURI + + "?$select=DrillState,ID,name,DistanceFromRoot" + + "&$apply=orderby(name)/" + + "com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID',Levels=2)" + + "&$count=true")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.value[0].ID").value(10)) + .andExpect(jsonPath("$.value[0].name").value("Fiction")) + .andExpect(jsonPath("$.value[0].DrillState").value("expanded")) + .andExpect(jsonPath("$.value[0].DistanceFromRoot").value(0)) + .andExpect(jsonPath("$.value[1].ID").value(11)) + .andExpect(jsonPath("$.value[1].name").value("Drama")) + .andExpect(jsonPath("$.value[1].DrillState").value("leaf")) + .andExpect(jsonPath("$.value[1].DistanceFromRoot").value(1)) + .andExpect(jsonPath("$.value[11].ID").value(21)) + .andExpect(jsonPath("$.value[11].name").value("Biography")) + .andExpect(jsonPath("$.value[11].DrillState").value("collapsed")) + .andExpect(jsonPath("$.value[11].DistanceFromRoot").value(1)) + .andExpect(jsonPath("$.value[14]").doesNotExist()); } @Test @@ -70,7 +96,7 @@ void testExpandNonFiction() throws Exception { + "&$apply=descendants($root/GenreHierarchy,GenreHierarchy,ID,filter(ID eq 20),1)" + "/orderby(ID)")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.value[0].ID").value(21)) + .andExpect(jsonPath("$.value[0].ID").value(21)) .andExpect(jsonPath("$.value[0].name").value("Biography")) .andExpect(jsonPath("$.value[0].DrillState").value("collapsed")) .andExpect(jsonPath("$.value[1].ID").value(23)) @@ -100,11 +126,12 @@ void testCollapseAll() throws Exception { @Test @WithMockUser(username = "admin") void testExpandAll() throws Exception { - ResultActions expectactions = - client.perform(get(genresURI + String url = genresURI + "?$select=DistanceFromRoot,DrillState,ID,LimitedDescendantCount,name" + "&$apply=com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID')" - + "&$count=true&$skip=0&$top=238")) + + "&$count=true&$skip=0&$top=238"; + + ResultActions expectations = client.perform(get(url)) .andExpect(status().isOk()) .andExpect(jsonPath("$.value[0].ID").value(10)) .andExpect(jsonPath("$.value[0].name").value("Fiction")) @@ -113,8 +140,109 @@ void testExpandAll() throws Exception { .andExpect(jsonPath("$.value[14].name").value("Speech")) .andExpect(jsonPath("$.value[14].DrillState").value("leaf")) .andExpect(jsonPath("$.value[15]").doesNotExist()); - if (env.acceptsProfiles(Profiles.of("hybrid"))) { - expectactions.andExpect(jsonPath("$.value[0].LimitedDescendantCount").value(9)); - } + if (isOnHana()) { + expectations.andExpect(jsonPath("$.value[0].LimitedDescendantCount").value(9)); + } + } + + @Test + @WithMockUser(username = "admin") + void testSearch() throws Exception { + ResultActions expectations = client.perform(get(genresURI + + "?$select=DistanceFromRoot,DrillState,ID,LimitedDescendantCount,name" + + "&$apply=ancestors($root/GenreHierarchy,GenreHierarchy,ID,search(\"ry\"),keep start)" + + "/orderby(name)" + + "/com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID')" + + "&$count=true")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.value[0].ID").value(10)) + .andExpect(jsonPath("$.value[0].name").value("Fiction")) + .andExpect(jsonPath("$.value[0].DrillState").value("expanded")) + .andExpect(jsonPath("$.value[0].DistanceFromRoot").value(0)) + .andExpect(jsonPath("$.value[1].ID").value(19)) + .andExpect(jsonPath("$.value[1].name").value("Fairy Tale")) + .andExpect(jsonPath("$.value[1].DrillState").value("leaf")) + .andExpect(jsonPath("$.value[1].DistanceFromRoot").value(1)) + .andExpect(jsonPath("$.value[2].ID").value(16)) + .andExpect(jsonPath("$.value[2].name").value("Mystery")) + .andExpect(jsonPath("$.value[2].DrillState").value("leaf")) + .andExpect(jsonPath("$.value[2].DistanceFromRoot").value(1)) + .andExpect(jsonPath("$.value[3].ID").value(12)) + .andExpect(jsonPath("$.value[3].name").value("Poetry")) + .andExpect(jsonPath("$.value[3].DrillState").value("leaf")) + .andExpect(jsonPath("$.value[3].DistanceFromRoot").value(1)) + .andExpect(jsonPath("$.value[4]").doesNotExist()); + if (isOnHana()) { + expectations.andExpect(jsonPath("$.value[0].LimitedDescendantCount").value(3)) + .andExpect(jsonPath("$.value[1].LimitedDescendantCount").value(0)) + .andExpect(jsonPath("$.value[2].LimitedDescendantCount").value(0)) + .andExpect(jsonPath("$.value[3].LimitedDescendantCount").value(0)); + } + } + + @Test + @WithMockUser(username = "admin") + void testFilterNotExpanded() throws Exception { + client.perform(get(genresURI + + "?$select=DrillState,ID,name,DistanceFromRoot" + + "&$apply=ancestors($root/GenreHierarchy,GenreHierarchy,ID,filter(name eq 'Autobiography'),keep start)/orderby(name)" + + "/com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID',Levels=1)")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.value[0].ID").value(20)) + .andExpect(jsonPath("$.value[0].name").value("Non-Fiction")) + .andExpect(jsonPath("$.value[0].DrillState").value("collapsed")) + .andExpect(jsonPath("$.value[0].DistanceFromRoot").value(0)) + .andExpect(jsonPath("$.value[1]").doesNotExist()); + } + + @Ignore + @Test + @WithMockUser(username = "admin") + void testFilterExpandLevels() throws Exception { + String expandLevelsJson = """ + [{"NodeID":10,"Levels":1},{"NodeID":20,"Levels":1}]\ + """; + String unencoded = genresURI + "?$select=DistanceFromRoot,DrillState,ID,LimitedDescendantCount,name" + + "&$apply=ancestors($root/GenreHierarchy,GenreHierarchy,ID,filter(name eq 'Autobiography'),keep start)/orderby(name)" + + "/com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID',Levels=1,ExpandLevels=" + + expandLevelsJson + ")&$count=true"; + String uriString = UriComponentsBuilder.fromUriString(unencoded).toUriString(); + URI uri = URI.create(uriString); + client.perform(get(uri)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.value[0].ID").value(20)) + .andExpect(jsonPath("$.value[0].name").value("Non-Fiction")) + .andExpect(jsonPath("$.value[0].DrillState").value("expanded")) + .andExpect(jsonPath("$.value[0].DistanceFromRoot").value(0)) + .andExpect(jsonPath("$.value[2]").doesNotExist()); + } + + @Test + @WithMockUser(username = "admin") + void testStartTwoLevelsOrderByDescHANA() throws Exception { + assumeThat(env.getActiveProfiles()).contains("hybrid"); + client.perform(get(genresURI + + "?$select=DrillState,ID,name,DistanceFromRoot" + + "&$apply=orderby(name desc)/" + + "com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID',Levels=2)" + + "&$count=true")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.value[0].ID").value(20)) + .andExpect(jsonPath("$.value[0].name").value("Non-Fiction")) + .andExpect(jsonPath("$.value[0].DrillState").value("expanded")) + .andExpect(jsonPath("$.value[0].DistanceFromRoot").value(0)) + .andExpect(jsonPath("$.value[1].ID").value(24)) + .andExpect(jsonPath("$.value[1].name").value("Speech")) + .andExpect(jsonPath("$.value[1].DrillState").value("leaf")) + .andExpect(jsonPath("$.value[1].DistanceFromRoot").value(1)) + .andExpect(jsonPath("$.value[3].ID").value(21)) + .andExpect(jsonPath("$.value[3].name").value("Biography")) + .andExpect(jsonPath("$.value[3].DrillState").value("collapsed")) + .andExpect(jsonPath("$.value[3].DistanceFromRoot").value(1)) + .andExpect(jsonPath("$.value[14]").doesNotExist()); + } + + private boolean isOnHana() { + return env.acceptsProfiles(Profiles.of("hybrid")); } -} +} \ No newline at end of file diff --git a/srv/src/test/java/my/bookshop/handlers/HierarchyHandlerSorterTest.java b/srv/src/test/java/my/bookshop/handlers/HierarchyHandlerSorterTest.java new file mode 100644 index 00000000..46bc78c2 --- /dev/null +++ b/srv/src/test/java/my/bookshop/handlers/HierarchyHandlerSorterTest.java @@ -0,0 +1,70 @@ +package my.bookshop.handlers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.List; +import org.junit.jupiter.api.Test; + +import cds.gen.adminservice.GenreHierarchy; +import my.bookshop.handlers.HierarchyHandler.Sorter; + +public class HierarchyHandlerSorterTest { + + @Test + public void testSortRoots() { + GenreHierarchy r1 = genre("Philosophical fiction"); + GenreHierarchy r2 = genre("Epic"); + List sorted = sorted(r1, r2); + + assertEquals("Epic", sorted.get(0).getName()); + assertEquals("Philosophical fiction", sorted.get(1).getName()); + } + + @Test + public void testSortSiblings() { + GenreHierarchy root = genre("Folklore"); + GenreHierarchy g1 = genre("Urban legend", root); + GenreHierarchy g2 = genre("Fairy tale", root); + List sorted = sorted(g1, g2); + + assertEquals("Fairy tale", sorted.get(0).getName()); + assertEquals("Urban legend", sorted.get(1).getName()); + } + + @Test + public void testSortChildrenWithDifRoot() { + GenreHierarchy r1 = genre("Thriller"); + GenreHierarchy r2 = genre("Folklore"); + GenreHierarchy g2 = genre("Urban legend", r2); + List sorted = sorted(r1, g2); + + assertEquals("Urban legend", sorted.get(0).getName()); + assertEquals("Thriller", sorted.get(1).getName()); + } + + @Test + public void testSortChildrenSameRoot() { + GenreHierarchy r1 = genre("Folklore"); + GenreHierarchy g1 = genre("Urban legend", r1); + List sorted = sorted(g1, r1); + + assertEquals("Folklore", sorted.get(0).getName()); + assertEquals("Urban legend", sorted.get(1).getName()); + } + + private static GenreHierarchy genre(String name, GenreHierarchy parent) { + GenreHierarchy genre = GenreHierarchy.create(); + genre.setName(name); + if (parent != null) { + genre.setParent(parent); + } + return genre; + } + + private static GenreHierarchy genre(String name) { + return genre(name, null); + } + + private static List sorted(GenreHierarchy... h) { + return List.of(h).stream().sorted(new Sorter()).toList(); + } +} From a9db3675e85b7bffa587a59a3b2e20c710217571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20G=C3=B6rler?= Date: Tue, 17 Dec 2024 15:02:16 +0100 Subject: [PATCH 20/26] fix import --- srv/src/test/java/my/bookshop/GenreHierarchyTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/srv/src/test/java/my/bookshop/GenreHierarchyTest.java b/srv/src/test/java/my/bookshop/GenreHierarchyTest.java index 28f641a4..e4eb9f74 100644 --- a/srv/src/test/java/my/bookshop/GenreHierarchyTest.java +++ b/srv/src/test/java/my/bookshop/GenreHierarchyTest.java @@ -7,6 +7,7 @@ import java.net.URI; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -195,7 +196,7 @@ void testFilterNotExpanded() throws Exception { .andExpect(jsonPath("$.value[1]").doesNotExist()); } - @Ignore + @Disabled @Test @WithMockUser(username = "admin") void testFilterExpandLevels() throws Exception { From 333d2dace8ffcf14217dce6bf5e76018e2f13219 Mon Sep 17 00:00:00 2001 From: Evgeny Andreev Date: Tue, 17 Dec 2024 17:20:47 +0100 Subject: [PATCH 21/26] more genres (#412) --- db/data/my.bookshop-Books.csv | 10 +-- db/data/my.bookshop-Genres.csv | 45 ++++++---- .../java/my/bookshop/GenreHierarchyTest.java | 84 +++++++++---------- 3 files changed, 73 insertions(+), 66 deletions(-) diff --git a/db/data/my.bookshop-Books.csv b/db/data/my.bookshop-Books.csv index dd4baa1a..e5453379 100644 --- a/db/data/my.bookshop-Books.csv +++ b/db/data/my.bookshop-Books.csv @@ -1,6 +1,6 @@ ID;TITLE;DESCR;AUTHOR_ID;STOCK;PRICE;CURRENCY_CODE;GENRE_ID;RATING;ISBN -f846b0b9-01d4-4f6d-82a4-d79204f62278;Wuthering Heights;"Wuthering Heights, Emily Brontë's only novel, was published in 1847 under the pseudonym ""Ellis Bell"". It was written between October 1845 and June 1846. Wuthering Heights and Anne Brontë's Agnes Grey were accepted by publisher Thomas Newby before the success of their sister Charlotte's novel Jane Eyre. After Emily's death, Charlotte edited the manuscript of Wuthering Heights and arranged for the edited version to be published as a posthumous second edition in 1850.";335c7bcd-b826-4f14-a788-e0bf6738617a;12;11.11;GBP;11;4.5;979-8698267973 -9b084139-0b1e-43b6-b12a-7b3669d75f02;Jane Eyre;"Jane Eyre /ɛər/ (originally published as Jane Eyre: An Autobiography) is a novel by English writer Charlotte Brontë, published under the pen name ""Currer Bell"", on 16 October 1847, by Smith, Elder & Co. of London. The first American edition was published the following year by Harper & Brothers of New York. Primarily a bildungsroman, Jane Eyre follows the experiences of its eponymous heroine, including her growth to adulthood and her love for Mr. Rochester, the brooding master of Thornfield Hall. The novel revolutionised prose fiction in that the focus on Jane's moral and spiritual development is told through an intimate, first-person narrative, where actions and events are coloured by a psychological intensity. The book contains elements of social criticism, with a strong sense of Christian morality at its core and is considered by many to be ahead of its time because of Jane's individualistic character and how the novel approaches the topics of class, sexuality, religion and feminism.";e3da2c2e-72ee-45d5-8def-52964c7b252a;11;12.34;GBP;11;3.0;979-8598716472 -51061ce3-ddde-4d70-a2dc-6314afbcc73e;The Raven;"“The Raven"" is a narrative poem by American writer Edgar Allan Poe. First published in January 1845, the poem is often noted for its musicality, stylized language, and supernatural atmosphere. It tells of a talking raven's mysterious visit to a distraught lover, tracing the man's slow fall into madness. The lover, often identified as being a student, is lamenting the loss of his love, Lenore. Sitting on a bust of Pallas, the raven seems to further distress the protagonist with its constant repetition of the word ""Nevermore"". The poem makes use of folk, mythological, religious, and classical references.";e7643aae-2d2f-4656-bb2d-1328ad3c8045;333;13.13;USD;16;2.5;978-1092909747 -aebdfc8a-0dfa-4468-bd36-48aabd65e663;Eleonora;"""Eleonora"" is a short story by Edgar Allan Poe, first published in 1842 in Philadelphia in the literary annual The Gift. It is often regarded as somewhat autobiographical and has a relatively ""happy"" ending.";e7643aae-2d2f-4656-bb2d-1328ad3c8045;555;14;USD;16;1.0;979-8669820985 -4a519e61-3c3a-4bd9-ab12-d7e0c5329933;Catweazle;Catweazle is a British fantasy television series, starring Geoffrey Bayldon in the title role, and created by Richard Carpenter for London Weekend Television. The first series, produced and directed by Quentin Lawrence, was screened in the UK on ITV in 1970. The second series, directed by David Reid and David Lane, was shown in 1971. Each series had thirteen episodes, most but not all written by Carpenter, who also published two books based on the scripts.;3c081d9d-abda-4da9-8b6a-4f4555bb26bc;22;15;EUR;13;4.0;978-3473523023 +f846b0b9-01d4-4f6d-82a4-d79204f62278;Wuthering Heights;"Wuthering Heights, Emily Brontë's only novel, was published in 1847 under the pseudonym ""Ellis Bell"". It was written between October 1845 and June 1846. Wuthering Heights and Anne Brontë's Agnes Grey were accepted by publisher Thomas Newby before the success of their sister Charlotte's novel Jane Eyre. After Emily's death, Charlotte edited the manuscript of Wuthering Heights and arranged for the edited version to be published as a posthumous second edition in 1850.";335c7bcd-b826-4f14-a788-e0bf6738617a;12;11.11;GBP;103;4.5;979-8698267973 +9b084139-0b1e-43b6-b12a-7b3669d75f02;Jane Eyre;"Jane Eyre /ɛər/ (originally published as Jane Eyre: An Autobiography) is a novel by English writer Charlotte Brontë, published under the pen name ""Currer Bell"", on 16 October 1847, by Smith, Elder & Co. of London. The first American edition was published the following year by Harper & Brothers of New York. Primarily a bildungsroman, Jane Eyre follows the experiences of its eponymous heroine, including her growth to adulthood and her love for Mr. Rochester, the brooding master of Thornfield Hall. The novel revolutionised prose fiction in that the focus on Jane's moral and spiritual development is told through an intimate, first-person narrative, where actions and events are coloured by a psychological intensity. The book contains elements of social criticism, with a strong sense of Christian morality at its core and is considered by many to be ahead of its time because of Jane's individualistic character and how the novel approaches the topics of class, sexuality, religion and feminism.";e3da2c2e-72ee-45d5-8def-52964c7b252a;11;12.34;GBP;103;3.0;979-8598716472 +51061ce3-ddde-4d70-a2dc-6314afbcc73e;The Raven;"“The Raven"" is a narrative poem by American writer Edgar Allan Poe. First published in January 1845, the poem is often noted for its musicality, stylized language, and supernatural atmosphere. It tells of a talking raven's mysterious visit to a distraught lover, tracing the man's slow fall into madness. The lover, often identified as being a student, is lamenting the loss of his love, Lenore. Sitting on a bust of Pallas, the raven seems to further distress the protagonist with its constant repetition of the word ""Nevermore"". The poem makes use of folk, mythological, religious, and classical references.";e7643aae-2d2f-4656-bb2d-1328ad3c8045;333;13.13;USD;117;2.5;978-1092909747 +aebdfc8a-0dfa-4468-bd36-48aabd65e663;Eleonora;"""Eleonora"" is a short story by Edgar Allan Poe, first published in 1842 in Philadelphia in the literary annual The Gift. It is often regarded as somewhat autobiographical and has a relatively ""happy"" ending.";e7643aae-2d2f-4656-bb2d-1328ad3c8045;555;14;USD;117;1.0;979-8669820985 +4a519e61-3c3a-4bd9-ab12-d7e0c5329933;Catweazle;Catweazle is a British fantasy television series, starring Geoffrey Bayldon in the title role, and created by Richard Carpenter for London Weekend Television. The first series, produced and directed by Quentin Lawrence, was screened in the UK on ITV in 1970. The second series, directed by David Reid and David Lane, was shown in 1971. Each series had thirteen episodes, most but not all written by Carpenter, who also published two books based on the scripts.;3c081d9d-abda-4da9-8b6a-4f4555bb26bc;22;15;EUR;110;4.0;978-3473523023 diff --git a/db/data/my.bookshop-Genres.csv b/db/data/my.bookshop-Genres.csv index 88e73bdd..84bb85b0 100644 --- a/db/data/my.bookshop-Genres.csv +++ b/db/data/my.bookshop-Genres.csv @@ -1,16 +1,31 @@ ID;parent_ID;name -10;;Fiction -11;10;Drama -12;10;Poetry -13;10;Fantasy -14;10;Science Fiction -15;10;Romance -16;10;Mystery -17;10;Thriller -18;10;Dystopia -19;10;Fairy Tale -20;;Non-Fiction -21;20;Biography -22;21;Autobiography -23;20;Essay -24;20;Speech \ No newline at end of file +100;;Fiction +101;100;Action +102;100;Adventure +103;100;Drama +105;100;Poetry +106;100;Science Fiction +107;106;Utopian and Dystopian +108;107;Dystopia +109;108;Cyberpunk +110;109;Steampunk +104;100;Fantasy +111;104;Epic fantasy +112;104;High fantasy +113;100;Graphic Novel +115;100;Short Story +116;100;Romance +117;100;Mystery +123;117;Thriller +124;117;Suspense +118;100;Horror +119;100;Historical Fiction +120;100;Contemporary Fiction +121;100;Magical Realism +122;100;Literary Fiction +128;100;Fairy Tale +200;;Non-Fiction +201;200;Biography +202;201;Autobiography +203;200;Essay +204;200;Speech diff --git a/srv/src/test/java/my/bookshop/GenreHierarchyTest.java b/srv/src/test/java/my/bookshop/GenreHierarchyTest.java index e4eb9f74..b66d036c 100644 --- a/srv/src/test/java/my/bookshop/GenreHierarchyTest.java +++ b/srv/src/test/java/my/bookshop/GenreHierarchyTest.java @@ -42,7 +42,7 @@ void testGetAll() throws Exception { void testCountAll() throws Exception { client.perform(get(genresURI + "/$count")) .andExpect(status().isOk()) - .andExpect(jsonPath("$").value(15)); + .andExpect(jsonPath("$").value(30)); } @Test @@ -54,11 +54,11 @@ void testStartOneLevel() throws Exception { + "com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID',Levels=1)" + "&$count=true")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.value[0].ID").value(10)) + .andExpect(jsonPath("$.value[0].ID").value(100)) .andExpect(jsonPath("$.value[0].name").value("Fiction")) .andExpect(jsonPath("$.value[0].DistanceFromRoot").value(0)) .andExpect(jsonPath("$.value[0].DrillState").value("collapsed")) - .andExpect(jsonPath("$.value[1].ID").value(20)) + .andExpect(jsonPath("$.value[1].ID").value(200)) .andExpect(jsonPath("$.value[1].name").value("Non-Fiction")) .andExpect(jsonPath("$.value[1].DistanceFromRoot").value(0)) .andExpect(jsonPath("$.value[1].DrillState").value("collapsed")) @@ -74,19 +74,19 @@ void testStartTwoLevels() throws Exception { + "com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID',Levels=2)" + "&$count=true")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.value[0].ID").value(10)) + .andExpect(jsonPath("$.value[0].ID").value(100)) .andExpect(jsonPath("$.value[0].name").value("Fiction")) .andExpect(jsonPath("$.value[0].DrillState").value("expanded")) .andExpect(jsonPath("$.value[0].DistanceFromRoot").value(0)) - .andExpect(jsonPath("$.value[1].ID").value(11)) - .andExpect(jsonPath("$.value[1].name").value("Drama")) + .andExpect(jsonPath("$.value[1].ID").value(101)) + .andExpect(jsonPath("$.value[1].name").value("Action")) .andExpect(jsonPath("$.value[1].DrillState").value("leaf")) .andExpect(jsonPath("$.value[1].DistanceFromRoot").value(1)) - .andExpect(jsonPath("$.value[11].ID").value(21)) - .andExpect(jsonPath("$.value[11].name").value("Biography")) - .andExpect(jsonPath("$.value[11].DrillState").value("collapsed")) - .andExpect(jsonPath("$.value[11].DistanceFromRoot").value(1)) - .andExpect(jsonPath("$.value[14]").doesNotExist()); + .andExpect(jsonPath("$.value[20].ID").value(204)) + .andExpect(jsonPath("$.value[20].name").value("Speech")) + .andExpect(jsonPath("$.value[20].DrillState").value("leaf")) + .andExpect(jsonPath("$.value[20].DistanceFromRoot").value(1)) + .andExpect(jsonPath("$.value[21]").doesNotExist()); } @Test @@ -94,16 +94,16 @@ void testStartTwoLevels() throws Exception { void testExpandNonFiction() throws Exception { client.perform(get(genresURI + "?$select=DrillState,ID,name" - + "&$apply=descendants($root/GenreHierarchy,GenreHierarchy,ID,filter(ID eq 20),1)" + + "&$apply=descendants($root/GenreHierarchy,GenreHierarchy,ID,filter(ID eq 200),1)" + "/orderby(ID)")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.value[0].ID").value(21)) + .andExpect(jsonPath("$.value[0].ID").value(201)) .andExpect(jsonPath("$.value[0].name").value("Biography")) .andExpect(jsonPath("$.value[0].DrillState").value("collapsed")) - .andExpect(jsonPath("$.value[1].ID").value(23)) + .andExpect(jsonPath("$.value[1].ID").value(203)) .andExpect(jsonPath("$.value[1].name").value("Essay")) .andExpect(jsonPath("$.value[1].DrillState").value("leaf")) - .andExpect(jsonPath("$.value[2].ID").value(24)) + .andExpect(jsonPath("$.value[2].ID").value(204)) .andExpect(jsonPath("$.value[2].name").value("Speech")) .andExpect(jsonPath("$.value[2].DrillState").value("leaf")) .andExpect(jsonPath("$.value[3]").doesNotExist()); @@ -134,15 +134,15 @@ void testExpandAll() throws Exception { ResultActions expectations = client.perform(get(url)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.value[0].ID").value(10)) + .andExpect(jsonPath("$.value[0].ID").value(100)) .andExpect(jsonPath("$.value[0].name").value("Fiction")) .andExpect(jsonPath("$.value[0].DrillState").value("expanded")) .andExpect(jsonPath("$.value[0].DistanceFromRoot").value(0)) - .andExpect(jsonPath("$.value[14].name").value("Speech")) - .andExpect(jsonPath("$.value[14].DrillState").value("leaf")) - .andExpect(jsonPath("$.value[15]").doesNotExist()); + .andExpect(jsonPath("$.value[29].name").value("Speech")) + .andExpect(jsonPath("$.value[29].DrillState").value("leaf")) + .andExpect(jsonPath("$.value[30]").doesNotExist()); if (isOnHana()) { - expectations.andExpect(jsonPath("$.value[0].LimitedDescendantCount").value(9)); + expectations.andExpect(jsonPath("$.value[0].LimitedDescendantCount").value(24)); } } @@ -156,28 +156,29 @@ void testSearch() throws Exception { + "/com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID')" + "&$count=true")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.value[0].ID").value(10)) + .andExpect(jsonPath("$.value[0].ID").value(100)) .andExpect(jsonPath("$.value[0].name").value("Fiction")) .andExpect(jsonPath("$.value[0].DrillState").value("expanded")) .andExpect(jsonPath("$.value[0].DistanceFromRoot").value(0)) - .andExpect(jsonPath("$.value[1].ID").value(19)) - .andExpect(jsonPath("$.value[1].name").value("Fairy Tale")) + .andExpect(jsonPath("$.value[1].ID").value(120)) + .andExpect(jsonPath("$.value[1].name").value("Contemporary Fiction")) .andExpect(jsonPath("$.value[1].DrillState").value("leaf")) .andExpect(jsonPath("$.value[1].DistanceFromRoot").value(1)) - .andExpect(jsonPath("$.value[2].ID").value(16)) - .andExpect(jsonPath("$.value[2].name").value("Mystery")) + .andExpect(jsonPath("$.value[2].ID").value(128)) + .andExpect(jsonPath("$.value[2].name").value("Fairy Tale")) .andExpect(jsonPath("$.value[2].DrillState").value("leaf")) .andExpect(jsonPath("$.value[2].DistanceFromRoot").value(1)) - .andExpect(jsonPath("$.value[3].ID").value(12)) - .andExpect(jsonPath("$.value[3].name").value("Poetry")) - .andExpect(jsonPath("$.value[3].DrillState").value("leaf")) - .andExpect(jsonPath("$.value[3].DistanceFromRoot").value(1)) - .andExpect(jsonPath("$.value[4]").doesNotExist()); + .andExpect(jsonPath("$.value[3].ID").value(122)) + .andExpect(jsonPath("$.value[6].name").value("Short Story")) + .andExpect(jsonPath("$.value[6].DrillState").value("leaf")) + .andExpect(jsonPath("$.value[6].DistanceFromRoot").value(1)) + .andExpect(jsonPath("$.value[7]").doesNotExist()) + ; if (isOnHana()) { - expectations.andExpect(jsonPath("$.value[0].LimitedDescendantCount").value(3)) + expectations.andExpect(jsonPath("$.value[0].LimitedDescendantCount").value(6)) .andExpect(jsonPath("$.value[1].LimitedDescendantCount").value(0)) .andExpect(jsonPath("$.value[2].LimitedDescendantCount").value(0)) - .andExpect(jsonPath("$.value[3].LimitedDescendantCount").value(0)); + .andExpect(jsonPath("$.value[6].LimitedDescendantCount").value(0)); } } @@ -189,7 +190,7 @@ void testFilterNotExpanded() throws Exception { + "&$apply=ancestors($root/GenreHierarchy,GenreHierarchy,ID,filter(name eq 'Autobiography'),keep start)/orderby(name)" + "/com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID',Levels=1)")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.value[0].ID").value(20)) + .andExpect(jsonPath("$.value[0].ID").value(200)) .andExpect(jsonPath("$.value[0].name").value("Non-Fiction")) .andExpect(jsonPath("$.value[0].DrillState").value("collapsed")) .andExpect(jsonPath("$.value[0].DistanceFromRoot").value(0)) @@ -228,19 +229,10 @@ void testStartTwoLevelsOrderByDescHANA() throws Exception { + "com.sap.vocabularies.Hierarchy.v1.TopLevels(HierarchyNodes=$root/GenreHierarchy,HierarchyQualifier='GenreHierarchy',NodeProperty='ID',Levels=2)" + "&$count=true")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.value[0].ID").value(20)) - .andExpect(jsonPath("$.value[0].name").value("Non-Fiction")) - .andExpect(jsonPath("$.value[0].DrillState").value("expanded")) - .andExpect(jsonPath("$.value[0].DistanceFromRoot").value(0)) - .andExpect(jsonPath("$.value[1].ID").value(24)) - .andExpect(jsonPath("$.value[1].name").value("Speech")) - .andExpect(jsonPath("$.value[1].DrillState").value("leaf")) - .andExpect(jsonPath("$.value[1].DistanceFromRoot").value(1)) - .andExpect(jsonPath("$.value[3].ID").value(21)) - .andExpect(jsonPath("$.value[3].name").value("Biography")) - .andExpect(jsonPath("$.value[3].DrillState").value("collapsed")) - .andExpect(jsonPath("$.value[3].DistanceFromRoot").value(1)) - .andExpect(jsonPath("$.value[14]").doesNotExist()); + .andExpect(jsonPath("$.value[0].ID").value(200)) + .andExpect(jsonPath("$.value[1].ID").value(204)) + .andExpect(jsonPath("$.value[20].ID").value(101)) + .andExpect(jsonPath("$.value[21]").doesNotExist()); } private boolean isOnHana() { From b7953a0c2b246c16e7cb686c3cb014552bd302d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20G=C3=B6rler?= Date: Wed, 18 Dec 2024 00:27:43 +0100 Subject: [PATCH 22/26] update to 3.6.0-m2451 --- pom.xml | 2 +- srv/src/test/java/my/bookshop/GenreHierarchyTest.java | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 53dbefe6..bfc3aa3c 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ 21 - 3.6.0-m2450 + 3.6.0-m2451 5.13.0 3.5.3 3.8.4 diff --git a/srv/src/test/java/my/bookshop/GenreHierarchyTest.java b/srv/src/test/java/my/bookshop/GenreHierarchyTest.java index b66d036c..e519a2b4 100644 --- a/srv/src/test/java/my/bookshop/GenreHierarchyTest.java +++ b/srv/src/test/java/my/bookshop/GenreHierarchyTest.java @@ -7,7 +7,6 @@ import java.net.URI; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -197,12 +196,11 @@ void testFilterNotExpanded() throws Exception { .andExpect(jsonPath("$.value[1]").doesNotExist()); } - @Disabled @Test @WithMockUser(username = "admin") void testFilterExpandLevels() throws Exception { String expandLevelsJson = """ - [{"NodeID":10,"Levels":1},{"NodeID":20,"Levels":1}]\ + [{"NodeID":100,"Levels":1},{"NodeID":200,"Levels":1}]\ """; String unencoded = genresURI + "?$select=DistanceFromRoot,DrillState,ID,LimitedDescendantCount,name" + "&$apply=ancestors($root/GenreHierarchy,GenreHierarchy,ID,filter(name eq 'Autobiography'),keep start)/orderby(name)" @@ -212,7 +210,7 @@ void testFilterExpandLevels() throws Exception { URI uri = URI.create(uriString); client.perform(get(uri)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.value[0].ID").value(20)) + .andExpect(jsonPath("$.value[0].ID").value(200)) .andExpect(jsonPath("$.value[0].name").value("Non-Fiction")) .andExpect(jsonPath("$.value[0].DrillState").value("expanded")) .andExpect(jsonPath("$.value[0].DistanceFromRoot").value(0)) From 1c85e8a21472c94a5332a5371c0ee84135f60dec Mon Sep 17 00:00:00 2001 From: Olena Date: Wed, 18 Dec 2024 09:57:13 +0100 Subject: [PATCH 23/26] remove DEBUG --- srv/src/main/resources/application.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/srv/src/main/resources/application.yaml b/srv/src/main/resources/application.yaml index 4c11a7a6..6766721d 100644 --- a/srv/src/main/resources/application.yaml +++ b/srv/src/main/resources/application.yaml @@ -1,8 +1,7 @@ --- logging: level: - com.sap.cds.auditlog: DEBUG - com.sap.cds.persistence.sql: DEBUG + '[com.sap.cds.auditlog]': DEBUG spring: jmx: enabled: true From 8fd4aadaa03deb79471a26aedd4d403b3a6c5024 Mon Sep 17 00:00:00 2001 From: Evgeny Andreev Date: Wed, 18 Dec 2024 13:34:49 +0100 Subject: [PATCH 24/26] 3.6.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bfc3aa3c..27fd49ed 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ 21 - 3.6.0-m2451 + 3.6.0 5.13.0 3.5.3 3.8.4 From 0e451549a1a729a9a7d8461eef955ed1419d70f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20G=C3=B6rler?= Date: Thu, 19 Dec 2024 10:14:37 +0100 Subject: [PATCH 25/26] formatting --- db/books.cds | 24 +++++++++++++----------- db/reviews.cds | 10 +++++----- srv/admin-service.cds | 2 +- srv/review-service.cds | 26 ++++++++++++++------------ 4 files changed, 33 insertions(+), 29 deletions(-) diff --git a/db/books.cds b/db/books.cds index 9799d0a6..1fd06ae9 100644 --- a/db/books.cds +++ b/db/books.cds @@ -24,7 +24,7 @@ entity Books : cuid, managed { } entity Authors : cuid, managed { - @assert.format : '^\p{Lu}.*' // assert that name starts with a capital letter + @assert.format: '^\p{Lu}.*' // assert that name starts with a capital letter name : String(111); dateOfBirth : Date; dateOfDeath : Date; @@ -36,20 +36,22 @@ entity Authors : cuid, managed { // annotations for Data Privacy annotate Authors with -@PersonalData : { DataSubjectRole : 'Author', EntitySemantics : 'DataSubject' } -{ - ID @PersonalData.FieldSemantics : 'DataSubjectID'; - name @PersonalData.IsPotentiallySensitive; +@PersonalData: { + DataSubjectRole: 'Author', + EntitySemantics: 'DataSubject' +} { + ID @PersonalData.FieldSemantics: 'DataSubjectID'; + name @PersonalData.IsPotentiallySensitive; } /** * Hierarchically organized Code List for Genres */ entity Genres { - key ID : Integer; - name : localized String(255); - descr : localized String(1000); - parent : Association to Genres; - children : Composition of many Genres - on children.parent = $self; + key ID : Integer; + name : localized String(255); + descr : localized String(1000); + parent : Association to Genres; + children : Composition of many Genres + on children.parent = $self; } diff --git a/db/reviews.cds b/db/reviews.cds index 7015fbf6..318844f7 100644 --- a/db/reviews.cds +++ b/db/reviews.cds @@ -9,17 +9,17 @@ using my.bookshop.Books from './books'; entity Reviews : cuid, managed { @cds.odata.ValueList - book : Association to Books; - rating : Rating; - title : String(111); - text : String(1111); + book : Association to Books; + rating : Rating; + title : String(111); + text : String(1111); } // input validation annotate Reviews with { title @mandatory; rating @assert.range; - book @mandatory @assert.target; + book @mandatory @assert.target; } type Rating : Integer enum { diff --git a/srv/admin-service.cds b/srv/admin-service.cds index 139fd52f..4d3b93f3 100644 --- a/srv/admin-service.cds +++ b/srv/admin-service.cds @@ -2,7 +2,7 @@ using {sap.common.Languages as CommonLanguages} from '@sap/cds/common'; using {my.bookshop as my} from '../db/index'; using {sap.changelog as changelog} from 'com.sap.cds/change-tracking'; using {my.common.Hierarchy as Hierarchy} from './hierarchy'; -using {sap.attachments.Attachments} from`com.sap.cds/cds-feature-attachments`; +using {sap.attachments.Attachments} from 'com.sap.cds/cds-feature-attachments'; extend my.Orders with changelog.changeTracked; diff --git a/srv/review-service.cds b/srv/review-service.cds index 599a2d79..0afa2e92 100644 --- a/srv/review-service.cds +++ b/srv/review-service.cds @@ -1,31 +1,33 @@ using {my.bookshop as my} from '../db/index'; -@path : 'review' +@path: 'review' service ReviewService { entity Reviews as projection on my.Reviews; @readonly - entity Books as projection on my.Books excluding { - createdBy, - modifiedBy - } + entity Books as + projection on my.Books + excluding { + createdBy, + modifiedBy + } @readonly entity Authors as projection on my.Authors; @readonly - entity Genres as projection on my.Genres; + entity Genres as projection on my.Genres; // access control restrictions - annotate Reviews with @restrict : [ + annotate Reviews with @restrict: [ { - grant : '*', - to : 'authenticated-user', - where : (createdBy=$user) + grant: '*', + to: 'authenticated-user', + where: (createdBy = $user) }, { - grant : '*', - to : 'admin', + grant: '*', + to: 'admin', } ]; } From c127115985f11757978b15328d0063b7ca07371c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20G=C3=B6rler?= Date: Thu, 19 Dec 2024 10:36:40 +0100 Subject: [PATCH 26/26] add disclaimer --- .../bookshop/handlers/HierarchyHandler.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java b/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java index 40e762d5..bdd875a0 100644 --- a/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java +++ b/srv/src/main/java/my/bookshop/handlers/HierarchyHandler.java @@ -42,8 +42,18 @@ import static cds.gen.adminservice.AdminService_.GENRE_HIERARCHY; @Component -@Profile("default") +@Profile("default") // non-HANA @ServiceName(AdminService_.CDS_NAME) +/** + * On HANA, requests for GenreHierarchy are handled generically. + * + * This handler is only in effect when running with the `default` profile on + * H2. It is a stand-in for non-HANA until the CAP Java runtime can also handle + * requests for hierarchies on non-HANA databases. + * + * The handler is neither functionally complete nor correct for all requests. It + * is not intended as a blue-print for custom code. + */ public class HierarchyHandler implements EventHandler { private final PersistenceService db; @@ -202,7 +212,7 @@ private List topLevels(CqnTopLevelsTransformation topLevels, Cqn private List topLevelsLimit(CqnTopLevelsTransformation topLevels, CqnPredicate filter) { long limit = topLevels.levels(); - Map lookup = new HashMap<>(); + Map lookup = new HashMap<>(); Map expandLevels = topLevels.expandLevels(); CqnSelect getRoots = Select.from(GENRE_HIERARCHY).where(gh -> gh.parent_ID().isNull().and(filter)); @@ -230,10 +240,9 @@ private List topLevelsLimit(CqnTopLevelsTransformation topLevels if (!expandLevels.isEmpty()) { List expandedIds = expandLevels.keySet().stream().map(key -> (Integer) key).toList(); - CqnSelect expandedCQN = Select.from(AdminService_.GENRE_HIERARCHY).where(gh -> - CQL.and(filter, + CqnSelect expandedCQN = Select.from(AdminService_.GENRE_HIERARCHY).where(gh -> CQL.and(filter, CQL.or(gh.ID().in(expandedIds), gh.parent_ID().in(expandedIds)))); - + List expanded = db.run(expandedCQN).listOf(GenreHierarchy.class); expanded.forEach(gh -> { if (!lookup.keySet().contains(gh.getId())) {