Extending SaaS Applications
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:
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:
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:
- Clone our
cap/samples
repository
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
- Ensure to have the latest version of
@sap/cds-dk
installed globally:
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:
cd orders
cds add mtx
npm update
cd orders
cds add mtx
npm update
Essentially, this automates the following two steps...
- Add
@sap/cds-mtxs
package dependency:
npm add @sap/cds-mtxs
npm add @sap/cds-mtxs
- Switch on
cds.requires.extensibility
in your package.json:
package.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
{
"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:
- How to setup test tenants for extension projects
- How to assign requisite roles to extension developers
- How to start extension projects from provided templates
- How to find deployed app urls of test and prod tenants
- What can be extended? → which services, entities, ...
- With enclosed documentation to the models for these services and entities.
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:
Create a new CAP project —
orders-ext
in our walkthrough:shcd .. cds init orders-ext code orders-ext # if you use VSCode
cd .. cds init orders-ext code orders-ext # if you use VSCode
Add this to your package.json:
package.json
{
"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 bycds pull
. This in turn allows us/the extenders to refer to the app's base model, likeusing ... from '@capire/orders'
.
Add Sample Content
Create a new file app/extensions.cds and fill in this content:
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 },
...
];
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:
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:
# 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:
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
{
"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
:
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...
Open a new terminal, and in there subscribe as customer tenant
t1
:shcds subscribe t1 --to http://localhost:4006 -u carol:
cds subscribe t1 --to http://localhost:4006 -u carol:
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 tenantt1
.
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...
Set up a test tenant
t1-ext
shcds subscribe t1-ext --to http://localhost:4006 -u carol:
cds subscribe t1-ext --to http://localhost:4006 -u carol:
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
, assigningbob
as extension developer fort1-ext
:
package.json: cds > requires > auth > users
{
"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:
code ../orders-ext
code ../orders-ext
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.
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:
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
:
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:
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:
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:
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.
cds build
cds push --to http://localhost:4006 -u bob:
cds build
cds push --to http://localhost:4006 -u bob:
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.
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:
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.
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:
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
:
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.
// 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
.
// 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
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.
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:
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: `
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:
{
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'] }
}