SAP Cloud Application Programming Model Getting Started (CAP)

Aus MattWiki

This article describes how to get started with building Cloud Application Programming Model (CAP) projects and according artefacts with Core Data Services (CDS).

For local native development see SAP HANA Local Native Development

Sources and Further Reading

Description URL
All-in-one Quick Start https://github.com/SAP-samples/cloud-cap-walkthroughs/blob/master/exercises-node/intro/README.md
Back to basics SAP Cloud Application Programming Model (CAP) Playlist https://www.youtube.com/playlist?list=PL6RpkC85SLQBHPdfHQ0Ry2TMdsT-muECx
Github Repository for Back to basics Playlist https://github.com/qmacro/capb2b
Qmacro / DJ Adams https://qmacro.org/tags/cap/
Qmacro @ SAP Community https://community.sap.com/t5/user/viewprofilepage/user-id/53
SAP CAP Documentation https://cap.cloud.sap/
SAP CAP Sample Repositories (Use opensap*-Branches) https://github.com/SAP-samples/cloud-cap-samples
GitHub Repositories of CAP and Fiori Showcases https://github.com/SAP-samples/cap-sflighthttps://github.com/SAP-samples/fiori-elements-feature-showcase
ABAP Freak Show Ep. 1 - HANA Cloud and the Business Application Studio https://www.youtube.com/watch?v=a3WPQwmpbvI&list=PLoc6uc3ML1JR38-V46qhmGIKG07nXwO6X&index=71

Installation of Prerequisites

Relevant Tools from SAP

npm i -g @sap/cds
npm i -g @sap/cds-dk

Additional 3rd Party Tools

npm i -g hana-cli

Setup of CAP Projects

Create CDS Project

cds init bookshop

or

cds init MyCDSProject --add hana, mta

Download and install dependencies:

npm install

Start Service

cds watch
cds w

This also monitors changes to the underlying files and restarts when a file has changed.

Prevent Reloading

To prevent reloading the service, for example when saving temporary files in the project directory, save the files in one of the directoryies which are excluded from monitoring for file changes, which are:

  • _out
  • node_modules
  • @types
  • @cds-models

For more details see: https://qmacro.org/blog/posts/2024/04/10/avoid-design-time-cap-server-restarts-when-maintaining-local-data-files/

Add Persistency to CDS

In case of SQLite first install SQLite3 packages by executing in the root of the project folder:

npm i sqlite3 -D

Deploy data model to different database types:

cds deploy

This deploys the cds entity models and csv files to the database specified in package.json in section cds.requires.db. This should look like this:

{ "cds": 
    { "requires": {
       "db": {
          "kind": "sqlite",
        "credentials": { "url": "db.sqlite" } 
            }
        }
    }
}

For more explainations see for SQLite see https://cap.cloud.sap/docs/guides/databases-sqlite

cds deploy --to sqlite          # Deploys to sqlite.db
cds deploy --to sqlite:my.db    # Deploys to my.db
cds deploy --to hana            # Deploys to HDI container on HANA
                                # (Requires Cloud Foundry login)    

This does not update the package.json secion cds.requires.db any more as opposed to older cds versions.

In order to see the actual created SQL statements:

cds compile srv/cat-service.cds --to sql     # Creates SQL statements
cds compile srv/cat-service.cds --to hana    # Creates hdbcds or hdbtable/hdbview artefacts

When deployed to SQLite database use this statement to view the database:

sqlite3 my.db -cmd .dump

Create CSV header files in db location (by default db/data) for modeled entities:

cds add data

Deployment to SAP Business Technology Platform (BTP)

Login to CF space:

cf login

Build Deployment Package

cds add hana

package.json needs to contain "kind": "hana". (really? - for which CF / HANA Cloud Version?)

Also when deploying to BTP which is internally HANA 4.0 "deploy-format": "hdbtable" has to be set and hdi-deploy should be at least version 4. Example:

{
    ...
    "devDependencies": {
        "@sap/hdi-deploy": "^4"
    },
    ...
    "cds": {
        "hana": {
            "deploy-format": "hdbtable"
        },
        "requires": {
            "db": {
                "model": [
                    "db",
                    "srv"
                ],
                "kind": "hana"
            }
        }
    }
}

Deploy via Cloud Foundry CLI

Build the deployment artifacts:

cds build/all
cds build --production         # Only production profile?

Create a hana service for the hdi-container. The stated hdi-container name must correlate to the services-name in /gen/db/src/manifest.yaml.

cf create-service hana hdi-shared <app-db-hdi-container>

Now push the DB deployer application and also the actual service application:

cf push -f gen/db
cf push -f gen/srv --random-route

Deploy via MTA

Install MTA Build Tool

npm install -g mbt

Generate MTA project descriptor file mta.yaml and build MTA archive:

cds add mta
mbt build

Deploy MTA archive to CF space:

cf deploy mta_archives/<buildmta>.mtar

Troubleshooting

Real-time output of CF services can be displayed in BAS terminal with

cf logs <appname>

CDS Commands Cheatsheet

Noteworthy Commands

Command Description
cds env Displays the effective configuration...
cds env ls ... in .properties format
cds compile services.cds Compile JSON-like database structure
cds compile services.cds | jq dto, but in actual JSON
cds compile services.cds --to sql Compile SQL statements from CDS
cds compile services.cds --to edmx Compile EDMX file (metadata) from CDS
cds compile services.cds --to csn Output CSN = Internal CDS Schema Notation...
cds compile services.cds --to csn | jq '.definitions | keys' ... and process output with JSON processor by extracting keys of definition nodes
cds compile schema.cds --to yaml Output Yaml of database schema
cds compile services.cds --to yaml Output Yaml of all services including their db schema
cds add data Create CSV header files in db location (by default db/data) for modeled entities

Using CDS REPL & JavaScript APIs

cds r
cds repl

This launches an read-eval-print-loop which can be used as an interactive playground to experiment with CDS' JavaScript APIs.

API Description
cds.utils.uuid() Returns an UUID
await cds.test() Start CDS server in the background and run the test() function

The following API requests need first running the following comand in the repl:

await cds.test()
API Description
for (x in cds.entities) console.log(x) Log to console the Names of entities (needs await cds.test() started before)
cds.entities['Books'] Return the entity definition based on db/schema.cds
await SELECT.from(cds.entities['Books']) Returns the JSON representing the results from the Books entities implicitly based on the schema level, which means, that no CQL from service level (aka the cds file in the srv folder) is processed
await SELECT.from(cds.entities('org.qmacro')['Books']) dto., but this time with explicitly specifying a namespace. 'org.qmacro' in this case is the schema level name space coming from the cds file in the db folder.
await SELECT.from(cds.entities('bookshop')['Books']) This explicitly specifies the bookshop namespace, which is the namespace from the service level in the cds file in the srv folder. So every coding like CQL in it will be processed too.

Accessing OData Services

For further details on OData Services see OData Cheat Sheet (CAP)


Custom Application Logic Within CAP

Custom Event Handlers

Create a .js-file with the same name as the service definition in the same subdirectory as the service definition file.

Example: srv/main.js file for srv/main.cds service definition:

console.log("Hello, World!")

const cds = require('@sap/cds')
module.exports = cds.service.impl(function () {
    console.log("I am in the anonymous function")
    this.on('READ', 'Books', () => {
        console.log("Handling READ of Books");
    })
})

This will fire on the READ handler of the Books entity.

For more details see: https://cap.cloud.sap/docs/guides/providing-services#custom-event-handlers

Custom Server

The following section is a example for auto reloading after a server has properly restarted to execute the command explained in section Continuous Reading from Services after File Change above.

Create a server.js file:

const fs = require('node:fs/promises')
require('@sap/cds').on('listening', async () => {
    await fs.writeFile('listening', '')
})

This creates a file called listening after the server restart has happened. We use this a a trigger to read after a file change:

ls listening | entr -c bash -c 'curl -s localhost:4004/odata/v4/bookshop/Orders | jq .'

For more details see:

https://cap.cloud.sap/docs/node.js/cds-server#custom-server-js

https://qmacro.org/blog/posts/2024/05/03/controlling-automatic-http-requests-in-cap-node.js-design-time-loops/

Custom Logger

Replace console.log with a custom logger:

const cds = require('@sap/cds')
const logger = cds.log('mylog')

module.exports = cds.service.impl(function () {
    logger("Hello World")
    this.after('READ', 'Books', (data, req) => {
      logger(data);
    })
})

In line 2 a logger is defined and in lines 5 and 7 utilized. This leads to custom labled output in the console like in lines 1 and 11:

[mylog] - Hello World
[cds] - using auth strategy { kind: 'mocked', impl: 'node_modules/@sap/cds/lib/auth/basic-auth' } 

[cds] - serving bookshop { impl: 'srv/main.js', path: '/odata/v4/bookshop' }

[cds] - server listening on { url: 'http://localhost:4004' }
[cds] - launched at 6/1/2024, 2:27:09 PM, version: 7.7.2, in: 294.413ms
[cds] - [ terminate with ^C ]

[odata] - GET /odata/v4/bookshop/Books 
[mylog] - [
  {
    ID:...

That way log levels can be implemented too. For more details see:

https://cap.cloud.sap/docs/node.js/cds-log

Custom Actions / Functions

Based on the OData specification there are Actions and Functions, which can be bound and unbound:

  • Actions modify data in the server
  • Functions retrieve data
  • Unbound actions/functions are like plain unbound functions in JavaScript.
  • Bound actions/functions always receive the bound entity's primary key as implicit first argument, similar to this pointers in Java or JavaScript.

From CDS perspective unbound actions/functions are recommended, as they are easier to invoke.

For more details see https://cap.cloud.sap/docs/guides/providing-services#implementing-actions-functions

Example of unbound function:

module.exports = cds.service.impl(function () {
    ...
    this.on(totalStock, async () => {
        const result = await SELECT .one .from(Books) .columns('sum(stock) as total') 
        return result.total
    })
    ...
})

Example of bound function, in this case the stockValue function is bound to the Books entity:

module.exports = cds.service.impl(function () {
    ...
    
    //this.on('getStock','Foo', ({params:[id]}) => stocks[id])
    this.on('stockValue',Books, (req) => {
      return 42
    })
})

For examples on how to invoke this functions see OData Cheat Sheet (CAP)