Skip to Content
Technical Articles

SAP CAP Remote Services & SAP Fiori Elements

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.

abc

List Report: Summary of all Countries world-wide and their corresponding Covid-19 cases sorted by new confirmed cases per default.

abc

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%20Model%20for%20Covid-19%20Service

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.

11 Comments
You must be Logged on to comment or reply to a post.
  • Hi Kai,

    Thank you for the blog. I could connect to remote service and pick up the data . But for me external\Covid19Api.js is not being called . I compared all the services looks exactly same as yours . But still not getting called. Can you suggest what mistake I could have made?

    • Hi Manikandan,

      where exactly are you stuck? Difficult to say. Have you checked that the on.before('READ', 'Countries', ()) handler is being executed when calling the corresponding OData service? Set a breakpoints in each handler and check if all are getting called. If the handler is not called, maybe there is a typo, e.g., 'Country' instead of 'Countries'?

  • Hi Kai Niklas

    Thank you for this great tutorial, Karl!

    I am especially interested in the "caching mechanism" you mention. From my understanding,

    inserting to db makes data persistent. It isn´t beeing cleared or deleted automatically...So,

    can we call it "caching" then? The reason why I am asking is because often, customers don´t want

    any data saved in HANA Cloud but rather retrieved from backend (ERP e.g.)....So actually, in CAP

    we don´t have a caching mechanism on that behalf, do we?

     

    BR

    Rufat

     

    • Hi,

      it's exactly as you said: The caching is implemented by storing the data into the db, in my case  sqlite. On every request I check if the stored data is current, e.g., not older than 1 day. If older, then I refresh the data.

      Other application caching mechanisms I know usually do it similarly by storing data in a db, memory, or file system. Depending on the requirements this differs. To assure that outdated data is removed, an additional batch job could be implemented.

      One could also think of another piece of infrastructure to handle the caching automatically, e.g., a proxy, which handles the caching on http level, i.e., cache-control. Then nothing needs to be stored.

      Best
      Kai