Skip to Content
Technical Articles
Author's profile photo Kevin Muessig

The End2End Journey: Advocates Service – CAP Service, OData V2/4 & REST in One single Project

The Advocates Service

The Advocates Service is a Node.js based SAP Cloud Application Programming Model service which is deployed on SAP BTP, Cloud Foundry runtime and connected to an SAP HANA Cloud database. The service has three endpoints defined so different types of applications can consume that service.

In this Blog Post I will talk about the implementation details of that service as well as how you can implement a multi-endpoint scenario within one CAP project.

You can find the project in the SAP Samples org on GitHub.

In case you also want to see me explaining this in a video go here:

Why do we need all these different endpoints?

The CAP technology exposes, by default, the OData V4 protocol but for some technologies an OData V2 or even a regular REST endpoint is needed for consumption. Luckily, CAP allows you to expose all of these options within one service definition and the actual code to do so is fairly simple and fast to implement.

OData V2

The OData V2 service is needed if you want to consume your CAP service with an Offline OData enabled iOS, Android or MDK app. The CDS OData V2 Adapter Proxy package can be utilised to expose the CAP service over OData V2.

OData V4

The OData V4 service endpoint is created by default and can be consumed by any frontend, microservice or any other piece of software which can parse the V4 responses. In example since mid of march the SAP BTP SDK for iOS can consume such a service without a problem, also any UI5 app can work with the service.

REST

With CAP you can also expose the service via REST protocol which is not following the OData specification. In our example this is useful if working with AppGyver as they expect some REST endpoint in their data source definition which returns the data in a certain structured way. The response should look like the following (Array of objects):

[
  {
    ID: "06f6456a-a200-4853-a359-0cc7c2f5fe81",
    createdAt: "2020-04-14T00:00:00.000Z",
    createdBy: "john.doe@company.com"
  },
  {
    ID: "06f6456a-a200-4234-a359-0cc7c2f5fe81",
    createdAt: "2020-04-14T00:00:00.000Z",
    createdBy: "john.doe@company.com"
  }
]

Implementation of the CAP Service

The Advocates Service contains out of 4 sets of Entities representing the

  • Members,
  • Skills,
  • SocialMediaPresence of the Advocates team,
  • the last entity represents a join table Members_Skills
using advocates.service as advocates from '../db/schema';

service AdvocatesService {
    @readonly : true
    entity Members as projection on advocates.Members;
    
    @readonly : true
    entity Skill as projection on advocates.Skill;
    
    @readonly : true
    entity Members_Skills as projection on advocates.Members2Skills;

    @readonly : true
    entity SocialMediaPresence as projection on advocates.SocialMediaPresence;
}

The database schema is defined and implemented as the following:

namespace advocates.service;

using {
    managed,
    sap,
    cuid
} from '@sap/cds/common';

entity Members  : cuid, managed {
    firstName   : String;
    lastName    : String;
    title       : String;
    focusArea   : String;
    skills      : Association to many Members2Skills on skills.member_ID = $self;
    socialMedia : Association to many SocialMediaPresence on socialMedia.member = $self;
    description : String;
}

entity Skill    : cuid, managed {
    name        : String;
    member      : Association to many Members2Skills on member.skill_ID = $self;
}

entity SocialMediaPresence  : cuid, managed {
    name    : String;
    url     : String;
    member  : Association to Members;
}

entity Members2Skills   : cuid, managed {
    member_ID           : Association to Members;
    skill_ID            : Association to Skill;
}

Here I am using the packages managed and cuid to have proper timestamps on my data entries and be able to use UUIDs within my database service.

The entity definition is pretty straightforward as it strictly follows the documentation of CAP.

The interesting part now is the actual exposure of the service to the different endpoints. This is done in the server.js file. This file automatically gets invoked by the build command and will cause to change the service bootstrapping in a way that it will respect whatever you’ve implemented in that file.

 

First of all we need to import the required packages:

const cds = require('@sap/cds')
const proxy = require('@sap/cds-odata-v2-adapter-proxy')
const port = process.env.PORT || 4004;

Now we define the global base directory as a helper for the cds.serve command later on. From that directory I will load the generated csn.json which will then be served to the REST endpoint. The csn.json file is defined as the following:

CSN (pronounced as “Season”) is a notation for compact representations of CDS models — tailored to serve as an optimized format to share and interpret models with minimal footprint and dependencies.

https://cap.cloud.sap/docs/cds/csn

Alright! Let us change the CAP bootstrapping to do exactly what we want:

  1. Use the OData V2 Proxy Adapter
    // define the path
    app.use(proxy({
        path: "v2",
        port: port
    }))
    
    // define the service
    app.use(proxy({
        services: {
            "/advocates/": "AdvocatesService",
        }
    }))
    
  2. Expose the service over REST using the csn.json file

    //CDS REST Handler
    let restURL = "/rest/"
    
    cds.serve('AdvocatesService')
            .from(global.__base + "/gen/csn.json")
            .to("rest")
            .at(restURL + 'advocates')
            .in(app)
            .catch((err) => {
                app.logger.error(err);
            })
  3. Enable CORS for AppGyver, the following code whitelists * which is for testing purpose only. There will be an extra Blog Post about why we have to do this.

    const cors = require('cors')
    app.use(cors())
    app.use((req, res, next) => {
        res.setHeader('Access-Control-Allow-Origin', 'https://platform.appgyver.com/');
        next();
    })

Finally we set the cds.server to the module exports and we are done.

// change the bootstrap of CAP

const cds = require('@sap/cds')
const proxy = require('@sap/cds-odata-v2-adapter-proxy')
const port = process.env.PORT || 4004;

global.__base = __dirname + "/"
console.log(global.__base)
console.log(`CDS Custom Boostrap from /srv/server.js`)

cds.on('bootstrap', app => {
    
    const cors = require('cors')
    app.use(cors())
    app.use((req, res, next) => {
        res.setHeader('Access-Control-Allow-Origin', '*');
        next();
    })

    //CDS REST Handler
    let restURL = "/rest/"

    app.use(proxy({
        path: "v2",
        port: port
    }))

    app.use(proxy({
        services: {
            "/advocates/": "AdvocatesService",
        }
    }))

    cds.serve('AdvocatesService')
        .from(global.__base + "/gen/csn.json")
        .to("rest")
        .at(restURL + 'advocates')
        .in(app)
        .catch((err) => {
            app.logger.error(err);
        })
})

module.exports = cds.server

This is all you have to do to expose your CAP service over multiple endpoints. Easy right 🙂?!

The mta.yaml for deployment to SAP BTP, Cloud Foundry runtime

As you probably now, the mta.yaml is for building a deployable archive for SAP BTP, Cloud Foundry runtime. It will include everything from the database schema to the information of endpoints and database connection.

I want to walk you through the mta definition for the advocates service.

Let us start with the metadata for the mta:

## appName = advocates-service
## language=nodejs; multiTenant=false
_schema-version: '3.1'
ID: advocates-service
version: 1.0.5
description: The Developer Advocates Service
parameters:
  enable-parallel-deployments: true

build-parameters:
  before-all:
    - builder: custom
      commands:
        - npm install
        - npx cds build

The server module definition describes the name and information the advocates service will need to be deployed and properly initiated by the Cloud Foundry runtime. It holds all the information the runtime needs to create and properly configure the container the advocates service will run in.

Ignoring some of the generated files will help reduce the archives size and will fasten up the deployment process.

# --------------------- SERVER MODULE ------------------------
  - name: advocates-service-srv
  # ------------------------------------------------------------
    type: nodejs
    path: . # root for nodejs because of CAP way // fix comment
    parameters:
      memory: 512M
      disk-quota: 2048M
      host: 'advocatesservice'
    requires:
      # Resources extracted from CAP configuration
      - name: advocates-service-db
    provides:
      - name: srv-api
        properties:
          srv-url: '${default-url}'
    build-parameters:
      ignore: [".*/", "*default-env.json", "./db/node_modules", "./node_modules"]

With a great service there comes a great database connection!

Here I define the database type, the name of the deployer and the properties as well as the build pack the deployer should use. What will happen during deployment is that a little application with the name of advocates-service-db-deployer will start up and create the HDI container. It will also make sure to deploy the database into the HDI container for the consumption of our data through the CAP service.

# -------------------- DB MODULE ------------------------
  # Do the deployment into the HDI container cds deploy --to hana
  - name: advocates-service-db-deployer
  # ------------------------------------------------------------
    type: hdb
    path: db
    parameters:
      buildpack: nodejs_buildpack
    requires:
      - name: advocates-service-db
        properties:
          TARGET_CONTAINER: '~{hdi-service-name}'
    build-parameters:
      ignore: ["default-env.json", ".env"]

Lastly, some definition for the needed resources of our deployment, in our case the HDI container:

# ------------------------------------------------------------
resources:
  # services extracted from CAP configuration
  # 'service-plan' can be configured via 'cds.requires.<name>.vcap.plan'
  # Create HDI container
  - name: advocates-service-db
# ------------------------------------------------------------
    type: com.sap.xs.hdi-container
    parameters:
      service: hana
      service-plan: hdi-shared
    properties:
      hdi-service-name: '${service-name}'

Here the full mta.yaml file:

## appName = advocates-service
## language=nodejs; multiTenant=false
_schema-version: '3.1'
ID: advocates-service
version: 1.0.5
description: The Developer Advocates Service
parameters:
  enable-parallel-deployments: true

build-parameters:
  before-all:
    - builder: custom
      commands:
        - npm install
        - npx cds build

modules:
  # --------------------- SERVER MODULE ------------------------
  - name: advocates-service-srv
  # ------------------------------------------------------------
    type: nodejs
    path: . # root for nodejs because of CAP way // fix comment
    parameters:
      memory: 512M
      disk-quota: 2048M
      host: 'advocatesservice'
    requires:
      # Resources extracted from CAP configuration
      - name: advocates-service-db
    provides:
      - name: srv-api
        properties:
          srv-url: '${default-url}'
    build-parameters:
      ignore: [".*/", "*default-env.json", "./db/node_modules", "./node_modules"]
  
  # -------------------- DB MODULE ------------------------
  # Do the deployment into the HDI container cds deploy --to hana
  - name: advocates-service-db-deployer
  # ------------------------------------------------------------
    type: hdb
    path: db
    parameters:
      buildpack: nodejs_buildpack
    requires:
      - name: advocates-service-db
        properties:
          TARGET_CONTAINER: '~{hdi-service-name}'
    build-parameters:
      ignore: ["default-env.json", ".env"]

# ------------------------------------------------------------
resources:
  # services extracted from CAP configuration
  # 'service-plan' can be configured via 'cds.requires.<name>.vcap.plan'
  # Create HDI container
  - name: advocates-service-db
# ------------------------------------------------------------
    type: com.sap.xs.hdi-container
    parameters:
      service: hana
      service-plan: hdi-shared
    properties:
      hdi-service-name: '${service-name}'

Now the service can be packaged via the MTA build tool and over the Cloud Foundry CLI deployed to SAP BTP.

In the root of the project execute:

mbt build

The generated mtar can than be deployed via:

cf deploy '<PATH>/advocates-service/mta_archives/advocates-service_1.0.5.mtar'

 

Hurray! A great advocates service is created and deployed with a solid connection to a HANA DB.

There is still a lot of work to do before the End2End example is done! So stay tuned and Happy Coding!

Assigned Tags

      3 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Abhijeet Kankani
      Abhijeet Kankani

      Hi Kevin,

       

      I would say nice blog as single project can be used in odata v2, v4 or rest.

      I want to clear one doubt regarding deployment, is it good to deploy app using mta.yaml or using manifest?

       

      I like the mta.yaml deployment but not sure how to use in Jenkins for building CI/CD pipeline. As if we are using manifest than only cf push is enough.

      For mta.yaml can we use these commands mbt build and cf deploy in Jenkins?

       

      Any suggestion or comment is appreciated.

      Regards,

      Abhijeet Kankani

      Author's profile photo Kevin Muessig
      Kevin Muessig
      Blog Post Author

      Hi Abhijeet Kankani ,

      thank you.

      You can also use the manifest approach. This should work as good as using the MTA approach:

      # Generated manifest.yml based on template version 0.1.0
      # appName = advocates-service
      # language=nodejs
      # multitenancy=false
      ---
      applications:
      # -----------------------------------------------------------------------------------
      # Backend Service
      # -----------------------------------------------------------------------------------
      - name: advocates-service-srv
        random-route: true  # for development only
        path: srv
        memory: 512M
        disk-quota: 2048M
        buildpack: nodejs_buildpack
        services:
        - advocates-service-db
      
      # -----------------------------------------------------------------------------------
      # HANA Database Content Deployer App
      # -----------------------------------------------------------------------------------
      - name: advocates-service-db-deployer
        path: db
        no-route: true
        health-check-type: process
        memory: 256M
        instances: 1
        buildpack: nodejs_buildpack
        services:
        - advocates-service-db
      

      The question if mbt build and cf deploy would work on Jenkins I am not sure as I haven't tried it out. But I wouldn't see an issue with this, as long as your Jenkins is somehow connected against your runtime. Everything else should be executable on command line over Jenkins.

      Cheers,

      Kevin

      Author's profile photo Abhijeet Kankani
      Abhijeet Kankani

      Thanks Kevin Muessig  for clarification.