Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
jhodel18
Active Contributor
In my last blog post Keep the Core Clean with CAP Model, we found out that CAP's service API doesn't support handling the draft-generated properties out-of-the-box, however, it's technically feasible to handle this ourselves in the custom handler. And you might be wondering, has anyone done it? Well, the answer is yes, because I did!

Let me backtrack and recall how I got here in the first place. First, I wanted to do a side-by-side extensibility using standard OData APIs with the goal in mind to keep the core clean. Then I looked into the available frameworks to help me achieve that, which are the RAP and CAP frameworks. And then I quickly realized that there was very little support in making this happen because both frameworks only supported the OData V2 service (non-draft) for this scenario. The golden standard now (the year 2023) for building applications from scratch is using Fiori Elements with draft capabilities (OData V4 is implied here). It's hard to settle for something less, especially when development is so much easy when using Fiori Elements with draft.

Therefore, if there's a tiny sliver of chance of making this functionality work, then I will take it. And so I did take that opportunity since CAP doesn't completely block me from implementing functionality through a custom handler! However, the road was bumpy and so painful because there are so many case scenarios that you need to write the custom logic for. And I believe that no developer should be subjected to such kind of torture ever again! That's why I made the Rizing CDSX (@rizing/cds-extension) module so you don't have to suffer like I did. If that sounds interesting to you, then please continue reading.

 


 

The Demo Project






Just like in the previous blog post that uses the CAP framework, the demo project can be found in the same GitHub repository: https://github.com/jcailan/cap-fe-samples.

 

Data Model


For the data model definition, we need to create an entity which I call Shadow Persistence Entity. This Shadow Persistence Entity (or SPE for short) simply means that the entity from a remote service is recreated as a CAP persistence entity. In some ways, it's like tricking the CAP framework that this is the real persistence entity, but in actual fact, the data will be persisted in a remote server using an external service.

Data Model: db > remote.cds
context remote {

@cdsx.api: 'product_external'
entity Products {
key ID : UUID;

@mandatory
name : String;

@mandatory
description : String;
imageUrl : String;
releaseDate : DateTime;
discontinuedDate : DateTime;

@mandatory
price : Decimal(16, 2);
height : Decimal(16, 2);
width : Decimal(16, 2);
depth : Decimal(16, 2);

@(
mandatory,
assert.range: [
0.00,
20.00
]
)
quantity : Decimal(16, 2);

@mandatory
UnitOfMeasure_ID : String(3);

@mandatory
Currency_ID : String(3);
DimensionUnit_ID : String(2);

@mandatory
Category_ID : String(1);
Supplier_ID : UUID;
createdBy : String;
createdAt : Timestamp;
modifiedBy : String;
modifiedAt : Timestamp;
}
}

The above cds file is an example of SPE. It's a replication of the entity definition of the Products entity for the remote service called product_external. The SPE contains the typical CAP CDS annotations except only for one annotation at the entity level -- @cdsx.api. The @cdsx.api annotation is used to specify that this Products entity should be proxied by an external service called product_external. If you have already worked on CAP that consumes external services before then you are already aware that this external service is configured in the package.json file, otherwise, you might want to check my earlier blog post that discusses this topic.

 

Service Model


The service model definition is very straightforward, it just contains the projection to the SPE Products entity with the @odata.draft.enabled annotation.

Service Model: srv > ProductCDSX.cds
using {remote} from '../db/remote';

service ProductCDSX {

@odata.draft.enabled
entity Products as projection on remote.Products;

}

 

Remote Service Configuration


It goes without saying that I have used cds import command to import the remote service's metadata.xml file to generate the so-called service definition file (csn file). And with this step, a remote service configuration is automatically added to my package.json file.

Configuration: package.json
	"cds": {
"requires": {
"product_external": {
"kind": "odata-v2",
"model": "srv/external/products"
}
}
}

 

Rizing CDSX Module


The Rizing CDSX module (or just CDSX) is easy to set up. Of course, like any other node module, you need to install it first and then bootstrap it into CAP's runtime by using one of CAP's available hook events.

Bootstrap CDSX: srv > server.js
const cds = require("@sap/cds");
const cdsx = require("@rizing/cds-extension");

cds.on("bootstrap", (app) => {
cdsx.load();
});

Just like that, it's very easy!

 

Testing






Now it's time to test the CAP service powered by CDSX! Since we are using the OData draft feature, it's best that we test this using a Fiori Element app. Luckily, the sample project is already fitted with a generated Fiori Element app and is ready to test.

  • just run the command cds watch as normal

  • then launch the only web application available -- launchpage.html

  • finally, click on the tile Manage Products (External Service) CDSX


And you will be able to test a Fiori Element app backed by a CAP-based OData V4 service with draft capabilities.



 

Closing






In my journey of finding the ideal solution for keeping the core clean and trying to make use of the latest available SAP frameworks, I found myself hitting some limitations. Through CAP's guiding principle of being an open but opinionated framework, I found a tiny sliver of hope that I could bridge the gap and implement the solution myself. And that solution was developed as a reusable node module which then gave birth to what I call now the Rizing CDSX -- it is easy to use and I find it very helpful as a complementary tool/framework that helps in keeping the core clean. I hope you may find it useful as well.

 

 

 

~~~~~~~~~~~~~~~~

Appreciate it if you have any comments, suggestions, or questions. Cheers!~
11 Comments
Ahmedkhan29789
Participant
Great explanation Jhodel, It's very informative.

 

I was wondering how easy it is to create a value help using fiori elements in your cds entity you have created.

Suppose, I click on create button and in that i want to choose the product in product field from a value help and fill rest of the fields for that product, the value help will fetch the list of products available in the api

 

I had a previous experience that it is not achieveable easily by using fiori elements and for this kind of scenario i have to choose the UI5 at last.

 

Try this kind of scenario sometime as well and if you achieve it very simply do share your experiences please.
jhodel18
Active Contributor
0 Kudos
Hi Ahmed,

Thanks for the feedback! I'm not sure I understand your requirement, because the example you gave is a bit off to me. The fictional API I used in the demo is products, so effectively the app I created is like a master data management app for products. Therefore, I can't use the same external service as value help, especially when creating a new product.

But if what you really mean is to create a value help against another property, let's say category, then it's relatively easy. All you have to do is create another entity for this value help like you normally would using CAP (persisted in HANA Cloud DB), or you could use another external API and then follow the same principle of creating an SPE with the @cdsx.api annotation.
david_kunz2
Advisor
Advisor
Hi jhodel18 ,

 

Cool stuff! Watch out for my talk at https://recap-conf.dev/ "A Leaner Approach to Draft", that might be useful for you!

 

Best regards,

David
jhodel18
Active Contributor

Hi david.kunz2 ,

Thanks for the feedback! Will definitely watch out for that, my talk will start in 10 minutes after yours. See you there! 🙂

Thanks and regards,

Jhodel

ulyssesbonfim
Explorer
0 Kudos
Hi David,

That seems a spot on feature when integrating with SAP ECC systems.

Would it be possible to share the demo files from your presentation?

 

I'm mostly intrigued by the srv.__datasource = srv 

I suppose that makes the whole implementation of cds.ApplicationService delegate implementation to srv for all entities? What about entities that are fully handled by CAP application or fully remote?

The documentation available on cap doesn't go as far as your presentation when covering the subject unfortunately .

 

Thanks
david_kunz2
Advisor
Advisor
Hi Ulysses,

Thank you! The srv._datasource = srv feature is still internal and I share your concerns that you might only want to consider one entity to be remote. As soon as we drop support for the old draft implementation (which blocks us from making more changes), we will look into supporting this use case officially.

Best regards,
David
ulyssesbonfim
Explorer
0 Kudos

I suppose that would be a likely scenario, since if the app is being handled by CAP the chances are that is not only for the draft capabilities but to also mix with entities that are CAP only.

 

Couldn't this check be based on CDS annotations rather than setting srv.__datasource ? I mean

@cds.external : true

@cds.persistence.skip : true

 

Does the lean draft changes anything on how the entities keys are exposed?

Today if @odata.draft.enabled it will add IsActiveEntity as key to the entity. Will this change with lean draft?

 

It would be great if you could share the code of your presentation. If that's not possible could you list the necessary steps to implementation the same scenario?

So far this is what I understood that needs to happen on CAP app1 (the one Fiori is consuming)

  • Create a entity with the same name and structure as the remote one (app2:
    ConferenceSrv) but omitting or setting to false @cds.external and @cds.persistence.skip 
  • This will create two db tables Conference and Conference.draft (not sure about this part)
  • Set srv.__datasource = srv when starting the cds.ApplicationService
  • Handle queries for CRUD actions on entity Conference to re-direct them to app2(
    ConferenceSrv), removing all Draft properties from the original query
const conferenceSrv = await cds.connect.to("ConferenceSrv")
srv.__datasource = srv

srv.on (["CREATE", "READ", "UPDATE", "DELETE"], "Conference", async (req) => {
if (req. query. SELECT) {
if (!req.query.SELECT.columns) {
req.query.SELECT.columns = []
for (const el in req.target.elements) {
req.query.SELECT.columns.push({ ref: [el] })
}
}
req.query.SELECT.columns = req.query.SELECT.columns.filter((c) =>
!([ "IsActiveEntity",
"SiblingEntity",
"DraftAdministrativeData_DraftUUID",
"DraftAdministrativeData",
"HasDraftEntity",
"HasActiveEntity"
]. includes (c.ref?. [0]))
)
}
return conferenceSrv.run(req.query)
})

 

Appreciate the feedback.

 

(@Jhodel, sorry to hijack your comment section 😅)

david_kunz2
Advisor
Advisor
Hi Ulysses,

That's a great suggestion to re-use @cds.external. We will definitely consider this as it would mean that stakeholders don't need to know about any other mechanism, they can just implement their custom handlers as usual. We need to carefully check this, but first we need to remove support for the old draft, as this is quite a substantial change. Here's the code from the demo, but please be aware that it most probably will change in the future.

Best regards,
David

 
  srv._datasource = srv;

srv.on(["CREATE", "READ", "UPDATE", "DELETE"], "Conference", async (req) => {
if (req.query.SELECT) {
if (!req.query.SELECT.columns) {
req.query.SELECT.columns = [];
for (const el in req.target.elements) {
req.query.SELECT.columns.push({ ref: [el] });
}
}
req.query.SELECT.columns = req.query.SELECT.columns.filter((c) =>
!([
"IsActiveEntity",
"SiblingEntity",
"DraftAdministrativeData_DraftUUID",
"DraftAdministrativeData",
"HasDraftEntity",
"HasActiveEntity",
].includes(c.ref?.[0]))
);
}
return conferenceSrv.run(req.query);
});
ulyssesbonfim
Explorer
0 Kudos
Cool, super thanks!

 

Do we need to create the same entities on app1 though?

Could you share the .cds files?
david_kunz2
Advisor
Advisor
0 Kudos

Yes, the same entity needs to be in the other app.

 

app1:

---

namespace draftApp;

using { conferenceApp } from 'conferenceapp';

entity Conference : conferenceApp.Conference {};

----

using { draftApp } from '../db/schema.cds';

service DraftSrv {
entity Conference as projection on draftApp.Conference;
}


app2:

namespace conferenceApp;

entity Conference {
key ID: UUID;
name: String(200);
startDate: DateTime;
endDate: DateTime;
location: String(200);
description: String(500);
organizer: String(200);
}

---

using conferenceApp from '../db/data-model';

service ConferenceSrv {
entity Conference as projection on conferenceApp.Conference;
}
----
using from './srv/cat-service.cds';

chholzermsg
Participant
0 Kudos
Hi jhodel18 ,

your lib would simplify things really a lot! Unfortunately I get a dump, when I try it out:

"msg":"context.getUrlObject is not a function",
"stacktrace":["TypeError: context.getUrlObject is not a function",
"at Object.reject (/home/vcap/app/node_modules/@rizing/cds-extension/lib/util/cdsx.js:97:41)",
"at module.exports (/home/vcap/app/node_modules/@rizing/cds-extension/lib/crud/read.js:215:8)",
"at next (/home/vcap/app/node_modules/@sap/cds/lib/srv/srv-dispatch.js:79:36)",
"at ApplicationService.handle (/home/vcap/app/node_modules/@sap/cds/lib/srv/srv-dispatch.js:83:6)",
"at process.processTicksAndRejections (node:internal/process/task_queues:95:5)",
"at async Promise.all (index 0)","at async all (/home/vcap/app/node_modules/@sap/cds/libx/_runtime/fiori/lean-draft.js:543:34)",
"at async cds.ApplicationService.handle (/home/vcap/app/node_modules/@sap/cds/libx/_runtime/fiori/lean-draft.js:201:20)",
"at async _readCollection (/home/vcap/app/node_modules/@sap/cds/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js:249:19)",
"at async /home/vcap/app/node_modules/@sap/cds/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js:480:16"],"id":"1598741",

It is just one table I use it for.

I use the following versions:

@cap-js/cds-types: 0.1.0
@cap-js/sqlite: 1.4.0
@sap/cds: 7.5.2
@sap/cds-compiler: 4.5.0
@sap/cds-dk: 7.5.1
@sap/cds-dk (global): 7.4.1
@sap/cds-fiori: 1.2.2
@sap/cds-foss: 5.0.0
@sap/cds-mtxs: 1.14.2
@sap/eslint-plugin-cds: 2.6.4
Node.js: v18.14.2

Regards

Christian
Labels in this area