Skip to content
On this page

Extending SaaS Applications

This guide explains how subscribers of SaaS applications can extend these on tenant level, thereby customizing them for their specific needs.

WARNING

Note: This guide is for the new MTX services package @sap/cds-mtxs. Projects still working with the previous MTX version, see the previous extensibility guide, and the migration guide.

Introduction & Overview

Subscribers (customers) of SaaS solutions frequently need to tailor these to their specific needs, for example, by adding specific extension fields and entities. All CAP-based applications intrinsically support this out-of-the-box.

The overall process is depicted in the following figure:

customization process

As a prerequisite, you need a CAP-based application, that was developed and deployed to SAP BTP as a multitenant application.

Walkthrough

Jumpstart (Optional)

Instead of going for a manual step-by-step experience, you can also download Orders Management application in cap/samples with all walkthrough preparation steps applied upfront, you may checkout the ext branch:

sh
git clone https://github.com/sap-samples/cloud-cap-samples -b ext
git clone https://github.com/sap-samples/cloud-cap-samples -b ext

Step-by-step

The following sections describe and allow a step-by-step walkthrough to build your application bottom-up:

  1. Clone our cap/samples repository
sh
git clone https://github.com/sap-samples/cloud-cap-samples
cd cloud-cap-samples
npm ci
git clone https://github.com/sap-samples/cloud-cap-samples
cd cloud-cap-samples
npm ci
  1. Ensure to have the latest version of @sap/cds-dk installed globally:
sh
npm i -g @sap/cds-dk
npm i -g @sap/cds-dk

As a SaaS Provider

CAP provides intrinsic extensibility, which means all your entities and services are extensible by default. All you have to do is to switch on extensibility by a single flag in your project settings. In addition, however, you usually would want to restrict what can be extended, and also add corresponding documentation and templates for your customers.

Enable Extensibility

The easiest way to enable extensibility is to use cds add mtx in your project root, for example, like this in our tutorial walkthrough:

sh
cd orders
cds add mtx
npm update
cd orders
cds add mtx
npm update

Essentially, this automates the following two steps...

  1. Add @sap/cds-mtxs package dependency:
sh
npm add @sap/cds-mtxs
npm add @sap/cds-mtxs
  1. Switch on cds.requires.extensibility in your package.json:

package.json

json
{
  "cds": {
    "requires": {
      "extensibility": true
    }
  }
}
{
  "cds": {
    "requires": {
      "extensibility": true
    }
  }
}

Restrict Extension Points

Usually you want to restrict, which services or entities your SaaS customers are supposed to extend, and to what degree they are allowed to do so. For example, in the package.json of the base app, you find the configuration to enforce the following restrictions:

  • All new elements have to start with x_ → to avoid name conflicts
  • Allows extending entities in namespace sap.capire.orders
    Restricted to 2 new elements maximum.
  • Allows extending the OrdersService
    Restricted to 2 new entities maximum.

Add cds.xt.ExtensibilityService to your package.json:

package.json: cds > requires

jsonc
{
  "cds.xt.ExtensibilityService": {
    "element-prefix": ["x_"],
    "extension-allowlist": [
      {
        "for": ["sap.capire.orders"],
        "kind": "entity",
        "new-fields": 2
      },
      {
        "for": ["OrdersService"],
        "new-entities": 2
      }
    ]
  }
}
{
  "cds.xt.ExtensibilityService": {
    "element-prefix": ["x_"],
    "extension-allowlist": [
      {
        "for": ["sap.capire.orders"],
        "kind": "entity",
        "new-fields": 2
      },
      {
        "for": ["OrdersService"],
        "new-entities": 2
      }
    ]
  }
}

See source of this in the cap/samples/orders repo.

Learn more about this in the MTX Services reference guide.

Provide Extension Guides

You should provide documentation to guide your customers through the steps to add extensions. This guide should provide application-specific information along the lines of the walkthrough steps presented in this guide.

Here's a rough checklist what this guide should cover:

Provide Template Projects

To jumpstart your customers with extension projects, you should provide respective template projects, as application-specific starting points. We recommend to ship that template with your application and offer it as downloadable archive.

Create an Extension Project (Template)

Extension projects are standard CAP projects extending the base application. Create one for your base app, following these steps:

  1. Create a new CAP project — orders-ext in our walkthrough:

    sh
    cd ..
    cds init orders-ext
    code orders-ext        # if you use VSCode
    cd ..
    cds init orders-ext
    code orders-ext        # if you use VSCode
  2. Add this to your package.json:

package.json

jsonc
{
  "name": "@capire/orders-ext",
  "extends": "@capire/orders"
}
{
  "name": "@capire/orders-ext",
  "extends": "@capire/orders"
}

  • As package name choose any reasonable name. It is used to identify this extension within a SaaS subscription. Your customers can adjust this.

  • Property extends is the package name of the base application. This is used as subdirectory of node_modules, inside the extension project, where the model of the SaaS app (the base model) will be stored by cds pull. This in turn allows us/the extenders to refer to the app's base model, like using ... from '@capire/orders'.

Add Sample Content

Create a new file app/extensions.cds and fill in this content:

cds
namespace x_orders.ext; // for new entities, if any
using { OrdersService, sap.capire.orders.Orders } from '@capire/orders';

extend Orders with {
  x_new_field : String;
}

// -------------------------------------------
// Fiori Annotations

annotate Orders:x_new_field with @title: 'New Field';
annotate OrdersService.Orders with @UI.LineItem: [
  ... up to { Value: OrderNo },
  { Value : x_new_field },
  ...
];
namespace x_orders.ext; // for new entities, if any
using { OrdersService, sap.capire.orders.Orders } from '@capire/orders';

extend Orders with {
  x_new_field : String;
}

// -------------------------------------------
// Fiori Annotations

annotate Orders:x_new_field with @title: 'New Field';
annotate OrdersService.Orders with @UI.LineItem: [
  ... up to { Value: OrderNo },
  { Value : x_new_field },
  ...
];

Learn more about what you can do in CDS extension models

Names of the .cds file(s) can be freely chosen. Yet, for the build system to work out of the box, it must be in either the app, srv, or db folder. Learn more about project layouts.

TIP

For the sake of simplicity, we recommend putting all extension files into ./app and removing the other folders ./srv and ./db from extension projects.

TIP

You may want to consider separating concerns by putting all the Fiori annotations into a separate file ./app/fiori.cds.

Add Test Data

To support quick-turnaround tests of extensions using cds watch add some test data. In your template project, create a file test/data/sap.capire.orders-Orders.csv like that:

csv
ID;createdAt;buyer;OrderNo;currency_code;
7e2f2640-6866-4dcf-8f4d-3027aa831cad;2019-01-31;john.doe@test.com;1;EUR
64e718c9-ff99-47f1-8ca3-950c850777d4;2019-01-30;jane.doe@test.com;2;EUR
ID;createdAt;buyer;OrderNo;currency_code;
7e2f2640-6866-4dcf-8f4d-3027aa831cad;2019-01-31;john.doe@test.com;1;EUR
64e718c9-ff99-47f1-8ca3-950c850777d4;2019-01-30;jane.doe@test.com;2;EUR

Add a Readme

Include additional documentation for the extension developer in a readme.md file inside the template project. For example like that in our cap/samples/orders-ext template:

md
# Getting Started

Welcome to your extension project to  `@capire/orders`.

It contains these folders and files, following our recommended project layout:

| File or Folder | Purpose                        |
|----------------|--------------------------------|
| `app/`         | all extensions content is here |
| `test/`        | all test content is here       |
| `package.json` | project configuration          |
| `readme.md`    | this getting started guide     |


## Next Steps

- `cds pull` latest models from your base application
- edit [`./app/extensions.cds`](./app/extensions.cds) to add your extensions
- `cds watch` your extension in local test-drives
- `cds build && cds push` your extension to **test** tenant
- `cds build && cds push` your extension to **prod** tenant


## Learn More

Learn more at https://cap.cloud.sap/docs/guides/extensibility/customization.
# Getting Started

Welcome to your extension project to  `@capire/orders`.

It contains these folders and files, following our recommended project layout:

| File or Folder | Purpose                        |
|----------------|--------------------------------|
| `app/`         | all extensions content is here |
| `test/`        | all test content is here       |
| `package.json` | project configuration          |
| `readme.md`    | this getting started guide     |


## Next Steps

- `cds pull` latest models from your base application
- edit [`./app/extensions.cds`](./app/extensions.cds) to add your extensions
- `cds watch` your extension in local test-drives
- `cds build && cds push` your extension to **test** tenant
- `cds build && cds push` your extension to **prod** tenant


## Learn More

Learn more at https://cap.cloud.sap/docs/guides/extensibility/customization.

Deploy Application

Return to your orders project:

sh
cd ../orders
cd ../orders

To make sure the tenant database is not reset after restarts of the application, add a configuration for a persistent SQL database:

package.json: cds > requires

jsonc
{
  "db": "sql-mt"
}
{
  "db": "sql-mt"
}

With your application enabled and prepared for extensibility, you would now deploy the application as described in the Deployment Guide.

Yet, to test-drive in your tutorial walkthrough, simulate this by simply running the app locally as usual with cds watch:

sh
cds watch
cds watch
./orders $ cds watch

cds serve all --with-mocks --in-memory?
watching: cds,csn,csv,ts,mjs,cjs,js,json,properties,
edmx,xml,env,css,gif,html,jpg,png,svg...
live reload enabled for browsers

        ___________________________


[cds](cds) - model loaded from 8 file(s):

  db/schema.cds
  srv/orders-service.cds
  app/fiori.cds
  ../node_modules/@sap/cds-mtxs/srv/bootstrap.cds
  ../node_modules/@sap/cds-mtxs/db/extensions.cds
  ../node_modules/@sap/cds-mtxs/srv/extensibility-service.cds
  ../node_modules/@sap/cds/common.cds
  ../common/index.cds

[cds](cds) - connect using bindings from: { registry: '~/.cds-services.json' }
[cds](cds) - connect to db > sqlite { url: 'db.sqlite' }
[cds](cds) - serving cds.xt.SaasProvisioningService { path: '/-/cds/saas-provisioning' }
[cds](cds) - serving cds.xt.ModelProviderService { path: '/-/cds/model-provider' }
[cds](cds) - serving cds.xt.DeploymentService { path: '/-/cds/deployment' }
[cds](cds) - serving OrdersService { path: '/orders', impl:'srv/orders-service.js' }
[cds](cds) - serving cds.xt.ExtensibilityService { path:'/-/cds/extensibility' }

[cds](cds) - server listening on { url:'http://localhost:4006' }
[cds](cds) - launched at 11/10/2022, 08:41:19, version: 6.2.0, in: 6.047s
[cds](cds) - [ terminate with ^C ]

As a SaaS Customer

The following sections provide step-by-step instructions about adding extensions. Instructions are given with respect to our local walkthrough setup.

Subscribe to SaaS App

It all starts with a customer subscribing to a SaaS application.
In your local setup you simulate this as follows...

  1. Open a new terminal, and in there subscribe as customer tenant t1:

    sh
    cds subscribe t1 --to http://localhost:4006 -u carol:
    cds subscribe t1 --to http://localhost:4006 -u carol:

    Learn more about tenant subscriptions

  2. Verify that it worked, by opening the Orders Management Fiori UI in a new private browser window and login as carol, which is assigned to tenant t1.

image-20221004054556898

Prepare an Extension Tenant

You need a test tenant to validate the extension before activating to production.
In your local setup you simulate this as follows...

  1. Set up a test tenant t1-ext

    sh
    cds subscribe t1-ext --to http://localhost:4006 -u carol:
    cds subscribe t1-ext --to http://localhost:4006 -u carol:
  2. Assign extension developers for the test tenant.

    As you're using mocked auth, simulate this step by adding the following to the base app's package.json, assigning bob as extension developer for t1-ext:

package.json: cds > requires > auth > users

json
{
  "bob": {
    "tenant": "t1-ext",
    "roles": ["cds.ExtensionDeveloper"]
  }
}
{
  "bob": {
    "tenant": "t1-ext",
    "roles": ["cds.ExtensionDeveloper"]
  }
}

Start an Extension Project

Extension projects are standard CAP projects extending the subscribed application. SaaS providers usually provide application-specific templates, that extension developers can download and open in their editor. That's what you did in our walkthrough as SaaS provider or you already downloaded that extension template with cap/samples. Now, you can just open the orders-ext folder in your editor, for example in VS Code:

sh
code ../orders-ext
code ../orders-ext

image-20221004094531996

Pull Latest Base Model

Next you need to download the latest CDS models from the subscribed SaaS application. We refer to it as base model.

sh
cds pull --from http://localhost:4006 -u bob:
cds pull --from http://localhost:4006 -u bob:

Run cds pull --help to see all available options.

This downloads the base model into node_modules/<extends>, as configured in your package.json.

Write the Extension

Edit the file app/extensions.cds and replace its content with the following:

cds
namespace x_orders.ext; // for new entities like SalesRegion below
using { OrdersService, sap, sap.capire.orders.Orders } from '@capire/orders';

extend Orders with { // 2 new fields....
  x_priority    : String enum {high; medium; low} default 'medium';
  x_salesRegion : Association to x_SalesRegion;
}

entity x_SalesRegion : sap.common.CodeList { // Value Help
  key code : String(11);
}


// -------------------------------------------
// Fiori Annotations

annotate Orders:x_priority with @title: 'Priority';
annotate x_SalesRegion:name with @title: 'Sales Region';

annotate OrdersService.Orders with @UI.LineItem: [
  ... up to { Value: OrderNo },
  { Value: x_priority },
  { Value: x_salesRegion.name },
  ...
];
namespace x_orders.ext; // for new entities like SalesRegion below
using { OrdersService, sap, sap.capire.orders.Orders } from '@capire/orders';

extend Orders with { // 2 new fields....
  x_priority    : String enum {high; medium; low} default 'medium';
  x_salesRegion : Association to x_SalesRegion;
}

entity x_SalesRegion : sap.common.CodeList { // Value Help
  key code : String(11);
}


// -------------------------------------------
// Fiori Annotations

annotate Orders:x_priority with @title: 'Priority';
annotate x_SalesRegion:name with @title: 'Sales Region';

annotate OrdersService.Orders with @UI.LineItem: [
  ... up to { Value: OrderNo },
  { Value: x_priority },
  { Value: x_salesRegion.name },
  ...
];

Learn more about what you can do in CDS extension models

TIP

Make sure no syntax errors are shown in the CDS editor before going on to the next steps.

Test-Drive Locally

To conduct an initial test of your extension, run it locally with cds watch:

sh
cds watch --port 4005
cds watch --port 4005

This starts a local Node.js application server serving your extension along with the base model and supplied test data stored in an in-memory database.
It does not include any custom application logic, though.

Add Local Test Data

To improve local test drives you can add local test data for extensions.

Edit the template-provided file test/data/sap.capire.orders-Orders.csv and add data for the new fields as follows:

csv
ID;createdAt;buyer;OrderNo;currency_code;x_priority;x_salesRegion_code
7e2f2640-6866-4dcf-8f4d-3027aa831cad;2019-01-31;john.doe@test.com;1;EUR;high;EMEA
64e718c9-ff99-47f1-8ca3-950c850777d4;2019-01-30;jane.doe@test.com;2;EUR;low;APJ
ID;createdAt;buyer;OrderNo;currency_code;x_priority;x_salesRegion_code
7e2f2640-6866-4dcf-8f4d-3027aa831cad;2019-01-31;john.doe@test.com;1;EUR;high;EMEA
64e718c9-ff99-47f1-8ca3-950c850777d4;2019-01-30;jane.doe@test.com;2;EUR;low;APJ

Create a new file test/data/x_orders.ext-x_SalesRegion.csv with this content:

csv
code;name;descr
AMER;Americas;North, Central and South America
EMEA;Europe, the Middle East and Africa;Europe, the Middle East and Africa
APJ;Asia Pacific and Japan;Asia Pacific and Japan
code;name;descr
AMER;Americas;North, Central and South America
EMEA;Europe, the Middle East and Africa;Europe, the Middle East and Africa
APJ;Asia Pacific and Japan;Asia Pacific and Japan

Verify the Extension

Verify your extensions are applied correctly by opening the Orders Fiori Preview in a new private browser window, login as bob, and see columns Priority and Sales Region filled as in the screenshot below:

image-20221004080722532

Note: the screenshot includes local test data, added as explained below.

This test data will only be deployed to the local sandbox and not be processed during activation to the productive environment.

Add Data

After pushing your extension, you have seen that the column for Sales Region was added, but is not filled. To change this, you need to provide initial data with your extension. To do so, copy the data file that you created before from test/data/ to db/data/. Also check further details about adding data

Push to Test Tenant

Let's push your extension to the deployed application in your test tenant for final verification before pushing to production.

sh
cds build
cds push --to http://localhost:4006 -u bob:
cds build
cds push --to http://localhost:4006 -u bob:

Learn more about cds push

TIP

Note: You pushed the extension with user bob, which in your local setup ensures they are sent to your test tenant t1-ext, not the production tenant t1.

Verify the Extension

Verify your extensions are applied correctly by opening the Order Management UI in a new private browser window, login as bob, and check that columns Priority and Sales Region are displayed as in the screenshot below. Also, check that there's content with a proper label in the Sales Region column.

image-20221004081826167

Is some data missing? Make sure you copied the data.

Activate the Extension

Finally, after all tests, verifications and approvals are in place, you can push the extension to your production tenant:

sh
cds build
cds push --to http://localhost:4006 -u carol:
cds build
cds push --to http://localhost:4006 -u carol:

TIP

Note: You pushed the extension with user carol, which in your local setup ensures they are sent to your production tenant t1.

Appendices

About Extension Models

This section explains in detail about the possibilities that the CDS languages provides for extension models.

All names are subject to extension restrictions defined by the SaaS app.

Extending the Data Model

Following the extend directive it is pretty straightforward to extend the application with the following new artifacts:

  • Extend existing entities with new (simple) fields.
  • Create new entities.
  • Extend existing entities with new associations.
  • Add compositions to existing or new entities.
  • Supply new or existing fields with default values, range checks, or value list (enum) checks.
  • Define a mandatory check on new or existing fields.
  • Define new unique constraints on new or existing entities.
cds
using {sap.capire.bookshop, sap.capire.orders} from '@capire/fiori';
using {
  cuid, managed, Country, sap.common.CodeList
} from '@sap/cds/common';

namespace x_bookshop.extension;

// extend existing entity
extend orders.Orders with {
  x_Customer    : Association to one x_Customers;
  x_SalesRegion : Association to one x_SalesRegion;
  x_priority    : String @assert.range enum {high; medium; low} default 'medium';
  x_Remarks     : Composition of many x_Remarks on x_Remarks.parent = $self;
}
// new entity - as association target
entity x_Customers : cuid, managed {
  email        : String;
  firstName    : String;
  lastName     : String;
  creditCardNo : String;
  dateOfBirth  : Date;
  status       : String   @assert.range enum {platinum; gold; silver; bronze} default 'bronze';
  creditScore  : Decimal  @assert.range: [ 1.0, 100.0 ] default 50.0;
  PostalAddresses : Composition of many x_CustomerPostalAddresses on PostalAddresses.Customer = $self;
}

// new unique constraint (secondary index)
annotate x_Customers with @assert.unique: { email: [ email ] } {
  email @mandatory;  // mandatory check
}

// new entity - as composition target
entity x_CustomerPostalAddresses : cuid, managed {
  Customer     : Association to one x_Customers;
  description  : String;
  street       : String;
  town         : String;
  country      : Country;
}

// new entity - as code list
entity x_SalesRegion: CodeList {
  key regionCode : String(11);
}

// new entity - as composition target
entity x_Remarks : cuid, managed {
  parent      : Association to one orders.Orders;
  number      : Integer;
  remarksLine : String;
}
using {sap.capire.bookshop, sap.capire.orders} from '@capire/fiori';
using {
  cuid, managed, Country, sap.common.CodeList
} from '@sap/cds/common';

namespace x_bookshop.extension;

// extend existing entity
extend orders.Orders with {
  x_Customer    : Association to one x_Customers;
  x_SalesRegion : Association to one x_SalesRegion;
  x_priority    : String @assert.range enum {high; medium; low} default 'medium';
  x_Remarks     : Composition of many x_Remarks on x_Remarks.parent = $self;
}
// new entity - as association target
entity x_Customers : cuid, managed {
  email        : String;
  firstName    : String;
  lastName     : String;
  creditCardNo : String;
  dateOfBirth  : Date;
  status       : String   @assert.range enum {platinum; gold; silver; bronze} default 'bronze';
  creditScore  : Decimal  @assert.range: [ 1.0, 100.0 ] default 50.0;
  PostalAddresses : Composition of many x_CustomerPostalAddresses on PostalAddresses.Customer = $self;
}

// new unique constraint (secondary index)
annotate x_Customers with @assert.unique: { email: [ email ] } {
  email @mandatory;  // mandatory check
}

// new entity - as composition target
entity x_CustomerPostalAddresses : cuid, managed {
  Customer     : Association to one x_Customers;
  description  : String;
  street       : String;
  town         : String;
  country      : Country;
}

// new entity - as code list
entity x_SalesRegion: CodeList {
  key regionCode : String(11);
}

// new entity - as composition target
entity x_Remarks : cuid, managed {
  parent      : Association to one orders.Orders;
  number      : Integer;
  remarksLine : String;
}

TIP

This example provides annotations for business logic handled automatically by CAP as documented in our cookbook "Providing Services".
Also, learn more about the basic syntax of the annotate directive.

Extending the Service Model

In the existing in OrdersService, the new entities x_CustomerPostalAddresses and x_Remarks are automatically included since they are targets of the corresponding compositions.

The new entities x_Customers and x_SalesRegion are autoexposed in a read-only way as CodeLists. Only if wanted to change it, you would need to expose them explicitly:

cds
using { OrdersService } from '@capire/fiori';

extend service OrdersService with {
  entity x_Customers   as projection on extension.x_Customers;
  entity x_SalesRegion as projection on extension.x_SalesRegion;
}
using { OrdersService } from '@capire/fiori';

extend service OrdersService with {
  entity x_Customers   as projection on extension.x_Customers;
  entity x_SalesRegion as projection on extension.x_SalesRegion;
}

Extending UI Annotations

The following snippet demonstrates which UI annotations you need to expose your extensions to the SAP Fiori elements UI.

Add UI annotations for the completely new entities x_Customers, x_CustomerPostalAddresses, x_SalesRegion, x_Remarks:

cds
using { OrdersService } from '@capire/fiori';

// new entity -- draft enabled
annotate OrdersService.x_Customers with @odata.draft.enabled;

// new entity -- titles
annotate OrdersService.x_Customers with {
  ID           @(
    UI.Hidden,
    Common : {Text : email}
  );
  firstName    @title : 'First Name';
  lastName     @title : 'Last Name';
  email        @title : 'Email';
  creditCardNo @title : 'Credit Card No';
  dateOfBirth  @title : 'Date of Birth';
  status       @title : 'Status';
  creditScore  @title : 'Credit Score';
}

// new entity -- titles
annotate OrdersService.x_CustomerPostalAddresses with {
  ID          @(
    UI.Hidden,
    Common : {Text : description}
  );
  description @title : 'Description';
  street      @title : 'Street';
  town        @title : 'Town';
  country     @title : 'Country';
}

// new entity -- titles
annotate x_SalesRegion : regionCode with @(
  title : 'Region Code',
  Common: { Text: name, TextArrangement: #TextOnly }
);


// new entity in service -- UI
annotate OrdersService.x_Customers with @(UI : {
  HeaderInfo       : {
    TypeName       : 'Customer',
    TypeNamePlural : 'Customers',
    Title          : { Value : email}
  },
  LineItem         : [
    {Value : firstName},
    {Value : lastName},
    {Value : email},
    {Value : status},
    {Value : creditScore}
  ],
  Facets           : [
  {$Type: 'UI.ReferenceFacet', Label: 'Main', Target : '@UI.FieldGroup#Main'},
  {$Type: 'UI.ReferenceFacet', Label: 'Customer Postal Addresses', Target: 'PostalAddresses/@UI.LineItem'}
],
  FieldGroup #Main : {Data : [
    {Value : firstName},
    {Value : lastName},
    {Value : email},
    {Value : status},
    {Value : creditScore}
  ]}
});

// new entity -- UI
annotate OrdersService.x_CustomerPostalAddresses with @(UI : {
  HeaderInfo       : {
    TypeName       : 'CustomerPostalAddress',
    TypeNamePlural : 'CustomerPostalAddresses',
    Title          : { Value : description }
  },
  LineItem         : [
    {Value : description},
    {Value : street},
    {Value : town},
    {Value : country_code}
  ],
  Facets           : [
    {$Type: 'UI.ReferenceFacet', Label: 'Main', Target : '@UI.FieldGroup#Main'}
  ],
  FieldGroup #Main : {Data : [
    {Value : description},
    {Value : street},
    {Value : town},
    {Value : country_code}
  ]}
}) {};

// new entity -- UI
annotate OrdersService.x_SalesRegion with @(
  UI: {
    HeaderInfo: {
      TypeName       : 'Sales Region',
      TypeNamePlural : 'Sales Regions',
      Title          : { Value : regionCode }
    },
    LineItem: [
      {Value: regionCode},
      {Value: name},
      {Value: descr}
    ],
    Facets: [
      {$Type: 'UI.ReferenceFacet', Label: 'Main', Target: '@UI.FieldGroup#Main'}
    ],
    FieldGroup#Main: {
      Data: [
        {Value: regionCode},
        {Value: name},
        {Value: descr}
      ]
    }
  }
) {};

// new entity -- UI
annotate OrdersService.x_Remarks with @(
  UI: {
    HeaderInfo: {
      TypeName       : 'Remark',
      TypeNamePlural : 'Remarks',
      Title          : { Value : number }
    },
    LineItem: [
      {Value: number},
      {Value: remarksLine}
    ],
    Facets: [
      {$Type: 'UI.ReferenceFacet', Label: 'Main', Target: '@UI.FieldGroup#Main'}
    ],
    FieldGroup#Main: {
      Data: [
          {Value: number},
          {Value: remarksLine}
      ]
    }
  }
) {};
using { OrdersService } from '@capire/fiori';

// new entity -- draft enabled
annotate OrdersService.x_Customers with @odata.draft.enabled;

// new entity -- titles
annotate OrdersService.x_Customers with {
  ID           @(
    UI.Hidden,
    Common : {Text : email}
  );
  firstName    @title : 'First Name';
  lastName     @title : 'Last Name';
  email        @title : 'Email';
  creditCardNo @title : 'Credit Card No';
  dateOfBirth  @title : 'Date of Birth';
  status       @title : 'Status';
  creditScore  @title : 'Credit Score';
}

// new entity -- titles
annotate OrdersService.x_CustomerPostalAddresses with {
  ID          @(
    UI.Hidden,
    Common : {Text : description}
  );
  description @title : 'Description';
  street      @title : 'Street';
  town        @title : 'Town';
  country     @title : 'Country';
}

// new entity -- titles
annotate x_SalesRegion : regionCode with @(
  title : 'Region Code',
  Common: { Text: name, TextArrangement: #TextOnly }
);


// new entity in service -- UI
annotate OrdersService.x_Customers with @(UI : {
  HeaderInfo       : {
    TypeName       : 'Customer',
    TypeNamePlural : 'Customers',
    Title          : { Value : email}
  },
  LineItem         : [
    {Value : firstName},
    {Value : lastName},
    {Value : email},
    {Value : status},
    {Value : creditScore}
  ],
  Facets           : [
  {$Type: 'UI.ReferenceFacet', Label: 'Main', Target : '@UI.FieldGroup#Main'},
  {$Type: 'UI.ReferenceFacet', Label: 'Customer Postal Addresses', Target: 'PostalAddresses/@UI.LineItem'}
],
  FieldGroup #Main : {Data : [
    {Value : firstName},
    {Value : lastName},
    {Value : email},
    {Value : status},
    {Value : creditScore}
  ]}
});

// new entity -- UI
annotate OrdersService.x_CustomerPostalAddresses with @(UI : {
  HeaderInfo       : {
    TypeName       : 'CustomerPostalAddress',
    TypeNamePlural : 'CustomerPostalAddresses',
    Title          : { Value : description }
  },
  LineItem         : [
    {Value : description},
    {Value : street},
    {Value : town},
    {Value : country_code}
  ],
  Facets           : [
    {$Type: 'UI.ReferenceFacet', Label: 'Main', Target : '@UI.FieldGroup#Main'}
  ],
  FieldGroup #Main : {Data : [
    {Value : description},
    {Value : street},
    {Value : town},
    {Value : country_code}
  ]}
}) {};

// new entity -- UI
annotate OrdersService.x_SalesRegion with @(
  UI: {
    HeaderInfo: {
      TypeName       : 'Sales Region',
      TypeNamePlural : 'Sales Regions',
      Title          : { Value : regionCode }
    },
    LineItem: [
      {Value: regionCode},
      {Value: name},
      {Value: descr}
    ],
    Facets: [
      {$Type: 'UI.ReferenceFacet', Label: 'Main', Target: '@UI.FieldGroup#Main'}
    ],
    FieldGroup#Main: {
      Data: [
        {Value: regionCode},
        {Value: name},
        {Value: descr}
      ]
    }
  }
) {};

// new entity -- UI
annotate OrdersService.x_Remarks with @(
  UI: {
    HeaderInfo: {
      TypeName       : 'Remark',
      TypeNamePlural : 'Remarks',
      Title          : { Value : number }
    },
    LineItem: [
      {Value: number},
      {Value: remarksLine}
    ],
    Facets: [
      {$Type: 'UI.ReferenceFacet', Label: 'Main', Target: '@UI.FieldGroup#Main'}
    ],
    FieldGroup#Main: {
      Data: [
          {Value: number},
          {Value: remarksLine}
      ]
    }
  }
) {};

Extending Array Values

Extend the existing UI annotation of the existing Orders entity with new extension fields and new facets using the special syntax for array-valued annotations.

cds
// extend existing entity Orders with new extension fields and new composition
annotate OrdersService.Orders with @(
  UI: {
    LineItem: [
      ... up to { Value: OrderNo },                             // head
      {Value: x_Customer_ID,            Label:'Customer'},     //> extension field
      {Value: x_SalesRegion.regionCode, Label:'Sales Region'}, //> extension field
      {Value: x_priority,               Label:'Priority'},     //> extension field
      ...,                                                     // rest
    ],
    Facets: [...,
      {$Type: 'UI.ReferenceFacet', Label: 'Remarks', Target: 'x_Remarks/@UI.LineItem'} // new composition
    ],
    FieldGroup#Details: {
      Data: [...,
        {Value: x_Customer_ID,            Label:'Customer'},      // extension field
        {Value: x_SalesRegion.regionCode, Label:'Sales Region'},  // extension field
        {Value: x_priority,               Label:'Priority'}       // extension field
      ]
    }
  }
);
// extend existing entity Orders with new extension fields and new composition
annotate OrdersService.Orders with @(
  UI: {
    LineItem: [
      ... up to { Value: OrderNo },                             // head
      {Value: x_Customer_ID,            Label:'Customer'},     //> extension field
      {Value: x_SalesRegion.regionCode, Label:'Sales Region'}, //> extension field
      {Value: x_priority,               Label:'Priority'},     //> extension field
      ...,                                                     // rest
    ],
    Facets: [...,
      {$Type: 'UI.ReferenceFacet', Label: 'Remarks', Target: 'x_Remarks/@UI.LineItem'} // new composition
    ],
    FieldGroup#Details: {
      Data: [...,
        {Value: x_Customer_ID,            Label:'Customer'},      // extension field
        {Value: x_SalesRegion.regionCode, Label:'Sales Region'},  // extension field
        {Value: x_priority,               Label:'Priority'}       // extension field
      ]
    }
  }
);

The advantage of this syntax is that you do not have to replicate the complete array content of the existing UI annotation, you only have to add the delta.

Semantic IDs

Finally, exchange the display ID (which is by default a GUID) of the new x_Customers entity with a human readable text which in your case is given by the unique property email.

cds
// new field in existing service -- exchange ID with text
annotate OrdersService.Orders:x_Customer with @(
  Common: {
    //show email, not id for Customer in the context of Orders
    Text: x_Customer.email  , TextArrangement: #TextOnly,
    ValueList: {
      Label: 'Customers',
      CollectionPath: 'x_Customers',
      Parameters: [
        { $Type: 'Common.ValueListParameterInOut',
          LocalDataProperty: x_Customer_ID,
          ValueListProperty: 'ID'
        },
        { $Type: 'Common.ValueListParameterDisplayOnly',
          ValueListProperty: 'email'
        }
      ]
    }
  }
);
// new field in existing service -- exchange ID with text
annotate OrdersService.Orders:x_Customer with @(
  Common: {
    //show email, not id for Customer in the context of Orders
    Text: x_Customer.email  , TextArrangement: #TextOnly,
    ValueList: {
      Label: 'Customers',
      CollectionPath: 'x_Customers',
      Parameters: [
        { $Type: 'Common.ValueListParameterInOut',
          LocalDataProperty: x_Customer_ID,
          ValueListProperty: 'ID'
        },
        { $Type: 'Common.ValueListParameterDisplayOnly',
          ValueListProperty: 'email'
        }
      ]
    }
  }
);

Localizable Texts

To externalize translatable texts, use the same approach as for standard applications, that is, create a i18n/i18n.properties file:

i18n/i18n.properties

properties
SalesRegion_name_col = Sales Region
Orders_priority_col = Priority
...
SalesRegion_name_col = Sales Region
Orders_priority_col = Priority
...

Then replace texts with the corresponding {i18n>...} keys from the properties file. Make sure to run cds build again.

Properties files must be placed in the i18n folder. If an entry with the same key exists in the SaaS application, the translation of the extension has preference.

This feature is available with @sap/cds 6.3.0 or higher.

Learn more about localization

About cds push

When run as cds push -s <subdomainName> (after logging in with another tenant), the command can activate the extension in other subdomains, effectively serving as a kind of cross-client transport mechanism.

TIP

To push a pre-packed extension archive (.tar.gz or .tgz), run cds push … <archive or URL>. The argument can be a local path to the archive or a URL to have it downloaded from.

Run cds push --help to see all available options.

About cds login

A passcode is a temporary authentication code, which is used to connect to the SaaS application. You received the Passcode URL from your tenant operator and it follows this pattern:

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

A passcode can be used only once and within a limited period of time. When expired, a new passcode has to be generated and sent again. If you omit the passcode when executing cds login, it is queried interactively.

The cds login command allows extension developers to save authentication tokens on the local machine with cds logout as its counterpart. It fetches a JWT token and save it for later use with this URL in either a plain-text file or a secure local storage. In order for secure storage to work, an additional Node.js module is needed:

sh
npm i -g keytar
npm i -g keytar

❗ Warning

In case of problems with keytar, you can add --plain, but this should only be done for non-productive tenants, since tokens are stored in plain text on the developer's machine.

CDS login usually works for several days, depending on configuration by the SaaS provider of the application. Once the prefetched token expires, commands requiring login (such as cds push) continue to work if a refresh token was originally obtained by cds login (again depending on SaaS configuration). In this case, a new token is automatically downloaded. Otherwise, you need to run cds login again with a fresh passcode.

Run cds login --help to see all available options.

About Adding Data to Extensions

As described in Add Data, you can provide local test data and initial data for your extension. In this guide we copied local data from the test/data folder into the db/data folder. When using SQLite, this step can be further simplified. For sap.capire.orders-Orders.csv, just add the new columns along with the primary key: `

csv
ID;x_priority;x_salesRegion_code
7e2f2640-6866-4dcf-8f4d-3027aa831cad;high;EMEA
64e718c9-ff99-47f1-8ca3-950c850777d4;low;APJ
ID;x_priority;x_salesRegion_code
7e2f2640-6866-4dcf-8f4d-3027aa831cad;high;EMEA
64e718c9-ff99-47f1-8ca3-950c850777d4;low;APJ

❗ Warning

Adding data only for the missing columns doesn't work when using SAP HANA as a database. With SAP HANA, you always have to provide the full set of data.

Predefined Mock Users

The following users are available for local testing as part of the basic/mock authentication strategy:

jsonc
{
  carol: { tenant: 't1', roles: ['admin', 'cds.Subscriber', 'cds.ExtensionDeveloper', 'cds.UIFlexDeveloper'] },
  dave:  { tenant: 't1', roles: ['cds.Subscriber'] },
  erin:  { tenant: 't2', roles: ['admin', 'cds.Subscriber', 'cds.ExtensionDeveloper'] },
  fred:  { tenant: 't2', roles: []  },
  yves:  { roles: ['internal-user'] }
}
{
  carol: { tenant: 't1', roles: ['admin', 'cds.Subscriber', 'cds.ExtensionDeveloper', 'cds.UIFlexDeveloper'] },
  dave:  { tenant: 't1', roles: ['cds.Subscriber'] },
  erin:  { tenant: 't2', roles: ['admin', 'cds.Subscriber', 'cds.ExtensionDeveloper'] },
  fred:  { tenant: 't2', roles: []  },
  yves:  { roles: ['internal-user'] }
}