Skip to Content
Technical Articles

CAP: Consume External Service – Part 2

In my previous blog post Consume External Service – Part 1, I have tested the application using mock and real data. Just by configuring the NorthWind OData service URL in the credentials.url property, I was able to connect to the external service. This approach is only applicable for local development testing. If you try to deploy this into SCP Cloud Foundry, you will encounter an error stating that this approach is not valid for production use.

The reason behind this is that external service consumption in this CDS framework is meant to use the Destination Service in Cloud Foundry. And this is the topic of this blog — deploying the Node.js project into SCP Cloud Foundry using Destination Service.

 

 

Prerequisites


 

Setup Destination Configuration


First of all, why do we need to setup a destination? A simple explanation is that it is best practice to avoid hard coding the external services that you use in your application inside your code. It is better for it to be configured separately from your application because as you deploy your application into different environment like DEV, QA, or PROD, you can have a different setup of the destination per environment. Also, it keeps the sensitive information like credentials outside of your code.

  • 1. Create a Destination Service Instance in Cloud Foundry

Service Name: demo-destination
Plan: lite
  • 2. Configure a new destination

You can also test the connection if the destination can be reached. A successful test will yield a message like the one below:

Connection to "NorthWind" established. Response returned: "307: Temporary Redirect"

That’s it! We have completed the setup of our destination.

 

Deploy the application to SCP Cloud Foundry


Now let’s go back to our Node.js project — if you were able to follow through my previous blog post, then your project will look exactly like the one I have below:

https://github.com/jcailan/azure-devops/tree/cap-es-part1

The next step is to prepare the application for deployment to SCP Cloud Foundry.

  • 1. Generate the mta.yaml file using the command below:
> cds add mta

  • 2. Generate security descriptor file xs-security.json using the command below:
> cds compile srv/ --to xsuaa > xs-security.json

For our case, we can ignore the warning shown in the terminal.

  • 3. Update mta.yaml file with declaration of xsuaa and destination resources and then binding it to our node module demo-srv.
modules:
 # --------------------- SERVER MODULE ------------------------
 - name: demo-srv
 # ------------------------------------------------------------
   type: nodejs
   path: gen/srv
   properties:
     EXIT: 1  # required by deploy.js task to terminate 
   requires:
     - name: demo-destination
     - name: demo-uaa


resources:
 - name: demo-destination
   type: org.cloudfoundry.existing-service
   parameters:
     service-name: demo-destination

 - name: demo-uaa
   type: org.cloudfoundry.managed-service
   parameters:
     path: ./xs-security.json
     service: xsuaa
     service-plan: application  

Note that in order for the destination service consumption to work, an xsuaa service is required to be bound to our application.

  • 4. Update the package.json cds configurations to use the NorthWind destination we have configured in SCP Cloud Foundry:
	"cds": {
		"requires": {
			"NorthWind": {
				"kind": "odata",
				"model": "srv/external/NorthWind",
				"credentials": {
					"destination": "NorthWind"
				}
			}
		}
	}
  • 5. Build the MTA Project by using the command:
> mbt build

Make sure you have saved all the files changes we did before doing the build.

An MTA archive file will be generated in the mta_archives folder.

  • 6. Deploy the MTA archive file into SCP Cloud Foundry using the command:
> cf deploy mta_archives/demo_1.0.0.mtar
  • 7. Once the deployment has completed, look out for the terminal logs which states the URL of your demo-srv module. In my case, here’s the generated URL:
s0017687913trial-dev-demo-srv.cfapps.eu10.hana.ondemand.com

 

Test the deployed Node.js app service


  • 1. Open the URL we got from the previous step using your favourite  browser, the initial page will show, and now click on the Products entity. You should see the result below:

As you can see, it is very easy to deploy the app and use the destination service. We don’t really need to do any additional JavaScript coding, all that we did is just do a little bit of configuration.

 

Testing the Node.js app locally


Now that we have deployed our application into SCP Cloud Foundry, we don’t really need to change the configuration back to a hardcoded external service URL just to test the app locally. We can test the app locally while still using the destination service in SCP Cloud Foundry. In order to be able to test locally, we need to capture the environment variable assigned to our Node.js module in SCP.

  • 1. Get the VCAP_SERVICES environment variable settings for demo-srv module

Sample data structure to capture:

{
	"VCAP_SERVICES": {
		"xsuaa": [
			{
				"label": "xsuaa",
				"provider": null,
				"plan": "application",
				"name": "demo-uaa",
				"tags": [
					"xsuaa"
				],
				"instance_name": "demo-uaa",
				"binding_name": null,
				"credentials": {
					"tenantmode": "dedicated",
					"sburl": "https://internal-xsuaa.authentication.eu10.hana.ondemand.com",
					"clientid": "<<clientid>>",
					"xsappname": "demo!t48303",
					"clientsecret": "<<clientsecret>>",
					"url": "https://myaccount.authentication.eu10.hana.ondemand.com",
					"uaadomain": "authentication.eu10.hana.ondemand.com",
					"verificationkey": "<<key>>",
					"apiurl": "https://api.authentication.eu10.hana.ondemand.com",
					"identityzone": "myaccount",
					"identityzoneid": "3779d905-0123-45f2-a66d-e5c3a78cb1c4",
					"tenantid": "3779d905-0123-45f2-a66d-e5c3a78cb1c4"
				},
				"syslog_drain_url": null,
				"volume_mounts": []
			}
		],
		"destination": [
			{
				"label": "destination",
				"provider": null,
				"plan": "lite",
				"name": "demo-destination",
				"tags": [
					"destination",
					"conn",
					"connsvc"
				],
				"instance_name": "demo-destination",
				"binding_name": null,
				"credentials": {
					"uaadomain": "authentication.eu10.hana.ondemand.com",
					"tenantmode": "dedicated",
					"clientid": "<<clientid>>",
					"instanceid": "a82d0b2d-0119-4c09-97ee-46793ad8dba2",
					"verificationkey": "<<keyu>>",
					"xsappname": "clonea82d0b2d01194c0997ee46793ad8dba2!b48303|destination-xsappname!b404",
					"identityzone": "myaccount",
					"clientsecret": "<<clientsecret>>",
					"tenantid": "3779d905-0123-45f2-a66d-e5c3a78cb1c4",
					"uri": "https://destination-configuration.cfapps.eu10.hana.ondemand.com",
					"url": "https://myaccount.authentication.eu10.hana.ondemand.com"
				},
				"syslog_drain_url": null,
				"volume_mounts": []
			}
		]
	}
}

Note that the information contained in VCAP_SERVICES environment variable have credential information and shouldn’t committed in your git repository.

  • 2. Create a file called default-env.json in the root of your project directory, and paste here the data you captured from SCP. If you are using the cds template project using the command cds init, then it has generated a .gitignore file that will automatically ignore default-env.json file we are creating here.
  • 3. Next thing to do is start the app locally using the command:
> cds watch
  • 4. Test the app on a browser by using the URL below:
http://localhost:4004/catalog/Products

As you can see we now can test the app locally using real data while also using the destination service of SCP Cloud Foundry.

If by any chance you still want to revert to testing the app using our mock data, you can still do so, you just need to amend the cds configuration in package.json to make the credentials section invalid. See below example:

	"cds": {
		"requires": {
			"NorthWind": {
				"kind": "odata",
				"model": "srv/external/NorthWind",
				"--credentials": {
					"destination": "NorthWind"
				}
			}
		}
	}

Save the changes and refresh your browser, then you will see the mock data is available again.

Closing


Now you know how easy it is to consume an external service using CAP Model and use Destination Service to manage the connection configuration. We also tested our application in three different ways:

  • Test the service directly in SCP Cloud Foundry
  • Test the service locally while still using SCP CF Destination Service
  • Test the service locally using mock data

Take note that mocking the data for your service is essential for automated unit testing, but this is another topic better dealt with over another blog.

UPDATE:

Taking this project further into — Unit Testing using Mocha and Chai

 

~~~~~~~~~~~~~~~~

Appreciate it if you have any comments, suggestions, or questions. Cheers!~

 

16 Comments
You must be Logged on to comment or reply to a post.
  • Hi Jhodel, Im having troubles making the call to the destination it always prints a 500 erro with a 400 status code in the message. Also it says about not finding a proccess env called HTTPS_PROXY and then it fails the service run.

    Don’t know whats happening, and I followed your blog perfectly.

    Thanks Jhodel Cailan

    /
  • Hi Jhodel,

    Very useful post, I followed your steps and works fine, but now I want to replay this for a S/4HANA  service, but I always get this error:

    {"message":"Attempting to retrieve destination from environment variable.","level":"info","custom_fields":{"package":"core","messageContext":"destination-accessor"},"logger":"sap-cloud-sdk-logger","timestamp":"2020-06-09T17:25:02.076Z","msg":"Attempting to retrieve destination from environment variable.","written_ts":1591723502076,"written_at":"2020-06-09T17:25:02.076Z"}
    {"message":"Could not retrieve destination from environment variable.","level":"info","custom_fields":{"package":"core","messageContext":"destination-accessor"},"logger":"sap-cloud-sdk-logger","timestamp":"2020-06-09T17:25:02.078Z","msg":"Could not retrieve destination from environment variable.","written_ts":1591723502078,"written_at":"2020-06-09T17:25:02.078Z"}
    {"message":"Attempting to retrieve destination from service binding.","level":"info","custom_fields":{"package":"core","messageContext":"destination-accessor"},"logger":"sap-cloud-sdk-logger","timestamp":"2020-06-09T17:25:02.078Z","msg":"Attempting to retrieve destination from service binding.","written_ts":1591723502078,"written_at":"2020-06-09T17:25:02.078Z"}
    {"message":"Unable to find a service binding for given name \"SCP-TO-DEVVIRTUALNEO\"! Found the following bindings: s4cap-s4cap-uaa-S0019246072-workspKL8pDH6LAjq+PI9x, s4cap-conns4cap-S0019246072-workspOeKcqQC3TvHCyLFe, s4cap-s4cap-db-hdi-container-S0019dJBJdRo5DnLNI72P, s4cap-dests4cap-S0019246072-worksp6fvZ7pO3i0qk1mEo.\n ","level":"info","custom_fields":{"package":"core","messageContext":"destination-accessor"},"logger":"sap-cloud-sdk-logger","timestamp":"2020-06-09T17:25:02.079Z","msg":"Unable to find a service binding for given name \"SCP-TO-DEVVIRTUALNEO\"! Found the following bindings: s4cap-s4cap-uaa-S0019246072-workspKL8pDH6LAjq+PI9x, s4cap-conns4cap-S0019246072-workspOeKcqQC3TvHCyLFe, s4cap-s4cap-db-hdi-container-S0019dJBJdRo5DnLNI72P, s4cap-dests4cap-S0019246072-worksp6fvZ7pO3i0qk1mEo.\n ","written_ts":1591723502079,"written_at":"2020-06-09T17:25:02.079Z"}
    {"message":"Could not retrieve destination from service binding.","level":"info","custom_fields":{"package":"core","messageContext":"destination-accessor"},"logger":"sap-cloud-sdk-logger","timestamp":"2020-06-09T17:25:02.079Z","msg":"Could not retrieve destination from service binding.","written_ts":1591723502079,"written_at":"2020-06-09T17:25:02.079Z"}
    {"message":"If you are not using SAP Extension Factory, this information probably does not concern you.","level":"info","custom_fields":{"package":"core","messageContext":"destination-accessor"},"logger":"sap-cloud-sdk-logger","timestamp":"2020-06-09T17:25:02.079Z","msg":"If you are not using SAP Extension Factory, this information probably does not concern you.","written_ts":1591723502079,"written_at":"2020-06-09T17:25:02.079Z"}
    {"message":"Attempting to retrieve destination from destination service.","level":"info","custom_fields":{"package":"core","messageContext":"destination-accessor"},"logger":"sap-cloud-sdk-logger","timestamp":"2020-06-09T17:25:02.080Z","msg":"Attempting to retrieve destination from destination service.","written_ts":1591723502080,"written_at":"2020-06-09T17:25:02.080Z"}
    {"message":"Unable to match a specific XSUAA service instance to the given JWT. The following XSUAA instances are bound: s4cap-SCPCFQAS91f91a19-69fd-4fcc-95de-a14f3e41a809!t4787. The following one will be selected: s4cap-SCPCFQAS91f91a19-69fd-4fcc-95de-a14f3e41a809!t4787. This might produce errors in other parts of the system!","level":"warn","custom_fields":{"package":"core","messageContext":"environment-accessor"},"logger":"sap-cloud-sdk-logger","timestamp":"2020-06-09T17:25:02.081Z","msg":"Unable to match a specific XSUAA service instance to the given JWT. The following XSUAA instances are bound: s4cap-SCPCFQAS91f91a19-69fd-4fcc-95de-a14f3e41a809!t4787. The following one will be selected: s4cap-SCPCFQAS91f91a19-69fd-4fcc-95de-a14f3e41a809!t4787. This might produce errors in other parts of the system!","written_ts":1591723502081,"written_at":"2020-06-09T17:25:02.081Z"}
    {"message":"Sucessfully retrieved destination from destination service.","level":"info","custom_fields":{"package":"core","messageContext":"destination-accessor"},"logger":"sap-cloud-sdk-logger","timestamp":"2020-06-09T17:25:02.365Z","msg":"Sucessfully retrieved destination from destination service.","written_ts":1591723502365,"written_at":"2020-06-09T17:25:02.365Z"}
    {"message":"OnPrem destination proxy settings from connectivity service will be used.","level":"info","custom_fields":{"package":"core","messageContext":"proxy-util"},"logger":"sap-cloud-sdk-logger","timestamp":"2020-06-09T17:25:02.365Z","msg":"OnPrem destination proxy settings from connectivity service will be used.","written_ts":1591723502365,"written_at":"2020-06-09T17:25:02.365Z"}
    {"message":"Unable to match a specific XSUAA service instance to the given JWT. The following XSUAA instances are bound: s4cap-SCPCFQAS91f91a19-69fd-4fcc-95de-a14f3e41a809!t4787. The following one will be selected: s4cap-SCPCFQAS91f91a19-69fd-4fcc-95de-a14f3e41a809!t4787. This might produce errors in other parts of the system!","level":"warn","custom_fields":{"package":"core","messageContext":"environment-accessor"},"logger":"sap-cloud-sdk-logger","timestamp":"2020-06-09T17:25:02.366Z","msg":"Unable to match a specific XSUAA service instance to the given JWT. The following XSUAA instances are bound: s4cap-SCPCFQAS91f91a19-69fd-4fcc-95de-a14f3e41a809!t4787. The following one will be selected: s4cap-SCPCFQAS91f91a19-69fd-4fcc-95de-a14f3e41a809!t4787. This might produce errors in other parts of the system!","written_ts":1591723502366,"written_at":"2020-06-09T17:25:02.366Z"}
    {"message":"Unable to create \"SAP-Connectivity-Authentication\" header: no JWT found on the current request.\n Continuing without header. Connecting to on-premise systems may not be possible.","level":"warn","custom_fields":{"package":"core","messageContext":"connectivity-service"},"logger":"sap-cloud-sdk-logger","timestamp":"2020-06-09T17:25:02.538Z","msg":"Unable to create \"SAP-Connectivity-Authentication\" header: no JWT found on the current request.\n Continuing without header. Connecting to on-premise systems may not be possible.","written_ts":1591723502538,"written_at":"2020-06-09T17:25:02.538Z"}
    {"message":"OnPrem destination proxy settings from connectivity service will be used.","level":"info","custom_fields":{"package":"core","messageContext":"proxy-util"},"logger":"sap-cloud-sdk-logger","timestamp":"2020-06-09T17:25:02.539Z","msg":"OnPrem destination proxy settings from connectivity service will be used.","written_ts":1591723502539,"written_at":"2020-06-09T17:25:02.539Z"}
    [2020-06-09T17:25:02.681Z | ERROR | 1003549]: Request failed with status code 403
    [2020-06-09T17:25:02.681Z | ERROR | 1003549]: Error stacktrace: Error: Request failed with status code 403 at createError (/home/vcap/app/node_modules/axios/lib/core/createError.js:16:15) at settle (/home/vcap/app/node_modules/axios/lib/core/settle.js:17:12) at IncomingMessage.handleStreamEnd (/home/vcap/app/node_modules/axios/lib/adapters/http.js:236:11) at IncomingMessage.emit (events.js:323:22) at endReadableNT (_stream_readable.js:1204:12) at processTicksAndRejections (internal/process/task_queues.js:84:21)

    This is my mta.yaml

    ID: s4cap
    _schema-version: '2.1'
    parameters:
      deploy_mode: html5-repo
    version: 0.0.1
    modules:
      - name: s4cap-approuter
        type: approuter.nodejs
        path: s4cap-approuter
        parameters:
          disk-quota: 256M
          memory: 256M
        requires:
          - name: s4cap_html5_repo_runtime
          - name: dest_s4cap
          - name: conn_s4cap
          - name: s4cap-uaa
          - name: srv_api
            group: destinations
            properties:
              forwardAuthToken: true
              name: srv_api
              url: '~{url}'
      - name: s4cap_ui_deployer
        type: com.sap.html5.application-content
        path: s4cap_ui_deployer
        requires:
          - name: s4cap_html5_repo_host
        build-parameters:
          requires:
            - name: s4capui
              artifacts:
                - './*'
              target-path: resources/s4capui
      - name: s4cap-db
        type: hdb
        path: db
        parameters:
          memory: 256M
          disk-quota: 512M
        requires:
          - name: s4cap-db-hdi-container
      - name: s4cap-srv
        type: nodejs
        path: srv
        parameters:
          memory: 512M
          disk-quota: 512M
        provides:
          - name: srv_api
            properties:
              url: '${default-url}'
        requires:
          - name: s4cap-db-hdi-container
          - name: s4cap-uaa
          - name: dest_s4cap
          - name: conn_s4cap
      - name: s4capui
        type: html5
        path: s4capui
        build-parameters:
          builder: custom
          commands:
            - npm install
            - npm run build
          supported-platforms: []
          build-result: dist
    resources:
      - name: s4cap_html5_repo_runtime
        parameters:
          service-plan: app-runtime
          service: html5-apps-repo
        type: org.cloudfoundry.managed-service
      - name: s4cap_html5_repo_host
        parameters:
          service-plan: app-host
          service: html5-apps-repo
        type: org.cloudfoundry.managed-service
      - name: s4cap-db-hdi-container
        type: com.sap.xs.hdi-container
        properties:
          hdi-container-name: '${service-name}'
      - name: s4cap-uaa
        type: org.cloudfoundry.managed-service
        parameters:
          service-plan: application
          service: xsuaa
          config:
            xsappname: 's4cap-${space}'
            tenant-mode: dedicated
          path: xs-security.json
      - name: dest_s4cap
        parameters:
          service-plan: lite
          service: destination
        type: org.cloudfoundry.managed-service
      - name: conn_s4cap
        parameters:
          service-plan: lite
          service: connectivity
        type: org.cloudfoundry.managed-service

    and my package.json

    {
    	"name": "s4cap-srv",
    	"description": "Generated from ../package.json, do not change!",
    	"version": "1.0.0",
    	"dependencies": {
    		"@sap/cloud-sdk-core": "^1.17.2",
    		"@sap/hana-client": "^2.4.196",
    		"@sap/xsenv": "^2.2.0",
    		"@sap/xssec": "^2.2.5",
    		"@sap/cds": "^3.34.2",
    		"passport": "^0.4.1",
    		"express": "^4.17.1",
    		"axios": "^0.19.2",
    		"moment": "^2.24.0",
    		"moment-timezone": "^0.5.28",
    		"execution-time": "^1.4.1",
    		"hdb": "^0.17.1",
    		"uuid": "^3.4.0"
    
    	},
    	"engines": {
    		"node": "^10 || ^12"
    	},
    	"devDependencies": {},
    	"scripts": {
    		"postinstall": "npm dedupe && node .build.js",
    		"start": "node ./node_modules/@sap/cds/bin/cds.js serve gen/csn.json",
    		"watch": "nodemon -w . -i node_modules/**,.git/** -e cds -x npm run build",
    		"start:express": "node --inspect express.js"
    	},
    	"private": true,
    	"cds": {
    		"requires": {
    			"db": {
    				"kind": "hana",
    				"model": "gen/csn.json"
    			},
    			"uaa": {
    				"kind": "xsuaa"
    			},
    			"Z_OD_SCP_CORE_0001_SRV": {
    				"kind": "odata",
    				"model": "srv/external/Z_OD_SCP_CORE_0001_SRV",
    				"credentials": {
    					"destination": "SCP-TO-DEVVIRTUALNEO",
    					"requestTimeout": 30000
    				},
    				"pool": {
    					"min": 1,
    					"max": 10
    				}
    			},
    			"Z_OD_SCP_VEHI_SRV": {
    				"kind": "odata",
    				"model": "srv/external/Z_OD_SCP_VEHI_SRV",
    				"credentials": {
    					"destination": "SCP-TO-DEVVIRTUALNEO",
    					"requestTimeout": 30000
    				},
    				"pool": {
    					"min": 1,
    					"max": 10
    				}
    			},
    			"ZCDS_VW_TEST_CDS": {
    				"kind": "odata",
    				"model": "srv/external/ZCDS_VW_TEST_CDS",
    				"credentials": {
    					"destination": "SCP-TO-DEVVIRTUALNEO",
    					"requestTimeout": 30000
    				},
    				"pool": {
    					"min": 1,
    					"max": 10
    				}
    			},
    			"NorthWind": {
    				"kind": "odata",
    				"model": "srv/external/NorthWind",
    				"credentials": {
    					"destination": "NorthWind",
    					"requestTimeout": 30000
    				},
    				"pool": {
    					"min": 1,
    					"max": 10
    				}
    			}
    		},
    		"auth": {
    			"passport": {
    				"strategy": "JWT"
    			}
    		},
    		"odata": {
    			"version": "v4"
    		}
    	}
    }

    The destination is been using without problems by other apps, but with this method using external services in CDS is not working.

    Thanks in advance

  • Hi again Jhodel,

    I noticed that you cannot do expands with a remote service like this although you can do filters.

    It’s just me or its the same on all people ?

    Thanks for the great post 😉

    /
    • Thanks Jose! Yes, it’s the framework issue 😅. It is a limitation at the moment. If you really need this feature then you have to code it yourself — without the use of CDS fluent API.

      Cheers! Jhodel

  • Thanks, Excellent Blog, worked perfectly.

    I want to expose few more entities .  I did below changes and it is not working. What should I code more ?

    using {NorthWind as external} from ‘./external/NorthWind.csn’;
    service CatalogService {
        @readonly
        entity Products as projection on external.Products {
            key ID, Name, Description, ReleaseDate, DiscontinuedDate, Rating, Price
        };
        @readonly
        entity Productdetails as projection on external.ProductDetails;
    }
    ————-
    const cds = require(‘@sap/cds’);
    module.exports = cds.service.impl(async function() {
        const { Products,Productdetails } = this.entities;
        const service = await cds.connect.to(‘NorthWind’);
        this.on(‘READ’, Products, request => {
            return service.tx(request).run(request.query);
        });
        this.on(‘READ’, Productdetails, request => {
            return service.tx(request).run(request.query);
        });
    });
    • Thanks Karthiheyan !!

      Is it not working because you got an error? or because it doesn’t show any data? I checked your code and it looks fine. You should just create a file called NorthWind-ProductDetails.csv which has a similar data like below:

      ProductID;Details
      1;Details of product 1
      2;Details of product 2
      • Hi,

        It is working locally. But after deploy to CF , it is giving below error,

        Request failed with status code 400  – error after clicking on Entityset in CF link.

         

        • Hi ,

          It worked after specifying field names like ID,Name

              @readonly
              entity Suppliers as projection on external.Suppliers{
                  key ID,Name
              };