32

The End2End Journey: Advocates Service – CAP Service, OData V2/4 & REST in O...

 3 years ago
source link: https://blogs.sap.com/2021/05/06/the-end2end-journey-advocates-service-cap-service-odata-v2-4-rest-in-one-single-project/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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.

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.

Service_Endpoints.png

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_V2_Metadata.png

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.

OData_V4_Metadata.png

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: "[email protected]"
  },
  {
    ID: "06f6456a-a200-4234-a359-0cc7c2f5fe81",
    createdAt: "2020-04-14T00:00:00.000Z",
    createdBy: "[email protected]"
  }
]

REST_Service.png

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.

84565597.png

diagram_legend.png

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.jsonwhich 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"]

Service_Deployment.png

With a great service there comes a great database connection!

download.jpeg

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-deployerwill 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"]

Instances.png

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.

minions.gif

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK