Skip to Content
Technical Articles

Part 1: How to build an extension application for SAP S/4HANA Cloud using CAP, SAP Cloud SDK and SAP Fiori elements

SAP S/4HANA Cloud customers have their own specific business needs. Some of these business needs can be implemented using In-App Extensibility, but some of them require Side-by-Side Extensibility. This blog post series covers how to do Side-by-Side Extensibility by building an application on SAP Cloud Platform.

More specific, how to build the application using SAP Cloud Application Programming Model – Node.js, SAP Cloud SDK – JavaScript and SAP Fiori elements.

What to expect from this blog post series?

The purpose of this blog post series is to showcase the end-to-end technical knowledge of building an extension application using the mentioned technologies. The application will follow a simple business scenario and will focus on implementation steps.

I will cover a step-by-step tutorial with code snippets, screenshots and explanations of how to develop and configure the application.

What’s the business scenario?

In the SAP S/4HANA Cloud system we have Business Partner Master Data. Let’s imagine we want some end-users to be able to modify the Addresses of these Business Partners.

Part 1: Building a CAP application

This part will cover how to use SAP Cloud Application Model following these main steps:

  • Generate a new extension application using CAP
  • Call SAP API Business Hub sandbox
  • Configure, build and deploy the application on SAP Cloud Platform
  • Add OData V2 adapter and READ one entity

What are the prerequisites for this part?

You should follow the blog post Part 0 from this series or have the following:

  • URL for the OData service
  • User and password for Basic Authentication

Access to:

  • SAP S/4HANA Cloud system
  • SAP Cloud Platform (you can create a trial account) with Cloud Foundry subaccount

Install:

  • Node.js (use the latest LTS version)
  • @sap/cds-dk globally by running in a Terminal the command: npm i -g @sap/cds-dk
  • SQLite if in you are using Windows: https://sqlite.org/download.html
  • cf CLI: https://docs.cloudfoundry.org/cf-cli/install-go-cli.html
  • MTA Build Tool: npm install -g mbt
  • CLI plugin for Multi-Target Application (MTA) operations in Cloud Foundry by running the command: cf install-plugin multiapps
  • Visual Studio Code
  • VS Code extension SAP Fiori tools – Extension Pack

If you want to avoid installing the prerequisites use SAP Business Application Studio instead of Visual Studio Code. Create a dev space for SAP Cloud Business Application. SAP Business Application Studio is also available in the SAP Cloud Platform trial account.

SAP S/4HANA Cloud system access is needed, but the initial steps can be done using the SAP API Business Hub sandbox. So don’t give up yet if you don’t have access!

Generate a new extension application using CAP

Steps to follow:

  1. Open a Terminal in a directory where you want the project to be stored and run the following command: cds init cap-adman
  2. Go to SAP API Business Hub and in the right upper corner press on Log on.
  3. Look for SAP S/4HANA Cloud Business Partner (A2X) OData API. Click on Details and then on Download API Specification and choose EDMX.
  4. Copy and paste the .edmx file in the project folder /cap-adman.
  5. In Visual Studio Code, open a Terminal which is pointing to the /cap-adman folder and run the command: cds import API_BUSINESS_PARTNER.edmx. In your project in the /srv folder, you will see now an /external folder where you can find the .edmx downloaded file and the .csn file generated by cds.
  6. Under /srv folder create a file address-manager-service.cds and copy the following contents inside it:
    using {API_BUSINESS_PARTNER as external} from './external/API_BUSINESS_PARTNER.csn';
    
    service AddressManagerService {
    
        @readonly
        entity BusinessPartners         as projection on external.A_BusinessPartner {
            BusinessPartner, LastName, FirstName, to_BusinessPartnerAddress
        };
    
        entity BusinessPartnerAddresses as projection on external.A_BusinessPartnerAddress {
            BusinessPartner, AddressID, Country, PostalCode, CityName, StreetName, HouseNumber
        }
    
    };
  7. For initial testing purposes we will create some mock data and run the app. Under /external folder create folder /data and inside of it a file with this exact name: API_BUSINESS_PARTNER-A_BusinessPartner.csv and paste the following contents inside of it:
    BusinessPartner;LastName;FirstName
    30413010;Smith;John
    30413011;Thomson;Georg​
  8. Open a Terminal and run the following command: cds watch
  9. Go to http:localhost:4004/ and navigate to http://localhost:4004/address-manager/BusinessPartners to see the mock data.

Call SAP API Business Hub sandbox

Steps to follow:

  1. Open a Terminal and run: npm install
  2. Open the package.json file and in the cds.requires.API_BUSINESS_PARTNER object after “model” put a comma and add the following:
    "credentials": {
      "url": "https://sandbox.api.sap.com/s4hanacloud/sap/opu/odata/sap/API_BUSINESS_PARTNER/"
    }
    ​

    It should look like this:

  3. In the /srv folder create a file named address-manager-service.js. It must have the exact same name as the sibling .cds service file. In that file paste the following code:
    const cds = require('@sap/cds');
    
    //here is the service implementation
    //here are the service handlers
    module.exports = cds.service.impl(async function () {
    
        //these are the entities from address-manager-service.cds file
        const { BusinessPartners, BusinessPartnerAddresses } = this.entities;
    
        //cds will connect to the external service API_BUSINESS_PARTNER
        //which is declared in package.json in the cds requires section
        const service = await cds.connect.to('API_BUSINESS_PARTNER');
    
        //this event handler is triggered when we call
        //GET http://localhost:4004/address-manager/BusinessPartners
        this.on('READ', BusinessPartners, async (req) => {
            try {
                const tx = service.transaction();
    
                //entity name as it is in the .csn file for the service API_BUSINESS_PARTNER
                let entity = 'A_BusinessPartner';
                //columns which we have declared in cds entity that we want to expose
                let columnsToSelect = ["BusinessPartner", "FirstName", "LastName"];
    
                return await tx.emit({
                    query: SELECT.from(entity)
                        .columns(columnsToSelect),
                    //For API Business Hub usage, we send custom APIKey header
                    headers: {
                        "APIKey": process.env.S4_APIKEY
                    }
                })
    
            } catch (err) {
                req.reject(err);
            }
        });
    
        //this event handler is triggered when we call
        //GET http://localhost:4004/address-manager/BusinessPartnerAddresses
        this.on('READ', BusinessPartnerAddresses, async (req) => {
            try {
                const tx = service.transaction();
    
                //entity name as it is in the .csn file for the service API_BUSINESS_PARTNER
                let entity = 'A_BusinessPartnerAddress';
                //columns which we have declared in cds entity that we want to expose
                let columnsToSelect = ["BusinessPartner", "AddressID", "Country", "PostalCode", "CityName", "StreetName", "HouseNumber"];
    
                return await tx.emit({
                    query: SELECT.from(entity)
                        .columns(columnsToSelect),
                    //For API Business Hub usage, we send custom APIKey header
                    headers: {
                        "APIKey": process.env.S4_APIKEY
                    }
                })
    
            } catch (err) {
                req.reject(err);
            }
        });
    
    });
  4. Since we are reading the APIKey from environment variables we need to declare in Terminal by running:
    $env:S4_APIKEY=">>>YOUR API KEY<<<"​
  5. Go to API Business Hub, Log In, go in the Business Partner A2X, click on Show API Key and copy the value. Paste it between the double quotes and press Enter.
    $env:S4_APIKEY="8JLQURhRAeEH8OHgXFTXcfmW6H0vrW5V”​
  6. Start the app by running: npm start in the Terminal. When you access http://localhost:4004 and go to http://localhost:4004/address-manager/BusinessPartners instead of the mock data you will see data from API Business Hub sandbox.

Configure, build and deploy on SAP Cloud Platform

Steps to follow:

  1. We will be using a Destination to access the SAP S/4HANA Cloud system. For accessing that Destination configuration, we need an Authorization & Trust Management (XSUAA) and a Destination service instance to be bound to the deployed application.
  2. Go to your SAP Cloud Platform subaccount to create the Destination configuration. At subaccount level, expand Connectivity and click on Destinations. Then Click on New Destination. Use the URL to the Business Partner OData Service: https://my306116-api.s4hana.ondemand.com/sap/opu/odata/sap/API_BUSINESS_PARTNER. For User and Password use the communication user and password.
  3. Now open package.json file and change the “credentials” object to:
    "credentials": {
      "destination": "s4hc"
    }
  4. Since we will be using the the SAP S/4HANA Cloud Destination to read Business Partners, we do not need to send custom headers anymore. This means that we can change tx.emit() to tx.run() and delete the headers object and put the query directly inside. Go in the address-manager-service.js file and make these changes:
    const cds = require('@sap/cds');
    
    //here is the service implementation
    //here are the service handlers
    module.exports = cds.service.impl(async function () {
    
        //these are the entities from address-manager-service.cds file
        const { BusinessPartners, BusinessPartnerAddresses } = this.entities;
    
        //cds will connect to the external service API_BUSINESS_PARTNER
        //which is declared in package.json in the cds requires section
        const service = await cds.connect.to('API_BUSINESS_PARTNER');
    
        //this event handler is triggered when we call
        //GET http://localhost:4004/address-manager/BusinessPartners
        this.on('READ', BusinessPartners, async (req) => {
            try {
                const tx = service.transaction();
    
                //entity name as it is in the .csn file for the service API_BUSINESS_PARTNER
                let entity = 'A_BusinessPartner';
                //columns which we have declared in cds entity that we want to expose
                let columnsToSelect = ["BusinessPartner", "FirstName", "LastName"];
    
                return await tx.run(
                    SELECT.from(entity)
                        .columns(columnsToSelect)
                )
    
            } catch (err) {
                req.reject(err);
            }
        });
    
        //this event handler is triggered when we call
        //GET http://localhost:4004/address-manager/BusinessPartnerAddresses
        this.on('READ', BusinessPartnerAddresses, async (req) => {
            try {
                const tx = service.transaction();
    
                //entity name as it is in the .csn file for the service API_BUSINESS_PARTNER
                let entity = 'A_BusinessPartnerAddress';
                //columns which we have declared in cds entity that we want to expose
                let columnsToSelect = ["BusinessPartner", "AddressID", "Country", "PostalCode", "CityName", "StreetName", "HouseNumber"];
    
                return await tx.run(
                    SELECT.from(entity)
                        .columns(columnsToSelect)
                )
    
            } catch (err) {
                req.reject(err);
            }
        });
    
    });​
  5. The XSUAA service instance uses a xs-security.json file for configuration. Generate this file by running in the Terminal the command: cds srv –to xsuaa > xs-security.json. This will create the file in the root folder.
  6. The application will be deployed as a Multi Target Application so that we can also create the services when doing this. To add the configuration file for the deployment, run in Terminal: cds add mta
  7. Open the mta.yaml file which was generated by cds and add the following lines. Be very careful with the indentation/spaces/tabs in the .yaml file as it is very sensible.
       requires:
         - name: uaa
         - name: dest
    
    resources:
    - name: uaa
      type: org.cloudfoundry.managed-service
      parameters:
        path: ./xs-security.json
        service: xsuaa
        service-name: my-uaa
        service-plan: application
    - name: dest
      type: org.cloudfoundry.managed-service
      parameters:
        service: destination
        service-name: my-dest
        service-plan: lite
  8. Open the package.json file and add the following scripts:
    "build": "mbt build -p=cf --mtar=AddressManager.mtar",
    "deploy": "cf deploy mta_archives/AddressManager.mtar"
  9. Open a Terminal and run the script: npm run build. This will take some time. When it is finished you can see a file under /mta_archives/AddressManager.mtar. This file will be used in the deployment.
  10. Log in to your space by running in Terminal: cf login.
  11. Run the script: npm run deploy. This will also take a while. But it will take care of deploying the app, creating the service instances and binding them to the app.
  12. You can now go to SAP Cloud Platform and access your app URL to see the app which is exposing SAP S/4HANA Cloud data.
  13. You can use the deployed application’s information when starting the local application. To do that start by creating a file in the root project folder with the name default-env.json with the following contents:
    {
        "VCAP_SERVICES": {…},
        "VCAP_APPLICATION": {…}
    }​
  14. Then run in Terminal the command: cf env cap-adman-srv and copy the values of VCAP_SERVICES and VCAP_APPLICATION in your default-env.json. Be careful to put in the correct format. Now run npm start. Go to http://localhost:4004 and you will see the data like in the deployed app.
  15. Let’s introduce also cds watch which will automatically detect changes. Go to package.json and add the following script:
    "watch": "npx cds watch"​
  16. Go also in the launch.json file and add a watch configuration like this:
    {
      "command": "cds watch",
      "name": "cds watch",
      "request": "launch",
      "type": "node-terminal",
      "skipFiles": [
        "<node_internals>/**"
      ]
    }
    ​

    You can also delete the env variable as we are now reading from the SAP S/4HANA Cloud system using the Destination.

Add OData V2 adapter and READ one entity

Steps to follow:

  1. In the /srv folder create a file server.js and paste the following code inside it:
    const proxy = require('@sap/cds-odata-v2-adapter-proxy')
    const cds = require('@sap/cds')
    cds.on('bootstrap', app => app.use(proxy()))
    module.exports = cds.server
  2. Open a Terminal and run: npm install @sap/cds-odata-v2-adapter-proxy –save
  3. Start your application with npm run watch. Access http://localhost:4004/address-manager/ which serves OData V4 and try to add /v2/ in the endpoint like this http://localhost:4004/v2/address-manager/ which servers OData V2. We will need the OData V2 service in the UI.
  4. We implemented a handler which READs all the BusinessPartners, but if we try http://localhost:4004/v2/address-manager/BusinessPartners(‘1000000’) this will not work yet because it is missing the JavaScript implementation for this. Go to address-manager-service.js and modify the code like this:
    const cds = require('@sap/cds');
    
    //here is the service implementation
    //here are the service handlers
    module.exports = cds.service.impl(async function () {
    
        //these are the entities from address-manager-service.cds file
        const { BusinessPartners, BusinessPartnerAddresses } = this.entities;
    
        //cds will connect to the external service API_BUSINESS_PARTNER
        //which is declared in package.json in the cds requires section
        const service = await cds.connect.to('API_BUSINESS_PARTNER');
    
        //this event handler is triggered when we call
        //GET http://localhost:4004/address-manager/BusinessPartners
        this.on('READ', BusinessPartners, async (req) => {
            try {
                const tx = service.transaction();
    
                //entity name as it is in the .csn file for the service API_BUSINESS_PARTNER
                let entity = 'A_BusinessPartner';
                //columns which we have declared in cds entity that we want to expose
                let columnsToSelect = ["BusinessPartner", "FirstName", "LastName"];
    
                //if there is a parameter
                if (req.params[0]) {
                    //If you look in the .csn file you will see that
                    //the key for our BusinessPartner entity is BusinessPartner column
                    const businessPartner = req.params[0].BusinessPartner;
                    return await tx.run(
                        SELECT.from(entity)
                            .columns(columnsToSelect)
                            .where({ 'BusinessPartner': businessPartner })
                    )
                } else {
                    //if no parameter, we read all Business Partners
                    return await tx.run(
                        SELECT.from(entity)
                            .columns(columnsToSelect)
                    )
                }
    
            } catch (err) {
                req.reject(err);
            }
        });
    
        //this event handler is triggered when we call
        //GET http://localhost:4004/address-manager/BusinessPartnerAddresses
        this.on('READ', BusinessPartnerAddresses, async (req) => {
            try {
                const tx = service.transaction();
    
                //entity name as it is in the .csn file for the service API_BUSINESS_PARTNER
                let entity = 'A_BusinessPartnerAddress';
                //columns which we have declared in cds entity that we want to expose
                let columnsToSelect = ["BusinessPartner", "AddressID", "Country", "PostalCode", "CityName", "StreetName", "HouseNumber"];
    
                //if there is parameter
                if (req.params[0]) {
                    //If you look in the .csn file you will see that
                    //the keys for our BusinessPartnerAddress entity are
                    //BusinessPartner and AddressID columns
                    const businessPartner = req.params[0].BusinessPartner;
                    const addressId = req.params[0].AddressID;
                    return await tx.run(
                        SELECT.from(entity)
                            .columns(columnsToSelect)
                            .where({
                                'BusinessPartner': businessPartner,
                                'AddressID': addressId
                            })
                    )
                } else {
                    //if no parameter, we read all Business Partner Addresses
                    return await tx.run(
                        SELECT.from(entity)
                            .columns(columnsToSelect)
                    )
                }
    
            } catch (err) {
                req.reject(err);
            }
        });
    
    });​
  5. Start the app from Debug panel in watch mode, put some breakpoints and look at the execution of the code while you access:

Lessons learned

During the development, the part with sending the APIKey needed some research because I initially did not know how to do it. I finally found the solution in this GitHub repository with questions and issues about CAP which I find extremely helpful when you are trying to learn how to use CAP because there you can find lots of answers and solutions.

Final words

Now you know how to:

  • develop the CAP application
  • configure, build & deploy to SAP Cloud Platform

Before starting the implementation of the app, I looked for a blog post that covers this technical scenario but I only found some which were similar and extremely helpful to me, so I will mention them and express many thanks to the authors:

Please feel free and I highly encourage anyone to share more knowledge about this topic or suggest better ways, give feedback and contribute to building a starter scenario for extension applications using JavaScript.

Part 2 is published

In the second part I will cover how to:

  • add Create, Update, Delete service handlers using SAP Cloud SDK
  • generate a Fiori app using Fiori Tools
  • add annotations in the CAP application
5 Comments
You must be Logged on to comment or reply to a post.