Skip to Content
Technical Articles
Author's profile photo Kai Niklas

SAP CAP Adapter to expose OData as custom REST Service

This article demonstrates how to consume, transform and expose a given OData Service as custom REST Service using SAP CAP as Adapter.

Motivation

Reasons why you want or need to create such an adapter:

  • Specific architecture requirements, e.g., harmonization of how services look like across the enterprise or microservice architecture
  • Access a service using different URL patterns, e.g., ressource/{id} instead of ressource({id})
  • Change structure and content of the payload, e.g., delete, add, change, rename, combine, etc. of fields
  • Change the standard OData query paramater names, e.g., pagination with ?pageSize and ?offset instead of $top and $skip
  • Reuse existing services without re-implementing them, e.g., OData services but also REST or classic “non-compliant” WS Services

Note: With OData V4 it is actually possible to address ressources in a REST-ish style as mentioned in the comments by David Kunz, i.e., ressource/{id} and ressource({id}) is both a valid option.

Outline

We will have a look at the following topics:

  • Setup the adapter project
  • Restructure or map OData to custom REST service
  • Custom pagination by mapping query parameters

Setup Adapter Project

As always we start with initializing the CAP Project using the command cds init.

Import OData Service

Now let’s import the OData Service which needs to be adapted. For demonstration purposes we use the good old Northwind Service from https://services.odata.org/V2/Northwind/Northwind.svc/. But it can be basically any OData Service.

  1. Navigate to the metadata URL at https://services.odata.org/V2/Northwind/Northwind.svc/$metadata
  2. Save the metadata file as .edmx into the project folder, e.g., .\\Northwind.edmx
  3. Run the command cds import Northwind.edmx. This command does two things:
    1. It converts the .edmx file into a .csn file and stores both into the folder .\\srv\\external\\
    2. An initial entry is created in package.json with the basic information to use the model

Configure the Service Destination

Later we want to call the OData service, therefore, we need to configure the destination. For development purposes it is enough to extend the configuration in package.json, so that it looks like this:

"cds": {
    "requires": {
      "Northwind": {
        "kind": "odata-v2",
        "model": "srv\\\\external\\\\Northwind",
        "credentials": {
          "url": "<https://services.odata.org/V2/Northwind/Northwind.svc/>"
        }
      }
    }
  }

Note: We could also add it into the .cdsrc.json file, but without the “cds” wrapper.

Note 2: If we want to deploy it to SAP BTP, we could use the destination service. There we can also store the authentication details for the service, as enterprise services are usually protected and we do not want to expose this information in a configuration file. See the documentation for more details: https://cap.cloud.sap/docs/guides/consuming-services#rest-and-odata

Expose the OData Service as REST Service

First we create the service definition in .\\srv\\Northwind.cds. With the @protocol annotation we tell CAP basically to omit all the OData specific information and use plain REST.

using {Northwind as external} from './external/Northwind.csn';

@protocol:'rest'
service NorthwindService {

    @readonly
    entity Categories as projection on external.Categories;

}

Second step is to create a custom service implementation in \\srv\\Northwind.js. This implementation calls the OData Service and returns the result according to the defined protocol in the previous step to the caller.

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

module.exports = cds.service.impl(async function () {
  const { Categories } = this.entities;
  const service = await cds.connect.to("Northwind");

  this.on("READ", Categories, (req) => {
    return service.tx(req).run(req.query);
  });

});

Let’s start our application with cds watch and navigate to http://localhost:4004/northwind/Categories. If we compare the output of the original service we see that the OData information is not present anymore, and also the encapsulation into {d: {results: [] }} is removed.

Restructure / Map OData to custom REST Service

A typical requirement for such an adapter is to expose the service with a different data structure which fulfills the REST standards of the company. There are different approaches which can be leveraged to achieve adding, removing, renaming, etc. of fields.

Restructure in CDS Entity View

The most simple and straight forward approach to realize removing and renaming of fields is to use the CDS service definition as follows:

using {Northwind as external} from './external/Northwind.csn';

@protocol:'rest'
service NorthwindService {

    @readonly
    entity Categories as projection on external.Categories {
        CategoryID as id,
        CategoryName as name,
        Description as descr,
        Picture as pictureBase64
    };

}

Advanced Restructuring in Service Implementation

If the CDS approach is not sufficient , we can directly manipulate the object in JavaScript which gets returned as JSON string from the REST Service. A sample implementation looks like this:

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

module.exports = cds.service.impl(async function () {
  const { Categories } = this.entities;
  const service = await cds.connect.to("Northwind");

  this.on("READ", Categories, async (req) => {
    // execute the query
    const categories = await service.tx(req).run(req.query);

    // multiple objects
    if (Array.isArray(categories)) {
      return categories.map((o) => transformObject(o));
    }

    // one object
    return transformObject(categories);
  });

  let transformObject = function (o) {
    // add field
    o.New = "Some value";

    // remove field
    delete o.Picture;

    // change field value
    o.Description = "Some Text: " + o.Description;

    return o;
  };
});

Custom Pagination by mapping query parameters

Let’s assume we need to provide pagination with ?pagesize and ?offset. We can simply map those query parameters to the standard OData $top and $skip and receive the desired results. The implementation could look like the following:

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

module.exports = cds.service.impl(async function () {
  const { Categories } = this.entities;
  const service = await cds.connect.to("Northwind");

  this.on("READ", Categories, async (req) => {
    // ?pagesize instead of OData $top
    if (req.req.query.pageSize) {
      req.query.SELECT.limit.rows = { val: parseInt(req.req.query.pageSize) };
    }

    // ?offeset instead of OData $skip
    if (req.req.query.offset) {
      req.query.SELECT.limit.offset = { val: parseInt(req.req.query.offset) };
    }

    // execute the query
    const categories = await service.tx(req).run(req.query);

    // multiple objects
    if (Array.isArray(categories)) {
      return categories.map((o) => transformObject(o));
    }

    return transformObject(categories);
  });
});

Now we can test pagination as follows: http://localhost:4004/northwind/Categories?pageSize=2&offset=2. We receive 2 items starting with ID 3, which means, that we skipped the first 2 items.

Conclusion

This article demonstrated different options to use SAP CAP as an adapter to expose a given OData Service as custom REST Service. We saw, that the effort to consume an OData Service with SAP CAP is quite low and the transformation to custom REST solely depends on the requirements.

Call to Action

Let me know in the comments which further requirements you see for such kind of adapters.

Assigned Tags

      5 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo David Kunz
      David Kunz

      Hi Kai Niklas ,

       

      Great blog post! One small remark: In newer OData versions, you can also specify the key using the REST-style notation, e.g. /MyEntity/1

      Best regards,
      David

      Author's profile photo Kai Niklas
      Kai Niklas
      Blog Post Author

      Thanks for the hint. I will update the article to reflect this.

      Do you know if and how this works with draft mode activated?

      Author's profile photo David Kunz
      David Kunz

      Yes, it also works in draft mode. There you have two keys (IsActiveEntity and the primary key).

      Example: http://localhost:4004/admin/Books/201/true

      (using the cloud-cap-samples example)

      Author's profile photo Martin Stenzig
      Martin Stenzig

      This is great and has a lot of applicability... I just finished a similar example to add a GeoJSON endpoint 24 hours ago and before I read this blog using bootstrapping in server.js.

      I guess some refactoring to make it even simpler is in order 🙂

      Thanks Kai!

      Author's profile photo Kai Niklas
      Kai Niklas
      Blog Post Author

      Have you seen this blog post? This is also about geo data in SAP CAP: