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: 
vvdries
Contributor

Welcome to the “CAP Advanced Programming model #3” of this blogposts series. In this last post you will get a hands-on with the “DiscoveryCenterService” and the “ServicesHandler” to connect, consume and expose the SAP Business Technology Platform Services in your SAP Cloud Application Programming Model project.


An overview of the blogposts in this blogs series:












< reCAP 2023 - CAP Advanced Programming model #1 >
< reCAP 2023 - CAP Advanced Programming model #2 >
< reCAP 2023 - CAP Advanced Programming model #3 >

The GitHub repository of the project can be found here:


 https://github.com/lemaiwo/ReCAPBTPServiceOverview/tree/main


 



Discovery-Center Service


In the previous blogpost the “BaseService.ts” file was introduced and is used to extend the “DiscoveryCenterService.ts” file. This allows us to implement and consume the methods of the abstract “BaseService” class.


 


When consuming external services in your CAP project you will find the external service configuration in your “package.json” file. You can also ease the configuration and consumption of your external services via the SAP CAP Hybrid Testing feature.


 


In the DiscoveryCenterService the external/remote service name (package.json), in this case the SAP BTP Discovery Center Service API, is passed to the constructor. This will make sure that all to be configured requests will go to and through the respective external service.



constructor() {
super(“DISCOVERY_CENTER_API”)
}

 

The first request the “DiscoveryCenterService” class needs to be able to send is the “getServices” request. This method will send an HTTP request to the rest API to retrieve all general information regarding the BTP services.



public async getServices(): Promise<Array<IDiscoveryCenterEntity>> {
return (await this.cdsService.get(`/${this.entityName}`) as unknown as Array<IDiscoveryCenterEntity>);
}

 

Now that all general information can be retrieved, the service details must be fetched to enrich the service information.  


Therefore the “getServiceDetails” method was introduced to the “DiscoveryCenterService” class.


This method takes in one parameter as the service id and passes it as a URL-parameter to the external service.



public async getServiceDetails(id: string): Promise<IDiscoveryCenterServiceEntity> {
const serviceDetails = (await this.cdsService.get(`/${this.entityNameDetails}?serviceId='${id}'`) as unknown as { GetServicesDetails: string });
return JSON.parse(serviceDetails.GetServicesDetails) as IDiscoveryCenterServiceEntity;
}

 

Since the SAP Discovery Center API only allows a certain amount of request per milliseconds/seconds and the service details need to be fetched individually the “getAllServiceDetails” was added to the “DiscoveryCenterService” class. This method takes in one parameter as an array of services? and sends the requests for the service details in chunks of 20, to not exceed the limit of the SAP BTP Discovery Center Service API.



public async getAllServiceDetails(services: Array<IDiscoveryCenterEntity>) {
const chunkSize = 20;
let serviceDetailsResult: IDiscoveryCenterServiceEntity[] = [];
for (let i = 0; i < services.length; i += chunkSize) {
if (i > 0) await new Promise(resolve => setTimeout(resolve, 1000));
const chunkServices = services.slice(i, i + chunkSize);
let serviceDetailsPromises: Promise<IDiscoveryCenterServiceEntity>[] = [];
for (const service of chunkServices) {
serviceDetailsPromises.push(this.getServiceDetails(service.Id));
}
const responses = await Promise.all(serviceDetailsPromises);
serviceDetailsResult = [...serviceDetailsResult, ...responses];
}
return serviceDetailsResult;
}


 

Services Handler


In part 2 of this blogpost series the “BaseHandler.ts” file was introduced as well and is used to extend the “ServicesHandler.ts” file. This allows us to implement and consume the methods of the abstract “BaseHandler” class.


 

As described in the CAPire documentation an entity can have custom handlers for different events. This is required for external services which is why the required events need to be passed to the constructor. These configured events require a respective implementation method as well. An empty “cachedDiscoveryCenters” array is initialized as well, to store the cached BTP services information.



 constructor() {
super([
{ event: 'on', type: 'READ' }
]);
this.cachedDiscoveryCenters = [];
}

 

Since the event “onRead” is passed to the constructor, a method named “onRead” needs to be implemented as well.


 


This method checks if the BTP services were already fetched, and if so it does not request them over and over again.


If this is not the case yet, the method calls the “ServicesHandler” its “getBTPServices” method to retrieve the information. Another cool CAP feature which is being used here as well is the “cds.spawn” functionality. This allows us to retrieve the BTP services every X milliseconds to update the cache. (different options next to “every” are possible)


 


Once fetched or retrieved from cache, it checks the skip and top parameters and splices the result.


 


If requested, the count parameter is assigned as well and the result is returned.



 protected async onRead(req: any,next:any,skip:number,top:number) {
let discoveryCenterResult: IDiscoveryCenterList = [];
if (this.cachedDiscoveryCenters.length > 0) {
discoveryCenterResult = [...this.cachedDiscoveryCenters];
}

if (discoveryCenterResult.length < 1) {
cds.spawn({ every: 3600000 }, async (tx) => {
discoveryCenterResult = await this.getBTPServices();
})
if (this.cachedDiscoveryCenters.length === 0) {
discoveryCenterResult = await this.getBTPServices();
}
}

const total = discoveryCenterResult.length;
if(top < total)discoveryCenterResult.splice(skip, top);
if (req.query.SELECT.count) discoveryCenterResult['$count'] = total;
return discoveryCenterResult;
}

 

The last method in the “ServicesHandler.ts” file and consumed by the “onRead” method is the “getBTPServices” method. This method requests the “DiscoveryCenterService” via the “BaseService” and fetches all the the BTP services via the “getServices” method of this “DiscoveryCenterService” instance.


 


Once the services have been retrieved they are passed to the “getAllServiceDetails” method of the “DiscoveryCenterService” instance to retrieve all service details. The result is processed to match the returning entity and is stored in the “cachedDiscoveryCenters” array before returning it.



private async getBTPServices(): Promise<IDiscoveryCenterList> {
this.discoveryCenterService = await this.getService(DiscoveryCenterService);
let discoveryCenterResult: IDiscoveryCenterList = [];
let discoveryCenter = await this.discoveryCenterService.getServices();
const serviceDetailsResult = await this.discoveryCenterService.getAllServiceDetails(discoveryCenter);

for (const service of discoveryCenter) {
const serviceDetail = serviceDetailsResult.find(serviceDetail => serviceDetail.Id === service.Id);
if (serviceDetail) {
for (const servicePlan of serviceDetail.servicePlans) {
for (const env of servicePlan.environments) {
discoveryCenterResult.push({
...service,
ServicePlan: servicePlan.Code,
ServicePlanName: servicePlan.Name,
Infrastructure: env.Infrastructure,
Platform: env.Platform,
region: env.Region
});
}
}
}
}
this.cachedDiscoveryCenters = [...discoveryCenterResult];
return discoveryCenterResult;
}

 

This concludes the full implementation of the handler and service file to expose the BTP services and their details. In case of an external service where write operations need to be foreseen, they can easily be implemented in the handler and service file as well.


 

Wrap-up


This setup allows the developer to separate the code in a reusable structure and implement event handling code in a clean and understandable way. Services can be used over all handlers and handlers are not implementing the same code over and over again. In case a different “service.cds” file contains the same entity and would be in need of exact the same handler, a handler file can be foreseen which only imports and exports the handler implementation of another “service.cds” file, this way the handler itself is reusable as well and code only needs to be maintained once!

With this I would like to wrap up this blog series and give a big thanks to Wouter & Gert for the awesome experience! It was fun to prepare and present at the reCAP 2023 event! 😎🧢

Cheers,

Dries

Labels in this area