Search

Extending SaaS Applications

How SaaS subscribers can extend SaaS applications on a tenant level.

Content

The cds-mtx Library

The cds-mtx library was published to npm.sap.com as @sap/cds-mtx.

Features:

  • Exposes tenant provisioning API, which can be consumed by SAP Cloud Platform’s subscription infrastructure for multi-tenant applications and services.
  • Exposes a REST API to get model data and to activate CDS extensions (tenant-specific)

Configuration

The cds-mtx library follows the CDS configuration method for the CAP Node.js stack. This means, configuration can be put into the package.json file, or into .cdsrc.json. The example below shows a typical .cdsrc.json file with cds-mtx relevant configuration:

{
    "mtx": {
        "element-prefix": ["Z_", "ZZ_"],
        "namespace-blacklist": ["com.sap.", "sap."],
        "entity-whitelist": ["my.bookshop.Books", "<another entity>", ...],
        "service-whitelist": ["AdminService", "<another service>", ...]
    },
    "odata": {"version": "v4"},
    "requires": {
        "db": {
            "kind": "hana",
            "model": ["db", "srv"],
            "multiTenant": true,
            "vcap": {"label": "managed-hana"}
        },
        "uaa": {
            "kind": "xsuaa"
        }
    }
}

In the mtx section, an array of allowed prefixes for CDS element names (table/service field names) and a CDS namespace blacklist for new entities and services can be configured. Field extensions can only be performed with those prefixes. Similarly, new entities and services can’t be created within the blacklisted CDS namespaces. Namespace restrictions are “inherited”, that is, protecting “com.sap.” will also protect “com.sap.*”


As an SAP developer, you should protect ["com.sap.", "sap."].


The mtx section may also list the entities and services that are allowed to be extended. The entity-whitelist and service-whitelist are listing the fully qualified names of the entities or services.

Note: If these lists are not provided, all entities or services are allowed to be extended. A warning will be logged by cds build in this case.

In case entries of the lists can’t be resolved, cds build will fail with an error.

The section for datasource db shows how to configure for a bound Instance Manager service for HDI containers.

The cds-mtx library exposes the “tenant provisioning API”, the “model API”, which is used to perform extensions, and the “metadata API” used to read tenant-specific models and metadata. There are use cases where only some of these APIs shall be exposed. This can be configured by:

"mtx": {
    "api": {
      "model":        true || false,   // default = true
      "provisioning": true || false,   // default = true
      "metadata":     true || false    // default = true
}

REST APIs

All APIs receive and respond with JSON payloads. Application-specific logic (for example, scope checks) can be added using event handlers.

Tenant Provisioning API
  • Onboard tenant
    PUT /mtx/v1/provisioning/tenant/<tenantId>
    

    Minimal request body:

    {
      "subscribedSubdomain": "<subdomain>",
      "eventType": "CREATE"
    }
    

    An application can mix-in application-specific parameters into this payload, which it can interpret within application handlers. Use the _application_ object to specify those. There’s one predefined sap object, which is interpreted by cds-mtx default handlers. With that, it’s possible to set service creation parameters to be used by the instance manager service when creating HDI container service instances. A typical use case is to provide the database_id to distinguish between multiple SAP HANA DBs mapped to one Cloud Foundry space.

    {
      "subscribedSubdomain": "<subdomain>",
      "eventType": "CREATE",
      "_application_": {
        "sap": {
          "managed-hana": {
            "provisioning_parameters": { "database_id" : "<HANA DB GUID>" },
            "binding_parameters": {"<key>" : "<value>"}
        }
      }  
    }
    

    Having two SAP HANA databases mapped to one space, onboarding won’t work out of the box, unless you’ve specified a default database. This can either be achieved by specifying a default when mapping a database to a space, or by setting a default when creating the instance-manager service instance:


    1. Set default when sharing a database. See Share an Instance with another Cloud Foundry Space for more details.

    2. Set default when creating the instance-manager service instance

    cf create-service managed-hana hdi-shared <service name> -c <config file>
    

    where the config file contains the default database_id:

    {
      "database_id" : "<HANA DB GUID>"
    }
    
  • Offboard tenant
    DELETE /mtx/v1/provisioning/tenant/<tenantId>
    
  • Subscription dependencies
    GET /mtx/v1/provisioning/dependencies
    

    Response body: Array of String. The default implementation returns an empty array.

  • GET onboarded tenants
    GET /mtx/v1/provisioning/tenant/
    

    Returns the list of onboarded tenants. For each tenant, the request body is returned which have been used for onboarding the tenant.

Model API
  • Get cds model content
    GET mtx/v1/model/content/<tenantId>
    

    Returns the two objects base and extension in response body:

    {
      "base": "<base model cds files>",
      "extension": "<extension cds files>"
    }
    
  • Activate extensions
    POST mtx/v1/model/activate
    

    Request body (example):

    {
      "tenant": "tenant1",
      "extension": "<cds extension files>",
      "undeployExtension": false
    }
    

    The extension element must be a JSON array of arrays. Each first-level array element corresponds to a CDS file containing CDS extensions. Each second-level array element must be a two entry array. The first entry specifies the file name. The second entry specifies the file content. Extension files for data models must be placed into a db-folder. Extensions for services must be placed into srv-folder. Entities of the base model (the nonextended model) are imported by using ... from '_base/...'.

    Request body detailed sample:

    {
    "tenant": "6c07c584-f463-46e5-9340-51958c51dd0a",
    "extension": [
      [
      "db/ext-entities.cds",
      "using my.bookshop from '_base/db/data-model'; \n extend entity bookshop.Books with { \n ISBN: String; \n rating: Integer \n  }"
      ],
      [
      "db/new-entities.cds",
      "namespace com.acme.ext; \n entity Categories { \n key ID: String; \n description: String; \n }"
      ],
      [
      "srv/ext-service.cds",
      "using CatalogService from '_base/srv/cat-service'; \n using com.acme.ext from '../db/new-entities'; \n extend service CatalogService with { \n  @insertonly entity Categories as projection on ext.Categories; \n }"
      ]
      ],
    "undeployExtension": false
    }
    
  • Upgrade base model from filesystem (asynchronous)
    POST mtx/v1/model/asyncUpgrade
    

    Request body:

    { 
      "tenants": ["tenantId1", "tenantId2", ...], 
      "autoUndeploy": <boolean>
    }
    

    Upgrade all tenants with request body { "tenants": ["all"] }.

    If autoUndeploy is set to true, the auto-undeploy mode of the HDI deployer is used. See HDI Delta Deployment and Undeploy Whitelist for more details.

    Response (example):

    { "jobID": "iy5u935lgaq" }
    

    The jobID can be used to query the status of the upgrade process:

    GET /mtx/v1/model/status/<jobID>
    

    During processing, the response can look like:

    {
      "error": null,
      "status": "RUNNING",
      "result": null
    }
    

    Once a job is finished, the collective status is reported like this:

    {
      "error": null,
      "status": "FINISHED",
      "result": {
          "tenants": {
              "<tenantId1>": {
                  "status": "SUCCESS",
                  "message": "",
                  "buildLogs": "<build logs>"
              },
              "<tenantId2>": {
                  "status": "FAILURE",
                  "message": "<some error log output>",
                  "buildLogs": "<build logs>"
              }
          }
      }
    }
    

    Logs are persisted for a period of 30 minutes before they get deleted automatically. If you’re requesting the job status “too late”, you get a 404 Not Found response.


Metadata API

All metadata APIs support eTags. By setting the corresponding header, you can check for model updates.

  • GET edmx
    GET /mtx/v1/metadata/edmx/<tenantId>
    

    Returns the edmx metadata of the (extended) model of the application.

    Optional URL parameters

    name=<service-name>
    language=<language-code>
    
  • GET csn
    GET /mtx/v1/metadata/csn/<tenantId>
    

    Returns the compiled (extended) model of the application.

  • GET languages
    GET /mtx/v1/metadata/languages/<tenantId>
    

    Returns the supported languages of the (extended) model of the application.

  • GET services
    GET /mtx/v1/metadata/services/<tenantId>
    

    Returns the services of the (extended) model of the application.

Exposing the Extension API

Tenant-specific extensions can be done by customers of your SaaS application by using the CDS command line client (see Extend SaaS Applications). The client uses the /mtx/v1/model/content and /mtx/v1/model/activate endpoints. There are two options to determine the root URL for these endpoints:

  • Use the Cloud Foundry route to the cds-mtx server directly
  • Use an approuter URL (if present) and create appropriate routes to the cds-mtx server.

For the latter approach, approuter configuration in xs-app.json should be chosen as in the following example:

...
"routes": [
  ...
  {
	"source": "^/extend/(.*)",
	"destination": "sidecar",
	"target": "$1",
	"authenticationType":"none"
  }
  ...
]
...

In this case, a customer creates an extension project with the following command:

cds extend https://<tenant-specific-route-to-approuter>/extend ...

It’s recommended to use the /extend/ path segment to achieve some harmonization. It’s required to set "authenticationType": "none" to avoid having the approuter dealing with authentication for this route (the process of fetching a JWT is different - see Extend SaaS Applications).

Enable a Node.js Project

NPM Dependencies

To use cds-mtx with SAP HANA and SAP Cloud Platform security, include the following dependencies into package.json:

"engines": {
    "node": ">=8.0.0 <11.0.0"
    },
"dependencies": {
    "@sap/cds": "<latest release>",
    "@sap/cds-mtx": "<latest release>",
    "@sap/hdi-deploy": "^3.11.4",
    "@sap/instance-manager": "^2",
    "@sap/hana-client": "^2",
    "@sap/xssec": "^2.1.16",
    "express": "^4.16.4",
    "passport": "^0.4.0"
}

Replace <latest release> by the latest cds and cds-mtx release available to you. The "node": version restriction must be set, due to version restrictions of hdi-deploy.

Server Start Script

To let your Node.js server serve the cds-mtx APIs and to connect to a multi-tenant setup for SAP HANA, use the following start script (for example, server.js):

const app = require('express')();
const cds = require('@sap/cds');
// connect to datasource 'db' which must be the HANA instance manager
cds.connect.to('db');
 // serve cds-mtx APIs
cds.mtx.in(app);
// serve application defined services: in combination with a CAP Java server, this won't appear here.
cds.serve('all');
const PORT = process.env.PORT || 4004;
app.listen(PORT);

4004 is the port used when starting the server locally.


Project Structure and cds build

Follow the usual recommendations and conventions described in the Getting Started guide. As described, to enable cds-mtx you need to do appropriate settings in package.json and use a Node server startup script (for example, server.js).

Your project should look like this (without any UI part):

package.json         // node project definition
.cdsrc.json          // cds configuration (if not in package.json)
server.js
db/
  csv/               // csv files
  dbmodel1.cds       // cds files for db activation
  dbmodel2.cds
  ...
srv/
  i18n/              // language property files
  srvmodel1.cds      // service definition cds files
  srvmodel2.cds
  ...
handlers/            // handler implementations
  impl1.js
  impl2.js
  ...

At runtime, cds-mtx expects all cds related files to be located in folder gen/sdc/. This folder is generated when executing cds build/all. As a prerequisite, cds configuration files must include:

"build": {
    "tasks": [ {"for": "mtx", "src": "." } ]
}

Don’t check in gen/-folders to version control.


Enable a Java Project

Event Handlers for cds-mtx APIs


Note: If you’re using a CAP Java server, it re-exposes the APIs required by SAP Cloud Platform’s SaaS Manager (the Provisioning API). It’s recommended to leverage the corresponding Java-based mechanisms to add handlers to these APIs. Handlers for the Model-API of cds-mtx must always be implemented on the Node.js server, because this isn’t re-exposed by the Java stack.


cds-mtx serves its APIs by using cds technology. Therefore, any additional application logic can be implemented as cds event handlers. Event handlers can be implemented in JavaScript files, which are named according to the compactified service name. Those files should be placed into the folder containing your cds service definition files (for example, the srv/ folder, but they can be placed anywhere where models are loaded from). In case of the cds-mtx APIs, use the files provisioning.js, model.js, and metadata.js. See below use cases for examples.


Use Case: Return Application URL After Onboarding

When connecting the tenant provisioning API to SAP Cloud Platform’s SaaS Manager, a handler for ProvisioningService must be implemented, which returns the application URL (to be used by subscribers) for displaying it in SAP Cloud Platform Cockpit. The below example assumes that the application sets an environment variable APP_URLPART. The task of the handler is to prefix the content of APP_URLPART with the tenant subdomain contained in the onboarding request, for returning the complete, tenant-specific application URL:

provisioning.js

module.exports = (service) => {
  // event handler for returning the tenant specific application URL as a response to an onboarding request
  service.on('UPDATE', 'tenant', async (req, next) => {
    const res = await next();          // IMPORTANT: call default implementation which is doing the HDI container creation
    let c = cds.env.for('app');        // use cds config framework to read app specific config node
    let appuri = typeof c.urlpart === "undefined" ? ' ' : c.urlpart;
    if (appuri === ' ') {
      console.log('[INFO ][ON_UPDATE_TENANT] Application URI for subscriptions is not configured.');
      return '';
    } else {
      let url = 'https://' + req.data.subscribedSubdomain + appuri;
      console.log('[INFO ][ON_UPDATE_TENANT] ' + 'Application URL is ' + url);
      return url;
    }
  });
}

A handler is registered which replaces the default handler for the provisioning service. UPDATE is relevant here, because SAP Cloud Platform sends a PUT request for onboarding. The environment variable isn’t read-in directly. Instead, the cds configuration framework is used, which allows configuring in various ways.


Use Case: Application-Specific Scope Checks

Another common task is to implement application-specific scope checks for the subscription requests. This can be done by implementing the “before - UPDATE -tenant” event handler within provisioning.js:

module.exports = (service) => {
  // event handler for doing some application specific scope checks
  service.before ('UPDATE', 'tenant', async (req) => {
    console.log('[INFO ][BEFORE_UPDATE_TENANT] ' + req.data.subscribedTenantId);
    if (!req.user.is('Callback')) {     // check for the scope "Callback"
      console.log('[ERROR][BEFORE_UPDATE_TENANT]: JWT does not contain required scope.');
      // Reject request
      const e = new Error('Forbidden');
      e.statusCode = 403;
      return req.reject(e);
    } else {
      console.log('[INFO ][BEFORE_UPDATE_TENANT]: JWT contains relevant scope');
    }
  });
...
}


Use Case: Conditional Offboarding

Applications may want to prevent tenant database container deletion if certain conditions are met (for instance, there are still subscribers accessing this database). This can be implemented by replacing the default “DELETE tenant” implementation:

module.exports = (service) => {
  ...
  // Event handler checking a condition before deleting the tenant DB container as a response to an offboarding request.
  // This handler overwrites the default handler!
  service.on ('DELETE', 'tenant', async (req, next) => {
    console.log('[INFO ][ON_DELETE_TENANT] ' + req.data.subscribedTenantId);
    if (1 === 2) {    // put your condition here
      console.log('[INFO ][ON_DELETE_TENANT]: Offboarding will not be executed');
    } else {
       await next();  // call default implementation which is doing regular offboarding
    }
  });
  ...
}


Use Case: Return Subscription Dependencies

SAP Cloud Platform’s SaaS Manager might request an array of subscription dependencies (“GET DEPENDENCIES callback”). This can be implemented by registering an ON-handler for the GET mtx/v1/provisioning/dependencies endpoint.

module.exports = (service) => {
...
  // event handler returning an array of dependencies as a response to the GET DEPENDENCIES request
    service.on('dependencies', async (req) => {
    const deps = ['first_dependency', 'second_dependency'];
    console.log('[INFO ][ON_GET_DEPENDENCIES] Dependent applications/services: ' + JSON.stringify(deps) );
    return deps;
  });
...
}

Extension Templates

The SaaS application provider can deliver template cds files to customers, which can demonstrate typical and useful extension use cases. When using the cds command line interface, a customer can check out these template files using the command cds extend --templates (cf. Fetching extension templates from the SaaS application).

As the application provider, you can place those template cds files into a tpl/ folder in the root of your development project. Use db/ and srv/ subfolders to indicate the use case for an extension file (domain model extension, or service extension).

Extend SaaS Applications

Subscribers (customers) of a SaaS application can extend data and service models in the context of their subscription (= tenant context = subaccount context). New fields can be added to SAP provided database tables. Those fields can be added to UIs as well, if these have been built with SAP’s Fiori Elements technology.

The overall process is depicted in the following figure:

customization process

Setup Tenant Landscape

Using SAP Cloud Platform Cockpit, an account administrator is setting-up a “landscape of tenants” (= multiple subaccounts) to enable a staged extension development scenario (for example, development and productive tenants). We recommend to setup at least a development tenant to test extended data-, service-, and UI models before activating these into the productive tenant.

Subscribe to SaaS Application

Using SAP Cloud Platform Cockpit, a subaccount administrator is subscribing to the SaaS application. During this subscription, the SaaS application automatically performs a tenant onboarding step, which (in case of using SAP HANA) allocates an SAP HANA persistence for this tenant (= subaccount) and deploys all database objects.


Extending cds services and database entities is only possible if the SaaS application is using the SAP HANA database service. Further, the SaaS application must have been enabled for extensibility by the SaaS provider.


Authorize Extension Developer

The extension is done by an extension developer (a customer role). The privilege to extend the base model of a tenant is linked to a scope of the SaaS application. Therefore, the security administrator of a subaccount has to grant this scope to the developer using SAP Cloud Platform Cockpit. As a prerequisite, the developer has to be registered within the Identity Provider linked to the subaccount.

There are two relevant scopes, which can be assigned to extension developers:

Scope  
ExtendCDS Create extension projects and apply extension files. Not authorized to delete tables created by previous extensions
ExtendCDSdelete In addition, enables deletion of tables created by previous extension, which can cause data loss!

The SaaS application delivers role templates including these scopes. To know these, consult the documentation of the SaaS application.

Start Extension Project

Extension developers initialize an extension project on the file system on a computer. Prerequisites:

  • The cds command line tools must be installed (see Local Setup).
  • The Identity Provider linked to the tenant’s subaccount must support the SAML standard.
  • It’s recommended to use an Integrated Development Environment (IDE) with one of the available CDS editors for authoring extension cds files.
  • Basic knowledge of the cds language.

Use the regular cds help feature to learn more about command options. For instance, to see a description of the command cds extend, use cds help extend.


The cds client is communicating with the SaaS application and fetches the “base model” (the not-yet-extended model) from there. An extension project folder is generated on the local file system. If an extension has already happened before, the last activated extension is fetched as well.

As an extension developer, initialize an extension project with the following command:

cds extend <app-url> -d <project-directory> -p <passcode>

<app-url> is specific to the SaaS application you’re going to extend. This URL can be found in the documentation for the respective SaaS application. Usually, <app-url> is the same URL visible on the subscriptions tab of SAP Cloud Platform Cockpit, which is used to launch the application, enhanced with an additional URL path segment (for example, /extend). However, the SaaS application can decide to provide a different URL for working with extensions.

<project-directory> is the folder on local disk, which will contain the extension project files.

<passcode> is a temporary authentication code, which is used to connect to the SaaS application. This passcode can be retrieved by opening a browser logon page. The URL of this browser page depends on SAP Cloud Platform landscape where the SaaS application is running and the tenant, which shall be extended:

<url> = https://<tenant-subdomain>.authentication.<landscape>.hana.ondemand.com/passcode

A passcode can be used only once and within a limited period of time. If the connection is successfully established, subsequent commands can be executed for a time period, which depends on the configuration of the Identity Provider linked to the Subaccount on SAP Cloud Platform. When expired, a new passcode has to be generated and send again.

As the connection is tenant-specific, <tenant-subdomain> must be the subdomain contained in <app-url> (the string preceding the first dot ‘.’). If not the case, the option -s <tenant-subdomain> can be used.

As a result of cds extend, an extension project is created in the specified folder. As an example, the following file/folder structure is generated on local disk:

myextproject/
  package.json    # extension project descriptor
  srv/
            # will contain service and ui-related extension cds files
  db/
            # will contain db-related extension cds files
  node_modules/
    _base/
       ...   # contains the base model provided by the SaaS application

The node_modules folder should be hidden when using an IDE, because it contains artifacts (the base cds model of the SaaS application) which can’t be changed. SaaS applications can provide templates to document how to do meaningful extensions of base entities and services.

This project structure follows the same conventions as introduced for developing entire cds applications. Model extension files, which are relevant for a database deployment must be placed in folder db/. Service extension files must be placed in folder srv/. The base model is treated like a reuse model. You can refer to it in extension files simply by using ... from '_base/...'


Extension developer should drive extension projects similar to other development projects. It’s recommended to use a version control system (for example, Git) to host the extension project sources.


Develop & Activate Extensions

Developing cds model files is supported by cds editor and cds build tools. Within these files, you can reference base model files through using ... from '_base/...' statements. Entities and services can be extended using the cds extend technique. The following example shows how to add two fields to a Books database table of a hypothetical Bookshop application. A file extension.cds is created (the file name doesn’t matter) within the db-folder:

using sap.bookshop from '_base/db/datamodel';

extend entity bookshop.Books with {
  GTIN: String(14);
  rating: Integer;
}

Extensions can be activated into a test tenant using the following command:

cds activate <extension-project-directory>

This uses the current connection for activation. If this connection doesn’t exist or has expired, then the options ‘-p <passcode>’, ‘-s <tenant-subdomain>’, and ‘--to <app-url>’ can be used to (re)connect. Activating an existing project into a different tenant requires setting <passcode> and <app-url> appropriately.


By using cds activate, it isn’t possible to upload csv-files into the extended tenant.



Executing cds extend on an Existing Extension Project

cds extend is used to create and initialize an extension project. Subsequent executions of cds extend must be done with the --force option to overwrite existing files (base model files and extension files). No files will be deleted. Features of a version control system should be used to detect and merge changes.

Fetching Extension Templates from the SaaS Application

SaaS applications can deliver template files, which demonstrate how to extend entities and services for this SaaS application. Such templates can be fetched from the server by using the --templates option of the cds extend command. As a result, a folder tpl will be generated which contains the extension template files.


Deploy Extensions to Production

The productive tenant (the productive subaccount on SAP Cloud Platform) is technically treated the same way as any development tenant. Role-based authorization can restrict the access to the productive tenant to dedicated extension developers. An extension project, which has been generated by referencing a development tenant, can be activated into a productive tenant by using cds activate with appropriate options.