Introduction:
In my last blog post, I introduced how to build a Fiori applicaiton by ABAP RESTful Programming Model (RAP).
In this blog post, I will use CAP (Cloud Application Programming Model) and Node.JS to rebuild the Fiori application in SAP Business Application Studio.
Let’s go over the requirements of the Fiori applicaiton.
- The Fiori application can list the Food Orders with key elements like Process Status, Processor, etc.
- Each ‘Food Order’ may has multiple line items.
- Users can filter the Food Orders by ‘Order Status’ and ‘Processor’.
- One processor can only process one food order at one time.
2. Example:
Before starting the development, I suggest you to read the CAP (Cloud Application Programming Model) guidence.
As the example will be introduced by CAP (Node.js) so I except you have some basic knowldge of Node.js.
Now let’s start the development process.
2.1 Create a new project in SAP Business Application Studio
There are two ways to do the development, Visio Studio Code or SAP Business Application Studio. If you are good with Visio Studio Code then you can follow the example from SAP Developer Center to configure the tool for the CAP development.
In this blog post, I will use SAP Business Application Studio to build the application.
Firstly, we should create a ‘Full Stack Cloud Application’ Dev Space in SAP Business Application Studio.
After that, we can create a new project from ‘CAP Project’ template (File -> New Project from Template) and select Node.js as the ‘runtime environment’ of the project.
2.2 Domain Modeling
As previously introduced, CDS (Core Data Services) plays a very important role in ABAP RESTful Programming model. This is similar in CAP development, we will use CDS to define the service and data model, and the service implementation also will be done by CDS.
Before continue read the blog post, I suggest you read the introduction of the CDS objects in the guidence. I will introduce some of the concepts within the blog post.
2.2.1 Data Model
We can use Definition Language (CDL) to create the data model.
You will find a default CDS file created under the ‘db’ folder which created with the project template. In this blog post, I will create a new CDS file ‘foodorders_db.cds’ under DB folder in the project.
using {Currency, managed, sap, cuid} from '@sap/cds/common';
namespace my.foodorders;
entity FoodOrder : managed {
key orderid : UUID @(Core.Computed : true);
ordernum : Integer;
status : Integer;
receivedtime : Timestamp;
processor : String;
orderstatus : Association to FoodOrderStatus;
orderprocessor : Association to FoodOrderProcessor;
foodorderitems : Composition of many FoodOrderItems on foodorderitems.order = $self;
}
entity FoodOrderItems : managed {
key itemid : UUID @(Core.Computed : true);
deliver_addr : String;
ord_comment : String;
payment_method : String;
order : Association to FoodOrder;
}
@cds.odata.valuelist
entity FoodOrderStatus : managed {
key status : Integer;
description : String;
}
@cds.odata.valuelist
entity FoodOrderProcessor : managed {
key processor : String;
status : Integer;
}
Some key concepts in the CDS file:
a. sap/cds/common
using {Currency, managed, sap, cuid} from '@sap/cds/common';
The introduction of ‘sap/cds/common’ is ‘CDS ships with a prebuilt model @sap/cds/common that provides common types and aspects.’ If you used some programming languages like JAVA or C# before, then you may family with this kind of annotation.
In this example, I will use Aspects ‘managed’ and ‘cuid’ from the prebuilt model.
For ‘managed’, I think it is similar to the ‘Managed Scenairo’ of Behavior Definitation in ABAP RESTful Programming Model. You will find some ideas on it from my previous blog post. The generic service will add elements to capture the create/update information of the related entity.
For ‘cuid’, just like the name, it will add canonical, universally unique primary key to the related enetity. The service will automatically fill the UUID-type key with auto-generated UUIDs.
I recommend you read the guidence to have a better understanding of the common types and aspects.
b. namespace
We can assign a namespace to each CDS file, the namespace will be auto used as the prefix of the entities created in the CDS file. I think this is useful when we need two eneities with same name under two CDS files.
c. entity
The eneties created in the CDS file will be created as database table. In this example, the enetites will be translated to tables in SQLite.
d. Association
Similar as ABAP RESTful Programming Model, CAP uses Associations to capture relationships between entities.
In this example, I will use the managed one-to-many association. As the code shows, there is no join clause assigned in the enetity. CDS will identify the foreign key from the target entities’s key.
e. Commposition
As the guidence mentioned, ‘Compositions are used to model document structures through “contained-in” relationships’. In this example, I will use commposition to define the relationship between the Food Order and Food Items (one to many).
2.2.2 Test Data
After create the data model, we should add some test data in the ‘db/data‘ folder.
my.foodorders-FoodOrderStatus.csv
status;description
1;Order Received
2;Order processing
3;Out of Deliver
4;Order Delivered
5;Order Finish
my.foodorders.FoodOrderProcessor.csv
processor;status
PR00000001;1
PR00000002;1
PR00000003;3
2.3 Service Definitions
2.3.1 Service Model
I will create a CDS file ‘foodorders_srv.cds’ under SRV folder in the project. The service is based on the Domain Model which we just created and will be exposed to the outside world as facades.
using my.foodorders as my from '../db/foodorders_db';
@path: 'service/foodorders'
service FoodOrdersService {
entity FoodOrder as projection on my.FoodOrder;
annotate FoodOrder with @odata.draft.enabled;
entity FoodOrderItems as projection on my.FoodOrderItems;
entity FoodOrderStatus as projection on my.FoodOrderStatus;
annotate FoodOrderStatus with @odata.draft.enabled;
entity FoodOrderProcessor as projection on my.FoodOrderProcessor;
annotate FoodOrderProcessor with @odata.draft.enabled;
}
2.3.2 UI Definition
using FoodOrdersService as my from './foodorders_srv';
@path: 'service/foodorders'
annotate my.FoodOrder with {
orderid @(
UI.Hidden
);
status @title: 'Status';
receivedtime @title: 'Received Time';
processor @title : 'Processor';
}
annotate my.FoodOrderItems with {
payment_method @title : 'Payment Method';
deliver_addr @title: 'Deliver Address';
ord_comment @title: 'Order Comment';
}
annotate my.FoodOrderStatus with {
status @title: 'Status';
description @title: 'Status Description';
}
annotate my.FoodOrderProcessor with {
processor @title: 'Processor ID';
name @title: 'Processor Name';
}
annotate my.FoodOrderStatus with @(
UI:
{
LineItem : [
{Value: status},
{Value: description}
],
Facets: [
{$Type: 'UI.ReferenceFacet', Label: 'Items', Target: '@UI.FieldGroup#Items'}
],
FieldGroup#Items:
{
Data: [
{Value: status},
{Value: description},
]
}
}
){ };
annotate my.FoodOrder with @(
UI: {
LineItem : [
{Value: status},
{Value: receivedtime},
{Value: processor}
],
Facets: [
{$Type: 'UI.CollectionFacet', Label: 'Food Order',
Facets: [
{$Type: 'UI.ReferenceFacet', Label: 'Header', Target: '@UI.FieldGroup#HeaderMain'},
{$Type: 'UI.ReferenceFacet', Label: 'Items', Target: 'foodorderitems/@UI.LineItem'},
]
}
],
FieldGroup#HeaderMain:
{
Data: [
{Value: status},
{Value: processor},
]
}
}
) {
status @(
Common.ValueListWithFixedValues : false,
Common.ValueListMapping : {
CollectionPath : 'FoodOrderStatus',
Label : 'Food Order Status',
Parameters : [
{
$Type : 'Common.ValueListParameterInOut',
LocalDataProperty : 'status',
ValueListProperty : 'status'
},
{
$Type : 'Common.ValueListParameterDisplayOnly',
ValueListProperty : 'description'
}
]
}
);
processor @(
Common.ValueListWithFixedValues : false,
Common.ValueListMapping : {
CollectionPath : 'FoodOrderProcessor',
Label : 'Food Order Processors',
Parameters : [
{
$Type : 'Common.ValueListParameterInOut',
LocalDataProperty : 'processor',
ValueListProperty : 'processor'
},
{
$Type : 'Common.ValueListParameterDisplayOnly',
ValueListProperty : 'status'
}
]
}
)
};
annotate my.FoodOrderItems with @(
UI: {
LineItem : [
{Value: payment_method},
{Value: deliver_addr}
],
Facets: [
{$Type: 'UI.ReferenceFacet', Label: 'Header', Target: '@UI.FieldGroup#ItemMain'}
],
FieldGroup#ItemMain:
{
Data: [
{Value: payment_method},
{Value: deliver_addr},
{Value: ord_comment}
]
}
}
) {
};
2.3.3 Custom Logic
As mentioned in requirement of the Fiori applicaiton, ‘One processor can only process one food order at one time’.
To handle the ruqimrent, we should add custom logic to the JavaScript file under SRV folder.
The code will be called before the CREATE action of ‘FoodOrder’ eneity.
Firstly, get the food order processor information, then use CDS Query Language (CQL) to get the Status of the food order processor.
If the processor’s status is ‘1’ (free to assign an order) then return the error message. Otherwise, then update the processor’s status to ‘2’ (processing an order).
module.exports = (srv) => {
// Check if the processor is busy with other orders
srv.before ('CREATE', 'FoodOrder', async (req) => {
const { FoodOrderProcessor } = srv.entities
const processor = await SELECT.one(FoodOrderProcessor).where({processor: req.data.processor})
if (processor.status != 1)
return req.error (400, 'Processor is busy with other food order!')
else
UPDATE (FoodOrderProcessor)
.set ({statue: 2})
.where ({processor: req.data.processor})
})
}
2.4 CDS Entities Deploy
As mentioned, the entities created in the CDS file will be synced to tables in SQLite database.
Create a new termial and run the commond below.
cds deploy --to sqlite
You should see the successfully deployed result.
2.5 Create Fiori Application
Open commond ‘Fiori: Open Applicaiton Generator’.
Select ‘List Report Object Page’.
Using the just created project as the data source.
Assign the Main eneity and the Navigation entity. Then assign the application information.
After that, the Fiori application information will be generated under the ‘app’ folder of the project.
3. Test
Click the ‘Start Debugging’ button.
As the terminal shows, the server is listening on http://localhost:4004 .
Open the Web Application by clicking the URL (hlocalhost:4004). A webpage will be opened, and then click the index.html under ‘Web Applications’.
Let’s try to create a new Food Order record.
As the selected Processor’s status is busy, so the error message popup.
a. Composition between Header and Items
As the page shows, one Food Order can links to multiple Items.
In Data Model, we defiened the Composition relationship between the entities FoodOrder and FoodOrderIteam.
foodorderitems : Composition of many FoodOrderItems on foodorderitems.order = $self;
In the UI definition, we used ‘UI.CollectionFacet’ to achieve this.
Facets: [
{$Type: 'UI.CollectionFacet', Label: 'Food Order',
Facets: [
{$Type: 'UI.ReferenceFacet', Label: 'Header', Target: '@UI.FieldGroup#HeaderMain'},
{$Type: 'UI.ReferenceFacet', Label: 'Items', Target: 'foodorderitems/@UI.LineItem'},
]
}
],
b. Value Help
Value Help is a very general requirement in developments, I will use the Value Help of field ‘Status’ as an example to introduce how to define it.
In Data Model, we need an Association links to the target entity ‘FoodOrderStatus’.
orderstatus : Association to FoodOrderStatus;
We should also add annotation ‘@cds.odata.valuelist’ to the target entity.
@cds.odata.valuelist
entity FoodOrderStatus : managed {
key status : Integer;
description : String;
}
In the UI definition, we can use ‘ValueListMapping’ to map the Local Data Property and Value List Property.
status @(
Common.ValueListWithFixedValues : false,
Common.ValueListMapping : {
CollectionPath : 'FoodOrderStatus',
Label : 'Food Order Status',
Parameters : [
{
$Type : 'Common.ValueListParameterInOut',
LocalDataProperty : 'status',
ValueListProperty : 'status'
},
{
$Type : 'Common.ValueListParameterDisplayOnly',
ValueListProperty : 'description'
}
]
}
);
4. Conclusion
In this blog post, I tried to introduce how to build a Fiori application by CAP (Node.js). I recommend you try to build your own application and share your experience in the comment.