Background
TL;DR – When building front-end applications, we want to make data visually accessible and editable to the user. The application relies on correctness, availability and reliability of the data source to bring the best in class user experience to the end user. To guarantee the seamless integration, the back-end system needs to be aware of the application’s requirements. Unfortunately, this is not assured in most cases, because most applications are built against legacy systems that originally had been build for a different use case. Given that fact, application developers face challenges to adapt the application logic, data processing and back-end connectivity by implementing client-side workarounds or complicated application flows.
In this blog post, I want to write about how an application developer can approach the challenge of working with such systems to avoid potential long-term issues.
It should be noted that a close working relationship between front-end, back-end developers as well as UI/UX design is absolutely crucial for success not only for greenfield applications but also for building on legacy systems.
Working with legacy systems
A potential issue with legacy systems is that the provided APIs and data structures are not or only partially suited for the front-end application’s use case. This reflects in deeply nested data structures or large amount of data being provided, that is not needed on the client. Complex parsing functionality to prepare the data in a format expected by the client is a potential result of this. At this point we are not even considering long response times or complicated authentication flows that can cause issues on the client side. Taking all potential factors into account the general interaction with such a system can be expensive, time-consuming on the implementation, code becomes hard to maintain and overall slows down the clients performance.
Another problem, client developers face are dependencies on multiple back-end systems. In enterprise it is common that multiple systems are interconnected with each other to represent a complex business scenario. That means, the client has to reach out to multiple systems to fetch the required data and execute business relevant procedures. This complexity should not be handled on the client and an abstraction layer would be best to ease the implementation efforts for the client.
Unfortunately, we don’t often have the luxury to define the back-end system, APIs and the underlying data structure to the exact needs for the front-end application. Instead of writing complex code to even out the above described issues, a developer needs a solution where it is possible to define and retrieve the data needed, in a structure that is expected, with the back-end functionality required for a successful business process on the client.
I’ve touched on some of the issues client developers face when interacting with systems not suited for their purpose but what are the implications of these issues?:
High maintainability
Back-end systems, which are being consumed by applications who weren’t thought of in the original design of the system itself run into issues with data structure, authentication flows, slow load times, and missing back-end features. The consuming application has to adapt to even out these points, which most often result in an extensive or complicated code base. If the back-end updates, or changes in any way, the client has to touch that code to keep consuming the provided APIs and data. Touching the data service layer within a front-end application is not uncommon to keep up-to-date with the backend system but it can be hard to maintain if the code being written has to touch on all the before mentioned complications.
Complicated testability
Like with the issue of high maintainability, complex testability is almost inherent because of the nature of the application’s code base for handling the back-end interaction and data processing.
Back-end complexity and long response times
Because the back-end system was designed without the client’s requirements in mind, the interaction with the back-end can be spotty (connection issues) or complex. The backend might perform in unexpected ways, perform too slowly because of it’s legacy or the massive amount of computing needed to produce the requested result. That can cause timeouts on the client side, because of available system resources allocated for the application or to keep user interaction in flow due to the client UIs asynchronous nature.
The application has to think of different mechanisms to compensate the backends behavior like caching mechanisms, persistence (delta handling between client and back-end), data processing or concurrency.
Thinking about the consequences of handling these issues on the client will eventually cause runtime problems , on top of the code issues described above, that need to be avoided. Finding a solution is crucial to achieve a great user experience.
If a re-write of the back-end system is not possible, what could be potential solutions?
Solutions
Build a new API
Technically, the simplest solution is to provide an API that provides only the data needed for the client and so executes the requested operations in a light-weight manner providing smaller amounts of data. Also, keeping data operations simple and business processes quick. This approach requires development resources on the back-end side to implement a new API, but most importantly the underlying functionality for abstracting complexity and potential performance issues with older systems. This results in a high demand of experienced developers, domain expertise, money and most importantly time.
Abstract the back-end data structure using SAP Graph and GraphQL
SAP Graph is defined as:
“Extending traditional API Management, Graph enables you to expose all your business data in the form of a semantically connected data graph, accessed via a single unified and powerful API.
Graph is a capability of API Management within SAP Integration Suite. With Graph, developers access your business data as a single semantically connected data graph, spanning the suite of SAP products and beyond.” (What Is Graph? – help.sap.com)
Whereas GraphQL, is a querying language for APIs and it is providing a runtime for fulfilling those queries with existing data.
You can ultimately combine both technologies with each other to expose data from multiple backends using SAP Graph and enable querying the data through GraphQL.
Use CAP to create a db-less middleware service
Think about a scenario where you have two different back-end services providing data through an API providing big data sets. A client needs to consume both APIs to provide value to end users by displaying relevant data and enabling users to execute CRUD (Create, Read, Update, Delete) operations on that data.
The client has the issue that the back-end service is providing data which is not relevant to it’s use case. An approach is to provide a middleware abstracting complexity from the client. It should also provide a single entry point for the client to operate on the data of both back-end services, that means the middleware has to aggregate data and redirect the client’s requests to the respective service.
In the following example, I want to show you how to use the Cloud Application Programming Model (CAP) for exposing and abstracting APIs. Authentication is not being covered as I want to focus on the data portion and how CAP can be utilized.
Looking at the solution diagram above, there are two services needed to be exposed to the client: Sales Service and Business Partner Employee Service. The Sales Service is a basic CAP OData service deployed on SAP BTP, Cloud Foundry runtime using SAP HANA Cloud as persistence. The Business Partner Employee OData Service is a SAP Cloud for Customer service which can be explored through SAP API Accelerator and is part of the S/4HANA suite.
The employee CAP OData service, is the db-less middleware service being implemented to aggregate and abstract both of the other services. The service implementation makes sure to only provide data needed by the client in one aggregated data set. For the client, the only data relevant is the employee and the related sales of the employee.
The client needs to display an employee detail view which shows information about the employee and all sales that employee is currently working on or has already completed. The employee information will be fetched from the Business Partner service and the sales information from the Sales Service. The only association between these two are by employee ID. Before looking into how CAP can be used to provide a custom entity bringing both information together, let’s look into the data structure of both services.
Business Partner – Employee Collection
The business partner service contains dozens of entities, but in this scenario only the employee collection is required. Looking into the employee collection OData metadata document it is clear that all that properties in there are not needed for the client’s requirements. That means, the Employee Service middleware needs to expose only the necessary properties (I have stripped out more than half of the properties for readability):
<EntityType Name=”EmployeeCollection”>
<Key>
<PropertyRef Name=”ObjectID”/>
</Key>
<Property Name=”ObjectID” Type=”Edm.String” MaxLength=”70″ Nullable=”false”/>
<Property Name=”EmployeeID” Type=”Edm.String” MaxLength=”20″/>
<Property Name=”UUID” Type=”Edm.Guid”/>
<Property Name=”UserID” Type=”Edm.String” MaxLength=”40″/>
<Property Name=”IdentityUUID” Type=”Edm.Guid”/>
<Property Name=”GlobalUserID” Type=”Edm.String” MaxLength=”36″/>
<Property Name=”BusinessPartnerID” Type=”Edm.String” MaxLength=”10″/>
<Property Name=”EmployeeValidityStartDate” Type=”Edm.Date”/>
<Property Name=”EmployeeValidityEndDate” Type=”Edm.Date”/>
<Property Name=”BusinessPartnerFormattedName” Type=”Edm.String” MaxLength=”480″/>
<Property Name=”TitleCode” Type=”Edm.String” MaxLength=”4″/>
<Property Name=”TitleCodeText” Type=”Edm.String”/>
<Property Name=”AcademicTitleCode” Type=”Edm.String” MaxLength=”4″/>
<Property Name=”AcademicTitleCodeText” Type=”Edm.String”/>
<Property Name=”FirstName” Type=”Edm.String” MaxLength=”40″ Nullable=”false”/>
<Property Name=”MiddleName” Type=”Edm.String” MaxLength=”40″/>
<Property Name=”LastName” Type=”Edm.String” MaxLength=”40″ Nullable=”false”/>
<Property Name=”SecondLastName” Type=”Edm.String” MaxLength=”40″/>
<Property Name=”NickName” Type=”Edm.String” MaxLength=”40″/>
<Property Name=”GenderCode” Type=”Edm.String” MaxLength=”1″/>
<Property Name=”GenderCodeText” Type=”Edm.String”/>
<Property Name=”MaritalStatusCode” Type=”Edm.String” MaxLength=”1″/>
<Property Name=”MaritalStatusCodeText” Type=”Edm.String”/>
<Property Name=”LanguageCode” Type=”Edm.String” MaxLength=”2″/>
<Property Name=”LanguageCodeText” Type=”Edm.String”/>
<Property Name=”NationalityCountryCode” Type=”Edm.String” MaxLength=”3″/>
…
<NavigationProperty Name=”BusinessUser” Type=”businesspartner.BusinessUserCollection”/>
<NavigationProperty Name=”EmployeeOrganisationalUnitAssignment” Type=”Collection(businesspartner.EmployeeOrganisationalUnitAssignmentCollection)”/>
<NavigationProperty Name=”EmployeeSalesResponsibility” Type=”Collection(businesspartner.EmployeeSalesResponsibilityCollection)”/>
…
</EntityType>
Sales Service
The sales service is far more basic, it just exposes the different sales and sales status corresponding to an employee ID. So no need to implement an abstraction here:
<EntityType Name=”Sales”>
<Key>
<PropertyRef Name=”ID”/>
</Key>
<Property Name=”ID” Type=”Edm.Guid” Nullable=”false”/>
<Property Name=”createdAt” Type=”Edm.DateTimeOffset” Precision=”7″/>
<Property Name=”createdBy” Type=”Edm.String” MaxLength=”255″/>
<Property Name=”modifiedAt” Type=”Edm.DateTimeOffset” Precision=”7″/>
<Property Name=”modifiedBy” Type=”Edm.String” MaxLength=”255″/>
<Property Name=”customer” Type=”Edm.String”/>
<Property Name=”employeeID” Type=”Edm.Guid”/>
<Property Name=”salesVolume” Type=”Edm.Int32″/>
<Property Name=”status” Type=”Edm.String”/>
</EntityType>
Employee Service
Alright, now let’s take a look into the Employee Service middleware. All needed there is the employee and the corresponding sales:
<ComplexType Name=”EmployeeResultSet”>
<Property Name=”employee” Type=”Edm.String”/>
<Property Name=”sales” Type=”Edm.String”/>
</ComplexType>
Implementation
I am not going into detail on how to integrate with external services, here I am focusing on data abstraction and providing a custom data structure needed by the client. If you want to learn on how to integrate with external services, take a look at the content for the Service integration with SAP Cloud Application Programming Model Codejam content on GitHub.
Create a projection on businesspartner.EmployeeCollection
First thing, create that abstracted entity for the above shown monster of an entity. With CAP and cds this is really easy, the only thing needed is an entity projection on the original entity. This allows for defining the properties needed by the client.
using { businesspartner } from ‘./businesspartner’;
namespace s4.bp;
entity Employees as projection on businesspartner.EmployeeCollection {
key UUID as ID,
EmployeeID as employeeID,
TitleCodeText as jobTitle,
FirstName as firstName,
LastName as lastName,
OfficePhoneNumber as phoneNumber,
Email as email
};
The above shown cds definition makes sure that only the defined properties get exposed to the consuming client, it also allows for a redefinition of property names. That is all a developer has to do in order to hide the vast amount of properties included in the Employee Collection of Business Partner.
Definition of the Employee Service
A projection for the sales service is not necessary because it already is in the format needed by the client, that means I can continue with the service definition for the middleware API. I call this EmployeeService:
service EmployeeService {
type EmployeeResultSet {
employee : String;
sales : String;
};
function fetchEmployee (forID: String) returns EmployeeResultSet;
}
Employee Service includes a custom type EmployeeResultSet , that exposes the employee from the Employee Collection and the related sales from Sales Service. The service defines a fetch function to retrieve the custom type EmployeeResultSet by providing the employee ID, and that is all for the service definition.
Before going into details on how to implement the service itself, one thing needs to be done first, the Employee Service has to be extended with the defined external Employees projection in order to make it known to the middleware.
using { s4.bp } from ‘./external/bp_simple_employee’;
using { EmployeeService } from ‘./employee-service’;
extend service EmployeeService with {
entity Employees as projection on bp.Employees;
}
Now, Employee Service knows about the projected Employees entity from the external business partner service.
Implementation of the Employee Service
In this example, I am using CAP with Node.js but the same approach applies to Java based projects.
For the Employee Service a implementation is needed to bring in business logic for the new middleware service. In the implementation, I am simply implementing the fetchEmployee(forID: String) function making sure that the correct employee is being fetched from the business partner service and that the related sales are being fetched from the sales service.
module.exports = (EmployeeService) => {
EmployeeService.on(‘fetchEmployee’, async function(req) {
const employeeID = req.data.forID
const employee = await fetchBPEmployee(employeeID)
const sales = await fetchSales(employeeID)
return { employee, sales }
})
}
The function does nothing more than fetching the employee and the sales with the help of the employee ID and returning it as the defined custom type EmployeeResultSet.
To fetch the required data, two custom methods are being implemented: fetchBPEmployee and fetchSales. Both, establish connection to the external service and sending a GET request to retrieve the needed data:
async function fetchBPEmployee(employeeID) {
const bpService = await cds.connect.to(‘EmployeeService’)
const employee = await bpService.run(
SELECT(‘*’)
.from(‘Employees’)
.where(
`employeeID = ‘${employeeID}’`
))
return employee
}
Did you notice that our request goes against the defined projection on EmployeeService and not the businesspartner.EmployeeCollection. This is exactly what is need, the request goes towards the projection on the external service and than gets routed to the actual back-end service. The projection makes sure that only the required data gets put into the response body:
// projection: http://localhost:4004/odata/v4/employee/Employees
// actual back-end: http://localhost:4004/odata/v4/businesspartner/EmployeeCollection
{
“@odata.context”: “$metadata#Employees”,
“value”: [
{
“ID”: “00163E03-A070-1EE2-88BA-37FB8F5F50A9”,
“employeeID”: “E1000”,
“jobTitle”: “Sales Representative”,
“firstName”: “Mike”,
“lastName”: “Summers”,
“phoneNumber”: “+16505555014”,
“email”: “ussalesrep01@sap.com”
},
{
“ID”: “00163E03-A070-1EE2-88BA-384A3440D0AA”,
“employeeID”: “E1001”,
“jobTitle”: “Sr. Sales Representative”,
“firstName”: “Phil”,
“lastName”: “Hughes”,
“phoneNumber”: “+16505555015”,
“email”: “ussalesrep02@ondemand.com”
}
]
}
The sales service is straight forward, requests get send directly to the service and the response is served back without mutation.
async function fetchSales(employeeID) {
const salesService = await cds.connect.to(‘SalesService’)
const sales = await salesService.run(
SELECT(‘*’)
.from(‘SalesService.Sales’)
.where(
`employeeID = ‘${employeeID}’`
))
return sales
}
The result looks like this:
{
“@odata.context”: “$metadata#Sales”,
“value”: [
{
“ID”: “2b23bb4b-4ac7-4a24-ac02-aa10cabd842c”,
“createdAt”: null,
“createdBy”: null,
“modifiedAt”: null,
“modifiedBy”: null,
“customer”: “Wood And Planks Inc.”,
“employeeID”: “E1000”,
“salesVolume”: 150000,
“status”: “closed”
},
{
“ID”: “6ccf474c-3881-44b7-99fb-59a2a4668418”,
“createdAt”: null,
“createdBy”: null,
“modifiedAt”: null,
“modifiedBy”: null,
“customer”: “TechnoCracks unlimited”,
“employeeID”: “E1001”,
“salesVolume”: 325000,
“status”: “open”
},
{
“ID”: “7a4ede72-244a-4f5f-8efa-b17e032d01ee”,
“createdAt”: null,
“createdBy”: null,
“modifiedAt”: null,
“modifiedBy”: null,
“customer”: “High Solar Energy AG”,
“employeeID”: “E1001”,
“salesVolume”: 500000,
“status”: “open”
}
]
}
And that is all which is required to make the Employee Service middleware work. Now, let me execute the fetchEmployee(forID:) request:
// http://localhost:4004/odata/v4/employee/fetchEmployee(forID=’E1000′)
{
“@odata.context”: “$metadata#EmployeeService.EmployeeResultSet”,
“employee”: [
{
“ID”: “00163E03-A070-1EE2-88BA-37FB8F5F50A9”,
“employeeID”: “E1000”,
“jobTitle”: “Sales Representative”,
“firstName”: “Mike”,
“lastName”: “Summers”,
“phoneNumber”: “+16505555014”,
“email”: “ussalesrep01@sap.com”
}
],
“sales”: [
{
“ID”: “2b23bb4b-4ac7-4a24-ac02-aa10cabd842c”,
“createdAt”: null,
“createdBy”: null,
“modifiedAt”: null,
“modifiedBy”: null,
“customer”: “Wood And Planks Inc.”,
“employeeID”: “E1000”,
“salesVolume”: 150000,
“status”: “closed”
}
]
}
Great! The middleware makes sure the correct employee as well as the corresponding sales get returned. It also makes sure that the data gets provided in the correct format.
Conclusion
The beautiful thing about being a Software Engineer is that there are so many possible solutions possible for one single problem. This is true for the above described problems, everything I’ve showed here are just ideas and should give you a good start when running into these issues.
The solution of building a db-less middleware is not considering complexity of authentication or slow back-end response times. SAP BTP can help you manage the authentication flows using Destination Services, and when it comes to slow back-end response times you can’t do anything there except of trying to manage caching within the middleware service. Fortunately, CAP is capable of caching data in a SQLite in-memory persistence.
If you are interested in the project, check out the CAP DB-less Middleware Service sample.
I hope this was helpful and I am looking forward to hearing about your opinions! Please, reach out to me over the comment section if you have feedback or questions.
With that, Happy Coding!
BackgroundTL;DR – When building front-end applications, we want to make data visually accessible and editable to the user. The application relies on correctness, availability and reliability of the data source to bring the best in class user experience to the end user. To guarantee the seamless integration, the back-end system needs to be aware of the application’s requirements. Unfortunately, this is not assured in most cases, because most applications are built against legacy systems that originally had been build for a different use case. Given that fact, application developers face challenges to adapt the application logic, data processing and back-end connectivity by implementing client-side workarounds or complicated application flows. In this blog post, I want to write about how an application developer can approach the challenge of working with such systems to avoid potential long-term issues. It should be noted that a close working relationship between front-end, back-end developers as well as UI/UX design is absolutely crucial for success not only for greenfield applications but also for building on legacy systems.Working with legacy systemsA potential issue with legacy systems is that the provided APIs and data structures are not or only partially suited for the front-end application’s use case. This reflects in deeply nested data structures or large amount of data being provided, that is not needed on the client. Complex parsing functionality to prepare the data in a format expected by the client is a potential result of this. At this point we are not even considering long response times or complicated authentication flows that can cause issues on the client side. Taking all potential factors into account the general interaction with such a system can be expensive, time-consuming on the implementation, code becomes hard to maintain and overall slows down the clients performance.Another problem, client developers face are dependencies on multiple back-end systems. In enterprise it is common that multiple systems are interconnected with each other to represent a complex business scenario. That means, the client has to reach out to multiple systems to fetch the required data and execute business relevant procedures. This complexity should not be handled on the client and an abstraction layer would be best to ease the implementation efforts for the client.Unfortunately, we don’t often have the luxury to define the back-end system, APIs and the underlying data structure to the exact needs for the front-end application. Instead of writing complex code to even out the above described issues, a developer needs a solution where it is possible to define and retrieve the data needed, in a structure that is expected, with the back-end functionality required for a successful business process on the client. I’ve touched on some of the issues client developers face when interacting with systems not suited for their purpose but what are the implications of these issues?:High maintainabilityBack-end systems, which are being consumed by applications who weren’t thought of in the original design of the system itself run into issues with data structure, authentication flows, slow load times, and missing back-end features. The consuming application has to adapt to even out these points, which most often result in an extensive or complicated code base. If the back-end updates, or changes in any way, the client has to touch that code to keep consuming the provided APIs and data. Touching the data service layer within a front-end application is not uncommon to keep up-to-date with the backend system but it can be hard to maintain if the code being written has to touch on all the before mentioned complications.Complicated testabilityLike with the issue of high maintainability, complex testability is almost inherent because of the nature of the application’s code base for handling the back-end interaction and data processing.Back-end complexity and long response timesBecause the back-end system was designed without the client’s requirements in mind, the interaction with the back-end can be spotty (connection issues) or complex. The backend might perform in unexpected ways, perform too slowly because of it’s legacy or the massive amount of computing needed to produce the requested result. That can cause timeouts on the client side, because of available system resources allocated for the application or to keep user interaction in flow due to the client UIs asynchronous nature.The application has to think of different mechanisms to compensate the backends behavior like caching mechanisms, persistence (delta handling between client and back-end), data processing or concurrency.Thinking about the consequences of handling these issues on the client will eventually cause runtime problems , on top of the code issues described above, that need to be avoided. Finding a solution is crucial to achieve a great user experience.If a re-write of the back-end system is not possible, what could be potential solutions?SolutionsBuild a new APITechnically, the simplest solution is to provide an API that provides only the data needed for the client and so executes the requested operations in a light-weight manner providing smaller amounts of data. Also, keeping data operations simple and business processes quick. This approach requires development resources on the back-end side to implement a new API, but most importantly the underlying functionality for abstracting complexity and potential performance issues with older systems. This results in a high demand of experienced developers, domain expertise, money and most importantly time.Abstract the back-end data structure using SAP Graph and GraphQLSAP Graph is defined as:”Extending traditional API Management, Graph enables you to expose all your business data in the form of a semantically connected data graph, accessed via a single unified and powerful API.Graph is a capability of API Management within SAP Integration Suite. With Graph, developers access your business data as a single semantically connected data graph, spanning the suite of SAP products and beyond.” (What Is Graph? – help.sap.com)Whereas GraphQL, is a querying language for APIs and it is providing a runtime for fulfilling those queries with existing data.You can ultimately combine both technologies with each other to expose data from multiple backends using SAP Graph and enable querying the data through GraphQL.Use CAP to create a db-less middleware serviceThink about a scenario where you have two different back-end services providing data through an API providing big data sets. A client needs to consume both APIs to provide value to end users by displaying relevant data and enabling users to execute CRUD (Create, Read, Update, Delete) operations on that data.The client has the issue that the back-end service is providing data which is not relevant to it’s use case. An approach is to provide a middleware abstracting complexity from the client. It should also provide a single entry point for the client to operate on the data of both back-end services, that means the middleware has to aggregate data and redirect the client’s requests to the respective service.In the following example, I want to show you how to use the Cloud Application Programming Model (CAP) for exposing and abstracting APIs. Authentication is not being covered as I want to focus on the data portion and how CAP can be utilized.Solution Diagram for Employee Service middleware Looking at the solution diagram above, there are two services needed to be exposed to the client: Sales Service and Business Partner Employee Service. The Sales Service is a basic CAP OData service deployed on SAP BTP, Cloud Foundry runtime using SAP HANA Cloud as persistence. The Business Partner Employee OData Service is a SAP Cloud for Customer service which can be explored through SAP API Accelerator and is part of the S/4HANA suite.The employee CAP OData service, is the db-less middleware service being implemented to aggregate and abstract both of the other services. The service implementation makes sure to only provide data needed by the client in one aggregated data set. For the client, the only data relevant is the employee and the related sales of the employee. The client needs to display an employee detail view which shows information about the employee and all sales that employee is currently working on or has already completed. The employee information will be fetched from the Business Partner service and the sales information from the Sales Service. The only association between these two are by employee ID. Before looking into how CAP can be used to provide a custom entity bringing both information together, let’s look into the data structure of both services.Business Partner – Employee CollectionThe business partner service contains dozens of entities, but in this scenario only the employee collection is required. Looking into the employee collection OData metadata document it is clear that all that properties in there are not needed for the client’s requirements. That means, the Employee Service middleware needs to expose only the necessary properties (I have stripped out more than half of the properties for readability): <EntityType Name=”EmployeeCollection”>
<Key>
<PropertyRef Name=”ObjectID”/>
</Key>
<Property Name=”ObjectID” Type=”Edm.String” MaxLength=”70″ Nullable=”false”/>
<Property Name=”EmployeeID” Type=”Edm.String” MaxLength=”20″/>
<Property Name=”UUID” Type=”Edm.Guid”/>
<Property Name=”UserID” Type=”Edm.String” MaxLength=”40″/>
<Property Name=”IdentityUUID” Type=”Edm.Guid”/>
<Property Name=”GlobalUserID” Type=”Edm.String” MaxLength=”36″/>
<Property Name=”BusinessPartnerID” Type=”Edm.String” MaxLength=”10″/>
<Property Name=”EmployeeValidityStartDate” Type=”Edm.Date”/>
<Property Name=”EmployeeValidityEndDate” Type=”Edm.Date”/>
<Property Name=”BusinessPartnerFormattedName” Type=”Edm.String” MaxLength=”480″/>
<Property Name=”TitleCode” Type=”Edm.String” MaxLength=”4″/>
<Property Name=”TitleCodeText” Type=”Edm.String”/>
<Property Name=”AcademicTitleCode” Type=”Edm.String” MaxLength=”4″/>
<Property Name=”AcademicTitleCodeText” Type=”Edm.String”/>
<Property Name=”FirstName” Type=”Edm.String” MaxLength=”40″ Nullable=”false”/>
<Property Name=”MiddleName” Type=”Edm.String” MaxLength=”40″/>
<Property Name=”LastName” Type=”Edm.String” MaxLength=”40″ Nullable=”false”/>
<Property Name=”SecondLastName” Type=”Edm.String” MaxLength=”40″/>
<Property Name=”NickName” Type=”Edm.String” MaxLength=”40″/>
<Property Name=”GenderCode” Type=”Edm.String” MaxLength=”1″/>
<Property Name=”GenderCodeText” Type=”Edm.String”/>
<Property Name=”MaritalStatusCode” Type=”Edm.String” MaxLength=”1″/>
<Property Name=”MaritalStatusCodeText” Type=”Edm.String”/>
<Property Name=”LanguageCode” Type=”Edm.String” MaxLength=”2″/>
<Property Name=”LanguageCodeText” Type=”Edm.String”/>
<Property Name=”NationalityCountryCode” Type=”Edm.String” MaxLength=”3″/>
…
<NavigationProperty Name=”BusinessUser” Type=”businesspartner.BusinessUserCollection”/>
<NavigationProperty Name=”EmployeeOrganisationalUnitAssignment” Type=”Collection(businesspartner.EmployeeOrganisationalUnitAssignmentCollection)”/>
<NavigationProperty Name=”EmployeeSalesResponsibility” Type=”Collection(businesspartner.EmployeeSalesResponsibilityCollection)”/>
…
</EntityType> Sales ServiceThe sales service is far more basic, it just exposes the different sales and sales status corresponding to an employee ID. So no need to implement an abstraction here: <EntityType Name=”Sales”>
<Key>
<PropertyRef Name=”ID”/>
</Key>
<Property Name=”ID” Type=”Edm.Guid” Nullable=”false”/>
<Property Name=”createdAt” Type=”Edm.DateTimeOffset” Precision=”7″/>
<Property Name=”createdBy” Type=”Edm.String” MaxLength=”255″/>
<Property Name=”modifiedAt” Type=”Edm.DateTimeOffset” Precision=”7″/>
<Property Name=”modifiedBy” Type=”Edm.String” MaxLength=”255″/>
<Property Name=”customer” Type=”Edm.String”/>
<Property Name=”employeeID” Type=”Edm.Guid”/>
<Property Name=”salesVolume” Type=”Edm.Int32″/>
<Property Name=”status” Type=”Edm.String”/>
</EntityType> Employee ServiceAlright, now let’s take a look into the Employee Service middleware. All needed there is the employee and the corresponding sales: <ComplexType Name=”EmployeeResultSet”>
<Property Name=”employee” Type=”Edm.String”/>
<Property Name=”sales” Type=”Edm.String”/>
</ComplexType> ImplementationI am not going into detail on how to integrate with external services, here I am focusing on data abstraction and providing a custom data structure needed by the client. If you want to learn on how to integrate with external services, take a look at the content for the Service integration with SAP Cloud Application Programming Model Codejam content on GitHub.Create a projection on businesspartner.EmployeeCollectionFirst thing, create that abstracted entity for the above shown monster of an entity. With CAP and cds this is really easy, the only thing needed is an entity projection on the original entity. This allows for defining the properties needed by the client. using { businesspartner } from ‘./businesspartner’;
namespace s4.bp;
entity Employees as projection on businesspartner.EmployeeCollection {
key UUID as ID,
EmployeeID as employeeID,
TitleCodeText as jobTitle,
FirstName as firstName,
LastName as lastName,
OfficePhoneNumber as phoneNumber,
Email as email
}; The above shown cds definition makes sure that only the defined properties get exposed to the consuming client, it also allows for a redefinition of property names. That is all a developer has to do in order to hide the vast amount of properties included in the Employee Collection of Business Partner.Definition of the Employee ServiceA projection for the sales service is not necessary because it already is in the format needed by the client, that means I can continue with the service definition for the middleware API. I call this EmployeeService: service EmployeeService {
type EmployeeResultSet {
employee : String;
sales : String;
};
function fetchEmployee (forID: String) returns EmployeeResultSet;
} Employee Service includes a custom type EmployeeResultSet , that exposes the employee from the Employee Collection and the related sales from Sales Service. The service defines a fetch function to retrieve the custom type EmployeeResultSet by providing the employee ID, and that is all for the service definition.Before going into details on how to implement the service itself, one thing needs to be done first, the Employee Service has to be extended with the defined external Employees projection in order to make it known to the middleware. using { s4.bp } from ‘./external/bp_simple_employee’;
using { EmployeeService } from ‘./employee-service’;
extend service EmployeeService with {
entity Employees as projection on bp.Employees;
} Now, Employee Service knows about the projected Employees entity from the external business partner service.Implementation of the Employee ServiceIn this example, I am using CAP with Node.js but the same approach applies to Java based projects.For the Employee Service a implementation is needed to bring in business logic for the new middleware service. In the implementation, I am simply implementing the fetchEmployee(forID: String) function making sure that the correct employee is being fetched from the business partner service and that the related sales are being fetched from the sales service. module.exports = (EmployeeService) => {
EmployeeService.on(‘fetchEmployee’, async function(req) {
const employeeID = req.data.forID
const employee = await fetchBPEmployee(employeeID)
const sales = await fetchSales(employeeID)
return { employee, sales }
})
} The function does nothing more than fetching the employee and the sales with the help of the employee ID and returning it as the defined custom type EmployeeResultSet.To fetch the required data, two custom methods are being implemented: fetchBPEmployee and fetchSales. Both, establish connection to the external service and sending a GET request to retrieve the needed data: async function fetchBPEmployee(employeeID) {
const bpService = await cds.connect.to(‘EmployeeService’)
const employee = await bpService.run(
SELECT(‘*’)
.from(‘Employees’)
.where(
`employeeID = ‘${employeeID}’`
))
return employee
} Did you notice that our request goes against the defined projection on EmployeeService and not the businesspartner.EmployeeCollection. This is exactly what is need, the request goes towards the projection on the external service and than gets routed to the actual back-end service. The projection makes sure that only the required data gets put into the response body: // projection: http://localhost:4004/odata/v4/employee/Employees
// actual back-end: http://localhost:4004/odata/v4/businesspartner/EmployeeCollection
{
“@odata.context”: “$metadata#Employees”,
“value”: [
{
“ID”: “00163E03-A070-1EE2-88BA-37FB8F5F50A9”,
“employeeID”: “E1000”,
“jobTitle”: “Sales Representative”,
“firstName”: “Mike”,
“lastName”: “Summers”,
“phoneNumber”: “+16505555014”,
“email”: “ussalesrep01@sap.com”
},
{
“ID”: “00163E03-A070-1EE2-88BA-384A3440D0AA”,
“employeeID”: “E1001”,
“jobTitle”: “Sr. Sales Representative”,
“firstName”: “Phil”,
“lastName”: “Hughes”,
“phoneNumber”: “+16505555015”,
“email”: “ussalesrep02@ondemand.com”
}
]
} The sales service is straight forward, requests get send directly to the service and the response is served back without mutation. async function fetchSales(employeeID) {
const salesService = await cds.connect.to(‘SalesService’)
const sales = await salesService.run(
SELECT(‘*’)
.from(‘SalesService.Sales’)
.where(
`employeeID = ‘${employeeID}’`
))
return sales
} The result looks like this: {
“@odata.context”: “$metadata#Sales”,
“value”: [
{
“ID”: “2b23bb4b-4ac7-4a24-ac02-aa10cabd842c”,
“createdAt”: null,
“createdBy”: null,
“modifiedAt”: null,
“modifiedBy”: null,
“customer”: “Wood And Planks Inc.”,
“employeeID”: “E1000”,
“salesVolume”: 150000,
“status”: “closed”
},
{
“ID”: “6ccf474c-3881-44b7-99fb-59a2a4668418”,
“createdAt”: null,
“createdBy”: null,
“modifiedAt”: null,
“modifiedBy”: null,
“customer”: “TechnoCracks unlimited”,
“employeeID”: “E1001”,
“salesVolume”: 325000,
“status”: “open”
},
{
“ID”: “7a4ede72-244a-4f5f-8efa-b17e032d01ee”,
“createdAt”: null,
“createdBy”: null,
“modifiedAt”: null,
“modifiedBy”: null,
“customer”: “High Solar Energy AG”,
“employeeID”: “E1001”,
“salesVolume”: 500000,
“status”: “open”
}
]
} And that is all which is required to make the Employee Service middleware work. Now, let me execute the fetchEmployee(forID:) request: // http://localhost:4004/odata/v4/employee/fetchEmployee(forID=’E1000′)
{
“@odata.context”: “$metadata#EmployeeService.EmployeeResultSet”,
“employee”: [
{
“ID”: “00163E03-A070-1EE2-88BA-37FB8F5F50A9”,
“employeeID”: “E1000”,
“jobTitle”: “Sales Representative”,
“firstName”: “Mike”,
“lastName”: “Summers”,
“phoneNumber”: “+16505555014”,
“email”: “ussalesrep01@sap.com”
}
],
“sales”: [
{
“ID”: “2b23bb4b-4ac7-4a24-ac02-aa10cabd842c”,
“createdAt”: null,
“createdBy”: null,
“modifiedAt”: null,
“modifiedBy”: null,
“customer”: “Wood And Planks Inc.”,
“employeeID”: “E1000”,
“salesVolume”: 150000,
“status”: “closed”
}
]
} Great! The middleware makes sure the correct employee as well as the corresponding sales get returned. It also makes sure that the data gets provided in the correct format.ConclusionThe beautiful thing about being a Software Engineer is that there are so many possible solutions possible for one single problem. This is true for the above described problems, everything I’ve showed here are just ideas and should give you a good start when running into these issues. The solution of building a db-less middleware is not considering complexity of authentication or slow back-end response times. SAP BTP can help you manage the authentication flows using Destination Services, and when it comes to slow back-end response times you can’t do anything there except of trying to manage caching within the middleware service. Fortunately, CAP is capable of caching data in a SQLite in-memory persistence.If you are interested in the project, check out the CAP DB-less Middleware Service sample.I hope this was helpful and I am looking forward to hearing about your opinions! Please, reach out to me over the comment section if you have feedback or questions.With that, Happy Coding! Read More Technology Blogs by SAP articles
#SAP
#SAPTechnologyblog
+ There are no comments
Add yours