SAP CAP Remote Services & SAP Fiori Elements

Evaluate and explore the capabilities of SAP CAP and SAP Fiori Elements with "Yet Another Covid-19 Tracker".

SAP CAP Remote Services & SAP Fiori Elements
5 min read
Posted: by Kai Niklas

In the past weeks I explored some capabilities of the SAP Cloud Application Programming Model (CAP) in more details in conjunction with SAP Fiori Elements. To evaluate and explore these capabilities I decided to build "Yet Another Covid-19 Tracker".

Note: This is a more advanced article. If you want to learn more about the basics of SAP CAP please refer to the official SAP CAP Getting Started Guide.

What you will learn #

The goal was to better understand and learn how to:

  • Call a remote service with standard CAP APIs
  • Explore options to map REST to OData Services
  • Visualize Data with SAP Fiori Elements

Here are some screenshots of what we are going to build.

List Report: Summary of all Countries world-wide and their corresponding Covid-19 cases sorted by new confirmed cases per default.
Object Page: Details of the country with key figures and a visualized historic data set.

The Remote Service #

There are plenty of services which could be used to grab Covid-19 data. I stumbled across https://covid19api.com which has a good (and free) REST API to consume. It provides two endpoints which I want to use for the App:

  • GET /summary : A summary of new and total cases per country, updated daily.
  • GET /total/country : Returns all cases by case type for a country.

CAP Data Model + Service Definition #

The remote REST API should be made available to the Fiori Elements front-end via the standard CAP Data Model + Annotation approach.

First, we create the data model. One entity which covers the countries with their summary (new and total cases) and one entity which covers the historic details per country. This is more or less a one-to-one copy of what the Covid-19 API provides. An association links both entities with each other.

I copied the attribute names of the original service to ease mapping. For a real-world application you may choose a different approach and also use different naming conventions, e.g., lowerCamelCase. Please refer to the Github Repository for more details on the data model.

Data Model for Covid-19 Service

Data Model for Covid-19 Service

The CDS service definition simply exposes the the entities as projections on them.

namespace srv;

using {db} from '../db/Covid';

service CovidService {
entity Countries as projection on db.Countries;
entity CountryHistoryDetails as projection on db.CountryHistoryDetails;
}

Options to get data #

There are basically 3 options from which we can choose to integrate the data provided by the Covid-19 API:

  1. Load data upfront: We could load all data upfront into the DB and schedule batch jobs to regularly fetch and load new data. This way we reuse CAP functionality and avoid calling a REST service over and over.
  2. Call REST service instead of querying the DB: We could override the on.READ event, call the REST service and map it to the defined data structure. This way we ensure to get the most up-to-date data. But on the other hand we also have to take care of applying and implementing OData functionality after retrieving the data, such as counting, sorting, filtering, selecting, aggregating, etc.
  3. Call REST service and write it to DB: We could override the before.READ event, call the REST service and write the data into the DB. This way, we do not need to replicate the OData functionality and have implicitly build a caching mechanism.

This article describes option 3.

Delegate Call to REST API #

As described, we first need to insert data into the DB. This can be done by implementing a before handler for our service:

const cds = require("@sap/cds");

module.exports = cds.service.impl(function () {
this.before("READ", "Countries", async (req, next) => {
const { Countries } = this.entities;

// delete all countries
await DELETE.from(Countries);

// fetch daily summary from covid API
const Covid19Api = await cds.connect.to("Covid19Api");
var countries = await Covid19Api.run(req.query);

// insert summary into Countries table
await INSERT.into(Countries).entries(countries);

return;
});

Note: For simplicity I only included the Countries handler and omitted the caching implementation.

In the handler we connect to our Service called Covid19API.

Define Remote REST API - Covid19API #

First, we define an remote service in .cdsrc.json named Covid19API. The URL property points to the root path of the API endpoint.

{
"requires": {
"Covid19Api": {
"kind": "rest",
"impl": "srv/external/Covid19Api.js",
"credentials": {
"url": "https://api.covid19api.com"
}
}
}
}

Now, we actually need to call the REST API. This can be done using RemoteServices (as of May 2021 this is not fully documented yet). A good explanation can also be found in the blog post by Robert Witt: Consuming a REST Service with the SAP Cloud Application Programming Model.

We create the class Covid19Api which extends cds.RemoteService. In the init method we need to add the required handlers:

  • this.reject: All events except READ are rejected
  • this.before: Responsible for translating the application service query (OData) to a query that the REST service understands.
  • this.on: Responsible for executing the REST call and translating the result back to the application service model.
const cds = require("@sap/cds");

class Covid19Api extends cds.RemoteService {
async init() {
this.reject(["CREATE", "UPDATE", "DELETE"], "*");

this.before("READ", "*", async (req) => {
if (req.target.name === "srv.CovidService.Countries") {
req.myQuery = req.query;
req.query = "GET /summary";
}

if (req.target.name === "srv.CovidService.CountryHistoryDetails") {
...
});

this.on("READ", "*", async (req, next) => {
if (req.target.name === "srv.CovidService.Countries") {
const response = await next(req);
var items = parseResponseCountries(response);
return items;
}

if (req.target.name === "srv.CovidService.CountryHistoryDetails") {
...
}
});

super.init();
}
}

function parseResponseCountries(response) {
var countries = [];

response.Countries.forEach((c) => {
var i = new Object();

i.Country = c.Country;
i.Slug = c.Slug;
i.CountryCode = c.CountryCode;
i.NewConfirmed = c.NewConfirmed;
i.TotalConfirmed = c.TotalConfirmed;
i.NewDeaths = c.NewDeaths;
i.TotalDeaths = c.TotalDeaths;
i.NewRecovered = c.NewRecovered;
i.TotalRecovered = c.TotalRecovered;
i.Date = c.Date;

countries.push(i);
});

return countries;
}

module.exports = Covid19Api;

Annotating the Service for SAP Fiori Elements #

To visualize the data we need to properly annotate the CDS Service definition. Let's have a look at the essential and interesting annotations:

List Report #

A table can be realized using LineItem:

annotate CovidService.Countries with @(
UI : {
LineItem : [
{Value : Country},
{Value : NewConfirmed},
{Value : TotalConfirmed},
{Value : NewDeaths},
{Value : TotalDeaths}
]
});

A table heading can be realized using HeaderInfo:

annotate CovidService.Countries with @(
UI : {
HeaderInfo : {
TypeName : 'Country',
TypeNamePlural : 'Countries',
}
});

A default sorting can be realized using PresentationVariant:

annotate CovidService.Countries with @(
UI : {
PresentationVariant : {
SortOrder : [
{
Property : NewConfirmed,
Descending : true
},
],
Visualizations : [ ![@UI.LineItem] ]
}
});

A filter field can be realized using SelectionFields:

annotate CovidService.Countries with @(
UI : {
SelectionFields : [
Country
]
});

Object Page #

A header section with data points can be realized using HeaderFacets in conjunction with DataPoints:

annotate CovidService.Countries with @(
UI : {
DataPoint#TotalConfirmed : {
$Type : 'UI.DataPointType',
Value : TotalConfirmed,
Title : 'Total Confirmed',
}
HeaderFacets : [
{
$Type : 'UI.ReferenceFacet',
Target : '@UI.DataPoint#TotalConfirmed'
}
]
});

A line chart with associated data can be realized using Facets and Chart:

annotate CovidService.Countries with @(
UI : {
Facets : [
{
$Type : 'UI.ReferenceFacet',
Target : 'CountryHistoryDetails/@UI.Chart',
Label : 'Total Numbers Chart',
}
]
});

annotate CovidService.CountryHistoryDetails with @(
UI : {
Chart : {
$Type : 'UI.ChartDefinitionType',
ChartType : #Line,
Dimensions : [
Date
],
Measures : [
deaths, confirmed
],
Title : 'Total Numbers Chart',
},
});

annotate CovidService.CountryHistoryDetails with @(
Analytics.AggregatedProperties : [
{
Name : 'deaths',
AggregationMethod : 'sum',
AggregatableProperty : 'Deaths',
![@Common.Label] : 'Deaths'
},
{
Name : 'confirmed',
AggregationMethod : 'sum',
AggregatableProperty : 'Confirmed',
![@Common.Label] : 'Confirmed'
}
]
);

A table with associated data can be realized using Facets and LineItem:

annotate CovidService.Countries with @(
UI : {
Facets : [
{
$Type : 'UI.ReferenceFacet',
Target : 'CountryHistoryDetails/@UI.LineItem',
Label : 'Total Numbers Table',
}
]
});

annotate CovidService.CountryHistoryDetails with @(
UI : {
LineItem : [
{Value : Date},
{Value : Confirmed},
{Value : Deaths},
{Value : Active},
{Value : Recovered}
]
});

Testing the App #

After starting the App via cds run we can use the build-in Fiori Preview option of the Service srv.CovidService/Countries:

Conclusion #

With the cds.RemoteService API we can use remote REST services as data sources for our application service in a CAP app. We can even use the DB to reuse the OData functionality and for caching.

We had a look at various Fiori Element annotations and how they can be realized with CAP. There are many more elements which could be explored. This is now up to you to extend and explore.

5 min read
Posted: by Kai Niklas

kai-niklas.de - All rights reserved