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.
The goal was to better understand and learn how to:
Here are some screenshots of what we are going to build.
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.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
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;
}
There are basically 3 options from which we can choose to integrate the data provided by the Covid-19 API:
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.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.
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
.
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 rejectedthis.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;
To visualize the data we need to properly annotate the CDS Service definition. Let's have a look at the essential and interesting annotations:
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
]
});
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}
]
});
After starting the App via cds run
we can use the build-in Fiori Preview option of the Service srv.CovidService/Countries
:
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.
Answer free text questions against the DB using GPT-3.
Let's build an adapter with SAP CAP to transform an OData Service to a custom REST Service
Unchain SAP CAP: How to enable social login and role based access control using Auth0
Deploy SAP CAP on Heroku and containerize it with Docker