or:
How to call
an OAuth protected Service
from UI5 app
using App Router
Quicklinks:
Diagram
Sample Project Files
Welcome to the second part of our tutorial about how to call our Backend service API from
frontend application
Our precondition: we don’t want to handle the OAuth flow in our application code.
So in the
previous tutorial we learnt how to use the App Router to handle the OAuth, and how to configure it
The good news: in the user interface, we don’t need to care about OAuth at all.
We only have to use the configured route, instead of directly calling the protected OData service
In the present tutorial, we’ll focus on what needs to be done in a
UI5 application
The user interface will be as minimalistic as possible, so we can focus on the required configuration of the UI5 app
BTW:
This blog is part of a
series of tutorials explaining the usage of
SAP Cloud Platform Backend service in detail.
Goal
Call an OAuth protected OData service from a UI5 application, without any extra code.
Use App Router to handle OAuth flow
Deploy to Cloud Foundry
This is how our simple UI5 app will look like.
What the screenshot doesn't show: that there are quite some configuration files in between the backend data and the frontend display.
The
diagram in the appendix tries to show the flow.
Prerequisites
Reading the
previous tutorial is a (pleasant) prerequisite, as it explains App Router configuration.
Other
prerequisites mentioned in previous tutorial are still valid
UI5 expertise is not required
Those who just need sample code may
jump directly to the appendix
Create Folder Structure
We create a project folder called
ui5_to_bs to make clear that our app should demonstrate how to call Backend service from UI5
The folder structure is very similar like in the previous tutorial. But instead of a simple HTML file, our webapp folder contains a UI5 application (also simple), with all relevant files
Create folder structure as follows:
C:\ui5_to_bs
appfolder
webapp
controller
App.controller.js
ProductList.controller.js
view
App.view.xml
ProductList.view.xml
Component.js
index.html
manifest.json
package.json
xs-app.json
manifest.yml
On my file system, it looks like this:
The following sections will go through each of these files and explain some relevant settings, but only those relevant settings which weren't discussed in
previous tutorial.
As usual, the full file content can be found in the
appendix section at the end of this tutorial
Cloud App
We deploy one app to Cloud Foundry, it is described by the
manifest.yml file
manifest.yml
In the
manifest.yml we define a destination with URL pointing to the host of our OData service:
"url": "https://backend-service-api.cfapps.eu10.hana.ondemand.com"
This allows to reuse this destination from multiple UI elements in our UI5 application, which may reach out to multiple services or entity sets
App Router
App Router configuration was described in detail in the
corresponding section in previous tutorial.
So we just need to note the differences
package.json
In our package.json file we now need to add the dependencies required by ui5:
"dependencies": {
"@sap/approuter": "^6.0.1",
"@openui5/sap.m": "^1",
"@openui5/sap.ui.core": "^1",
"@openui5/themelib_sap_belize": "^1"
},
xs-app.json
Just like in the previous blog, we define a route which points to the Backend service - destination
In this tutorial, we're free to give any name of our choice, so let’s give it a clear name so that we can recognize it later.
This route will be used by our UI5 app.
The name will make clear that UI5 won't point directly to a URL, but to an approuter route:
{
"source": "^/approuter/route/bas/productsrv/(.*)$",
"destination": "env_destination_bs_api",
. . .
UI5 application
Our UI5 application is very simple: It just displays a list of products.
It is a standard UI5 app with a list-element which is bound to an OData model
The only interesting topics about it:
How is the UI5 app started?
Answer: just like in previous tutorial, there’s a default route from App Router to UI5 app
How does it connect to the OAuth protected OData service?
Answer: via App Router
How does it connect to App Router?
Answer: it calls a route defined in App Router
How does it circumvent the OAuth protection?
Answer: it doesn’t. OAuth is handled by App Router
How is the list bound to App Router?
Answer: in manifest.json we point to the AppRouter-route
How...?
Enough questions, now let’s see it in detail
manifest.json
In the present blog, the
manifest.json is the most prominent file.
It contains some app and UI5 relevant settings, but the following 2 are interesting for us:
First we define a so-called "data source":
"dataSources": {
"productDataSource": {
"uri": "/approuter/route/bas/productsrv/odatav2/DEFAULT/PRODUCTSERVICE;v=1/",
This data source has a “uri” property which could be a hard-coded URL pointing to an OData service
But in our case: it points to the route which we defined earlier in our App Router config:
/approuter/route/bas/
This route points to the host of our OData service
As such, here, in manifest.json, we have to append the relative path to our service:
productsrv/odatav2/DEFAULT/PRODUCTSERVICE;v=1/"
Note:
The data source has to point to the root URL of the OData service, the so-called service-document URL
Not to an entity set (collection), e.g.
"…PRODUCTSERVICE;v=1/products"
We will specify the entity set name when binding a ui control in order to get the data (see below)
The second interesting setting in manifest:
We define a ui5-“model” which points to the data source defined above:
"sap.ui5": {
"models": {
"productModel": {
"dataSource": "productDataSource"
. . .
The ui5-model can be bound to ui controls and like that, data can be automatically fetched.
We’ll see it below
Note:
Most ui5 apps which use only one data model will specify an empty string as default model. Like that, the xml is less verbosy.
But as usual, I like to make everything very clear, so I’m giving a name here and later we will have to specify it
index.html
We’re using a standard
index.html, just like any other ui5 app, so we don’t need to describe anything.
Component.js
Same here
App.view.xml
Same here again, nothing special.
We use a standard app.view which instantiates a child view
ProductList.view.xml
Here we design out User Interface
This is the child view which contains one ui element: the list.
This list is supposed to display the products provided by the backend OData service
In order to bind a ui element to an OData service, it has to be bound to a ui5-model
As such, we bind the list control to the ui5-model which we defined in the manifest.json:
<List
items="{
path : 'productModel>/Products'
The snippet shows:
The list contains multiple “items”, the number of items depends on the amount of data in the backend. But we don't need to care, we just tell the list to get the items from the model with name
productModel
However, that is not enough.
As mentioned earlier, we have to specify which "entity set" we want to use
Remember:
An OData service can have multiple entity sets, which represent different collections of data (e.g. Products, salesOrders, customers, invoices, etc)
As such, we have to declare not only the model name but also the entity set name, to complete the path:
'productModel>/Products'
Next step:
Still: binding the list items to an entity set is not enough:
We also need to tell the items what pieces of data they should display.
With other words: each entity of an OData service consists of multiple properties which carry the actual data.
So here we’re supposed to name one or more property names.
The syntax is shown below:
<items>
<ObjectListItem
title="{productModel>name}">
Inside curly brackets we write the property name. Since we decided not to use default model (without name) we have to prepend the model name before the property name
Remember:
Our OData model has an "EntityType" with name “Products” and it has a property with name “name”
Instead of
{productModel>name}
we could write
{productModel>description}
or even
title="{productModel>name} - (with ID: {productModel>productID})">
How to know what to write?
As mentioned, the productModel" name is taken from
manifest.json.
The "productID" is taken from the OData service metadata:
https://backend-service-api...odatav2/DEFAULT/PRODUCTSERVICE;v=1/$metadata
In my example, it looks like this:
Note:
Same property names can also be taken from CDS file
App.controller.js
As promised, we’re not writing any custom code.
We only want to demonstrate how the OAuth protected resources do find their way from backend to user interface
ProductList.controller.js
Also no custom implementation
Summary
What have we learned in this tutorial?
The message:
Yes, OAuth protected OData service can be accessed from UI5 app |
The solution:
Deploy App Router.
Configure 2 routes:
First route to open web app
Second route to call the OAuth proteced OData service
Define destination pointing to the OData service
Configure App Router to handle OAuth flow
And the UI5 app?
It uses the second route instead of a directly using a destination or OData service
Nothing else to do in the UI5 app
Links
App Router documentation in
SAP Help Portal
UI5 documentation:
https://ui5.sap.com
SAP Cloud Platform Backend service in
SAP Help Portal documentation
Ads
Good moment for some advertisements...
Series of tutorials, lot of info around Backend service
Getting started with App Router:
part 1,
part 2,
part 3
See how App Router
forwards a token
Understanding OAuth
part 1 and
part 2
Manually accessing OAuth protected service
Implementing OAuth flow in node.js application
part 1 and
part 2
Install
node.js and configure SAP registry
Use
command line client to deploy to Cloud Foundry
Use Cloud Platform Cockpit to deploy to Cloud Foundry
Create API (OData service) in Backend service (EASY!!)
Create
XSUAA instance with correct parameters for Backend service
Learn about
JWT token and how to view the content
Appendix 1: Diagram
Appendix 2: All Project Files
In this appendix you can find all files required to run the described sample application.
For your convenience, see here again the folder structure:
CDS
This CDS file was used as target OData service for this sample project.
You may use it to
create an API (OData service) in SAP Cloud Platform Backend service
service ProductService {
entity Products{
key productID : String;
name : String;
description : String;
}
}
manifest.yml
---
applications:
- name: ui5_to_bs
memory: 256M
path: appfolder
services:
- XsuaaForHtml
env:
destinations: >
[
{
"name": "env_destination_bs_api",
"url": "https://backend-service-api.cfapps.eu10.hana.ondemand.com",
"forwardAuthToken": true
}
]
package.json
{
"name": "app03",
"dependencies": {
"@sap/approuter": "^6.0.1",
"@openui5/sap.m": "^1",
"@openui5/sap.ui.core": "^1",
"@openui5/themelib_sap_belize": "^1"
},
"scripts": {
"start": "node node_modules/@sap/approuter/approuter.js"
}
}
xs-app.json
{
"welcomeFile": "approuterhome/index.html",
"authenticationMethod": "route",
"routes": [
{
"source": "^/approuterhome/(.*)$",
"target": "$1",
"localDir": "webapp",
"authenticationType": "xsuaa"
},
{
"source": "^/approuter/route/bas/productsrv/(.*)$",
"target": "$1",
"destination": "env_destination_bs_api",
"authenticationType": "xsuaa"
}
]
}
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script
id="sap-ui-bootstrap"
src="https://ui5.sap.com/resources/sap-ui-core.js"
data-sap-ui-theme="sap_belize"
data-sap-ui-resourceroots='{
"com.example.bas": "./"
}'
data-sap-ui-oninit="module:sap/ui/core/ComponentSupport"
data-sap-ui-compatVersion="edge"
data-sap-ui-async="true">
</script>
</head>
<body class="sapUiBody" id="content">
<div data-sap-ui-component data-name="com.example.bas" data-id="container" data-settings='{"id" : "bas"}'></div>
</body>
</html>
Component.js
sap.ui.define([
"sap/ui/core/UIComponent"
], function (UIComponent) {
"use strict";
return UIComponent.extend("com.example.bas.Component", {
metadata : {
manifest: "json"
},
init : function () {
UIComponent.prototype.init.apply(this, arguments);
}
});
});
manifest.json
{
"_version": "1.12.0",
"sap.app": {
"id": "com.example.bas",
"type": "application",
"applicationVersion": {
"version": "1.0.0"
},
"dataSources": {
"productDataSource": {
"uri": "/approuter/route/bas/productsrv/odatav2/DEFAULT/PRODUCTSERVICE;v=1/",
"type": "OData",
"settings": {
"odataVersion": "2.0"
}
}
}
},
"sap.ui": {
"technology": "UI5",
"deviceTypes": {
"desktop": true
}
},
"sap.ui5": {
"rootView": {
"viewName": "com.example.bas.view.App",
"type": "XML",
"async": true,
"id": "app"
},
"dependencies": {
"minUI5Version": "1.60",
"libs": {
"sap.m": {}
}
},
"models": {
"productModel": {
"dataSource": "productDataSource"
}
}
}
}
App.controller.js
sap.ui.define([ "sap/ui/core/mvc/Controller"],
function (Controller) {
"use strict";
return Controller.extend("com.example.bas.controller.App", {
});
});
ProductList.controller.js
sap.ui.define(["sap/ui/core/mvc/Controller"],
function (Controller) {
"use strict";
return Controller.extend("com.example.bas.controller.ProductList", {
});
});
App.view.xml
<mvc:View
controllerName="com.example.bas.controller.App" xmlns="sap.m" xmlns:mvc="sap.ui.core.mvc" displayBlock="true">
<Shell>
<App>
<pages>
<Page title="User Interface for SAP Cloud Platform Backend service ">
<content>
<mvc:XMLView viewName="com.example.bas.view.ProductList"/>
</content>
</Page>
</pages>
</App>
</Shell>
</mvc:View>
ProductList.view.xml
<mvc:View
controllerName="com.example.bas.controller.ProductList" xmlns="sap.m" xmlns:mvc="sap.ui.core.mvc">
<List id="productList" class="sapUiResponsiveMargin" width="auto"
items="{
path : 'productModel>/Products'
}">
<headerToolbar>
<Toolbar>
<Title text="List of Products available in Backend service via App Router"/>
</Toolbar>
</headerToolbar>
<items>
<ObjectListItem
title="{productModel>name} - (with ID: {productModel>productID})">
</ObjectListItem>
</items>
</List>
</mvc:View>