Skip to content

Commit 4f3fe22

Browse files
committed
...
1 parent 38e09ea commit 4f3fe22

File tree

5 files changed

+162
-238
lines changed

5 files changed

+162
-238
lines changed
89.4 KB
Loading
134 KB
Loading
File renamed without changes.

guides/integration/calesi.md

Lines changed: 162 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ We can also `cds import` APIs from other sources, such as OData APIs to integrat
472472
\> [_OData V2_](https://api.sap.com/products/SAPS4HANACloud/apis/ODATA)
473473
- Find and open [_Business Partner (A2X)_](https://api.sap.com/api/API_BUSINESS_PARTNER/overview)
474474
- Switch to the *API Specification* subtab.
475-
- Click the download icon next to *OData EDMX* to download the `API_BUSINESS_PARTNER.edmx` file to your local machine.
475+
- Click the download icon next to *OData EDMX* to download the `.edmx` file.
476476
:::
477477

478478
2. Import that to the current project:
@@ -801,6 +801,8 @@ There are different options to provide initial data, test data, and mock data:
801801
802802
In all cases, the `.csv` files are placed next to the `.cds` files, and hence they are automatically detected and loaded into the in-memory database.
803803
804+
For Java, make sure to add the `--with-mocks` option to the `cds deploy` command used to generate the `schema.sql` in `srv/pom.xml`. This ensures that tables for the mocked remote entities are created in the database.
805+
804806
[Learn more about *Adding Initial Data*](../databases/initial-data) {.learn-more}
805807
806808
> [!tip] Mocking for Inner-Loop Development
@@ -811,33 +813,40 @@ In all cases, the `.csv` files are placed next to the `.cds` files, and hence th
811813
812814
### Required Custom Code
813815
814-
When it comes to real non-mocked integration with external services, custom code is required to handle the actual data integration. We can see that by starting two separate processes, one for the S/4 service, and one for the Xtravels app as follows from within the `cap/samples/xtravels` project folder:
815-
816-
1. Start the a mocked S/4 service in terminal 1:
816+
When it comes to real non-mocked integration with external services, custom code is required to handle the actual data integration. For a more realistic test, lets use `cds mock` to run the mocked services in separate processes. Run these commands **in separate terminals**:
817817
818818
```shell
819819
cds mock apis/capire/s4.cds
820820
```
821-
822-
2. Start the Xtravels app in terminal 2:
823-
821+
```shell
822+
cds mock apis/capire/xflights.cds
823+
```
824824
```shell
825825
cds watch
826826
```
827+
828+
In the log output of the xtravels app server we now see that it connects to the two other services instead of mocking them:
829+
830+
```zsh
831+
[cds] - connect to S4BusinessPartnerService > odata {
832+
url: 'http://localhost:54476/odata/v4/s4-business-partner'
833+
}
834+
```
827835
```zsh
828-
[cds] - mocking S4BusinessPartnerService { # [!code --]
829-
at: [ '/odata/v4/s4-business-partner' ], # [!code --]
830-
decl: 's4/external/API_BUSINESS_PARTNER.csn:7' # [!code --]
831-
} # [!code --]
836+
[cds] - connect to sap.capire.flights.data > hcql {
837+
url: 'http://localhost:54475/hcql/data'
838+
}
832839
```
833840
834-
Note that in the output there is no mention of a mocked `S4BusinessPartnerService` anymore. This is because the CAP runtime now detected that a service with that name is served by a different process within my local binding environment now, so we don't mock it in-process any longer.
841+
This is because the CAP runtime now detected that services with that name are served by different processes within our local binding environment now, so we don't mock them in-process any longer.
842+
843+
When we open the Fiori UI in the browser again, we see the data from the S/4 service is missing now, as we have not yet implemented the required custom code for the actual data integration, the same applies to the flight data from _xflights_:
835844
836-
When we open the Fiori UI in the browser again, we see the data from the S/4 service is missing now, as we have not yet implemented the required custom code for the actual data integration:
845+
![XTravels Fiori list view showing a table of travel requests, with the Customer column empty.](assets/xtravels-list-.png)
837846
838-
![XTravels Fiori list view showing a table of travel requests, with the Customer column empty.](assets/xtravels-list-no-s4.png)
847+
![XTravels Fiori details view showing a travel requests, with the flights data missing](assets/xtravels-bookings-.png)
839848
840-
In addition, when we look into the log output of the Xtravels app, we see something like that, which indicates that the Fiori client is desparately trying to fetch the missing customer data in _$batch_ requests, with one GET request per row, 30 in total, corresponding to the default page size. If we'd scroll the list in the UI this would repeat like crazy.
849+
In addition, when we again look into the log output, we see some bulk requests like shown below, which indicates that the Fiori client is desparately trying to fetch the missing customer data. If we'd scroll the list in the UI this would repeat like crazy.
841850
842851
<span style="font-size:63%">
843852
@@ -887,94 +896,160 @@ So, let's go on and fill this gap with required custom code...
887896
888897
### CAP-level Data Federation
889898
890-
Displaying external data in lists UIs commonly requires fast access to that data. Relying on live calls to remote services per row is clearly not an option, as that would lead to poor performance, excessive load on on server, and lacking all resilience. Instead, we need to replicate the required data from remote services into local replica tables, which can then be accessed fast and reliably by UIs using good old SQL JOINs.
899+
Displaying external data in lists commonly requires fast access to that data. Relying on live calls to remote services per row is clearly not an option, as that would lead to poor performance, excessive load on server, and a nightmare regarding resilience. Instead, we somehow need to ensure that all required data is available locally, so that it can be accessed fast and reliably by UIs, using good old SQL JOINs.
900+
901+
In the xtravels app we accomplished that with a simple, yet quite effective data replication solution, which automatically replicates data for all [consumption views](#consumption-views) tagged with the `@federated` annotation, for example:
902+
903+
```cds :line-numbers=4
904+
@federated entity Customers as projection on S4.A_BusinessPartner { ... }
905+
```
906+
907+
If a remote service is detected, these entities are turned into tables to serve as local persistence for replicated data (line 9 in the code below).
908+
909+
> [!tip] Stay Intentional -> <i>What, not how!</i> -> Minimal Assumptions
910+
>
911+
> By tagging entities with `@federated` we stay _intentional_ about **_what_** we want to achieve, and avoid any premature assumptions about **_how_** things are actually implemented. => This allows CAP runtimes – or your own _generic_ solutions, as in this case – to choose the best possible implementation strategies for the given environment and use case, which may differ between development, testing, and production environments, or might need to evolve over time.
912+
891913
892-
We did implement a simple, yet quite effective, solution in `srv/data-federation.js` in the Xtravels app as shown below:
914+
Here's the complete code, placed in file `srv/data-federation.js`:
893915
894916
::: code-group
895-
```js:line-numbers {9,16,24,25,26} [srv/data-federation.js]
917+
```js:line-numbers [srv/data-federation.js]
918+
const PROD = process.env.NODE_ENV === 'production' /* eslint-disable no-console */
896919
const cds = require ('@sap/cds')
897920
const feed = []
898921
899922
// Collect all entities to be federated, and prepare replica tables
900-
cds.on ('loaded', csn => {
901-
for (let each of cds.linked(csn).entities) if (each['@federated']) {
902-
let srv = each.__proto__?._service; if (!srv) continue
903-
if (!cds.requires[srv.name]?.credentials?.url) continue
904-
each['@cds.persistence.table'] = true //> table for replicas
905-
feed.push ({ entity: each.name, remote: srv.name })
923+
PROD || cds.on ('loaded', csn => {
924+
for (let e of cds.linked(csn).entities) {
925+
if (e['@federated']) {
926+
let srv = remote_srv4(e)
927+
if (is_remote(srv)) {
928+
e['@cds.persistence.table'] = true //> turn into table for replicas
929+
feed.push ({ entity: e.name, remote: srv })
930+
}
931+
}
906932
}
907933
})
908934
909935
// Setup and schedule replications for all collected entities
910-
cds.once ('served', () => Promise.all (feed.map (async each => {
911-
const remote = await cds.connect.to (each.remote)
912-
await remote.schedule ('replicate', each) .every ('3 seconds')
913-
remote.replicate ??=!! remote.on ('replicate', replicate_entity)
936+
PROD || cds.once ('served', () => Promise.all (feed.map (async each => {
937+
const srv = await cds.connect.to (each.remote)
938+
srv._once ??=! srv.on ('replicate', replicate)
939+
await srv.schedule ('replicate', each) .every ('3 seconds')
914940
})))
915941
916942
// Event handler for replicating single entities
917-
async function replicate_entity (req) {
943+
async function replicate (req) {
918944
let { entity } = req.data, remote = this
919-
let { t0 } = await SELECT.one `max(modifiedAt) as t0` .from (entity)
920-
let rows = await remote.read (entity) .where `modifiedAt > ${t0}`
945+
let { latest } = await SELECT.one `max(modifiedAt) as latest` .from (entity)
946+
let rows = await remote.run (
947+
SELECT.from (entity) .where `modifiedAt > ${latest}`
948+
)
921949
if (rows.length) await UPSERT (rows) .into (entity); else return
922-
console.log ('Replicated', rows.length, 'entries for:', { entity })
950+
console.log ('Replicated', rows.length, 'entries', { for: entity, via: this.kind })
923951
}
952+
953+
// Helpers to identify remote services, and check whether they are connected
954+
const remote_srv4 = entity => entity.__proto__._service?.name
955+
const is_remote = srv => cds.requires[srv]?.credentials?.url
924956
```
925957
:::
926958
927-
This is a generic solution which automatically replicates all entities annotated with `@federated`, as we did in the [consumption views](#consumption-views) before, for example:
959+
Let's have a closer look at this code, which handles these main tasks:
928960
929-
```cds :line-numbers=4
930-
@federated entity Customers as projection on S4.A_BusinessPartner { ... }
931-
```
961+
1. **Prepare Persistence** – When the model is `loaded`, before it's deployed to the database, we collect all to be `@federated` entities, check whether their respective services are remote, and if so, turn them into tables for local replicas (line 11).
932962
933-
These entities are detected, and, unless mocked, turned into tables to serve as local persistence for replicated data (line 9 in the code above).
963+
2. **Setup Replication** – Later when all services are `served`, we connect to each remote one (line 20), register a handler for replication (line 21), and schedule it to be invoked every three seconds (line 22).
964+
965+
3. **Replicate Data** – Finally, the `replicate` handler implements a simple polling-based data federation strategy, based on `modifiedAt` timestamps (lines 28-32), with the actual call to remote happening on line 29.
966+
967+
> [!tip] CAP-level Querying -> agnostic to databases & protocols
968+
> We work with **database-agnostic** and **protocol-agnostic** [CQL queries](../../cds/cql) both for interacting with the local database as well as for querying remote services. In effect, we got a fully generic solution for replication, i.e., it works for **_any_** remote service that supports OData, or HCQL.
934969
935-
> [!tip] Stay Intentional -> What, not how
936-
> By default, consumption views on top of remote entities are virtual and do not have local persistence. By setting the `@cds.persistence.table` annotation to `true`, we instruct the CAP database layer to create actual database tables for these entities, which can then be used to store replicated data locally.
937-
>
938-
> Yet, you as an application developer stay intentional about **_what_** you want to achieve (-> `@federated` tags), and **avoid any eager assumptions** about **_how_** this is actually implemented. => This allows CAP runtimes – or you own _generic_ framework plugins, as with the code above – to choose the best possible implementation strategy for the given environment and use case, which may differ between development, testing, and production environments.
939970
940-
The actual replication is accomplished by the highlighted lines 16 and 24-26, which implement a simple polling-based data federation mechanism, based on `modifiedAt` timestamps. Let's break that down line by line:
971+
#### Test Drive
941972
942-
```js :line-numbers=16
943-
const remote = await cds.connect.to (each.remote) // done once
973+
Let's see the outcome in action: to activate the above data federation code, edit `srv/server.js` file and uncomment the single line of code in there like this:
974+
975+
::: code-group
976+
```js :line-numbers [srv/server.js]
977+
process.env.NODE_ENV || require ('./data-federation')
944978
```
945-
Establishes a connection to the remote service (like the S/4 Business Partner service or XFlights service).
979+
:::
980+
981+
Restart the Xtravels app, and see these lines in the log output:
946982
947-
```js :line-numbers=24
948-
let { t0 } = await SELECT.one `max(modifiedAt) as t0` .from (entity)
983+
```zsh
984+
Replicated 49 entries { for: 'sap.capire.xflights.Supplements', via: 'hcql' }
985+
Replicated 44 entries { for: 'sap.capire.xflights.Flights', via: 'hcql' }
986+
Replicated 727 entries { for: 'sap.capire.s4.Customers', via: 'odata' }
949987
```
950988
951-
Queries the local replica table to find the timestamp of the most recently modified record.
952-
This determines the "last sync point" - only newer data needs to be fetched.
989+
The S/4 Business Partner service in terminal 1 shows the incoming OData request(s):
953990
954-
```js :line-numbers=25
955-
let rows = await remote.read (entity) .where `modifiedAt > ${t0}`
991+
```zsh
992+
[odata] - GET /odata/v4/s4-business-partner/A_BusinessPartner {
993+
'$select': 'BusinessPartner,PersonFullName,LastChangeDate',
994+
'$filter': 'LastChangeDate gt 2024-12-31'
995+
}
956996
```
957-
Fetches only records from the remote service that have been modified since the last sync.
958-
On an initial run, when no data exists locally, all records are fetched automatically, as `t0` will be `null`.
959997
960-
```js :line-numbers=26
961-
if (rows.length) await UPSERT (rows) .into (entity)
998+
While the xflights service in terminal 2 shows its incoming HCQL requests like that:
999+
1000+
```zsh
1001+
[hcql] - GET /hcql/data/ {
1002+
SELECT: {
1003+
from: { ref: [ 'sap.capire.flights.data.Flights' ] },
1004+
columns: [
1005+
{ ref: [ 'ID' ], as: 'ID' },
1006+
{ ref: [ 'date' ], as: 'date' },
1007+
{ ref: [ 'departure' ], as: 'departure' },
1008+
{ ref: [ 'arrival' ], as: 'arrival' },
1009+
{ ref: [ 'free_seats' ], as: 'free_seats' },
1010+
{ ref: [ 'modifiedAt' ], as: 'modifiedAt' },
1011+
{ ref: [ 'airline', 'icon' ], as: 'icon' },
1012+
{ ref: [ 'airline', 'name' ], as: 'airline' },
1013+
{ ref: [ 'origin', 'name' ], as: 'origin' },
1014+
{ ref: [ 'destination', 'name' ], as: 'destination' }
1015+
],
1016+
where: [
1017+
{ ref: [ 'modifiedAt' ] },
1018+
'>',
1019+
{ val: '2026-01-28T17:38:28.929Z' }
1020+
]
1021+
}
1022+
}
9621023
```
963-
For all received rows, `UPSERT` automatically inserts new records or updates existing ones in the local replica table.
9641024
965-
Noteworthy:
1025+
Finally, open the Fiori UI in the browser again, and see that customer data from S/4 as well as flight data from xflights is now displayed properly, thanks to the data federation implemented above.
9661026
967-
- `SELECT` and `UPSERT` commands are part of CAP Node.js' [`cds.ql`](../../node.js/cds-ql) and and create [_CQL_](../../cds/cql) query objects. When `await`ed as in line 24 and 26, they are executed against the local database, translated to database-native SQL under the hood.
1027+
![XTravels Fiori list view showing tarvel requests, now with customer names again.](assets/xtravels-list.png)
9681028
969-
- `remote.read` on the other hand executes queries against the connected remote service, translating them to the connected service's remote protocol, like OData or HCQL. It's actually a convenience shorthand for:
970-
```js
971-
remote.run (SELECT.from(entity) .where `modifiedAt > ${t0}`)
972-
```
1029+
![XTravels Fiori details view showing a travel requests, now with flight data again.](assets/xtravels-bookings.png)
1030+
1031+
1032+
1033+
### Delegating Requests
1034+
1035+
Value helps are common use cases where delegation of requests is needed, which we implemented like this in `srv/travel-service.js` for the `Customers` entity:
1036+
1037+
::: code-group
1038+
```js :line-numbers=24 [srv/travel-service.js]
1039+
const s4 = await cds.connect.to ('S4BusinessPartnerService')
1040+
this.on ('READ', Customers, req => s4.run (req.query))
1041+
```
1042+
:::
1043+
1044+
The event handler intercepts all `READ` requests to the `Customers` entity, and simply delegates the incoming query as-is to the connected S/4 service (line 26).
1045+
1046+
Noteworthy: The incoming request refers to:
9731047
974-
- In both/all cases we work with database-agnostic and protocol-agnostic queries constructed using [`cds.ql`](../../node.js/cds-ql), while CAP runtimes handle the protocol and dialect translations under the hood.
1048+
- the `TravelService.Customers` entity, which is a view on
1049+
- the `Customers` entity, which in turn is a consumption view on
1050+
- the remote `A_BusinessPartner` entity.
9751051
976-
> [!tip] Agnostic Querying, ... <i>as if they were local</i>
977-
> Both, queries executed against local databases, as well as queries executed against remote services using, are **database-agnostic** and **protocol-agnostic**, respectively. CAP runtimes handle all necessary translations under the hood, allowing us to write code that works regardless of where the data actually resides – be it with mocked data, replicated data, or real remote data. We can even switch between different integration and federation strategies later on.
1052+
The CAP runtime is aware of that and automatically translates the query into a query to the underlying entity, known to the remote service. Thereby, all select clauses, and where clauses, are fully preserved, translated, and delegated.
9781053
9791054
9801055
### Requests to Remote
@@ -983,32 +1058,35 @@ In addition to replication, there are also use cases where we need to send reque
9831058
9841059
::: code-group
9851060
```js :line-numbers=28 [srv/travel-service.js]
986-
const xflights = await cds.connect.to ('sap.capire.flights.data')
1061+
const xflights = await cds.connect.to ('sap.capire.flights.data') // [!code focus]
9871062
this.after ('SAVE', Travels, ({ Bookings=[] }) => Promise.all (
988-
Bookings.map (({ Flight_ID: flight, Flight_date: date }) => {
989-
return xflights.send ('POST', 'BookingCreated', { flight, date })
1063+
Bookings.map (({ Flight_ID, Flight_date }) => {
1064+
return xflights.BookingCreated (Flight_ID, Flight_date) // [!code focus]
9901065
})
9911066
))
9921067
```
993-
> Note: `this` is an instance of `TravelService`.
994-
1068+
:::
9951069
996-
#### Delegating Value Help Requests
9971070
998-
Another example is delegating value help requests to remote services, which we implemented like this in `srv/travel-service.js` for `Customers` -> S/4 Business Partners:
1071+
## Service Bindings
9991072
1000-
::: code-group
1001-
```js :line-numbers=24 [srv/travel-service.js]
1002-
const s4 = await cds.connect.to ('S4BusinessPartnerService')
1003-
this.on ('READ', Customers, req => s4.run (req.query))
1073+
Service bindings configure connectivity to remote services. They are injected respective connection points configure in CAP through `cds.requires.<service-name>` configurations, which are defined like this:
1074+
1075+
```tsx
1076+
cds.requires.<service-name> = {
1077+
kind?: 'odata' | 'odata-v2' | 'rest' | 'hcql' | 'graphql' ,
1078+
model?: '<path-to-csn-or-cds>',
1079+
credentials: {
1080+
url?: '<service-endpoint-url>',
1081+
username?: '<user-name>',
1082+
password?: '<password>',
1083+
token?: '<auth-token>',
1084+
}
1085+
}
10041086
```
1005-
:::
10061087
1007-
This intercepts all `READ` requests to the `Customers` entity, and simply delegates the original request's query to the connected S/4 service. This ensures we support all kind of select clauses, and where clauses, as they are fully preserved. The CAP runtime handles the necessary protocol translations under the hood, including mapping all projections to entities known to the remote service.
1008-
1009-
1010-
## Service Bindings
10111088
1089+
They are added to consuming applications' _package.json_ files, either manually, or automatically when using `cds import` as we saw earlier.
10121090
10131091
### CAP Node.js
10141092
@@ -1093,6 +1171,9 @@ cds watch
10931171
10941172
Open UI → flights data displayed
10951173
1174+
> [!tip] Decoupled Inner-Loop Development
1175+
> CAP runtimes automatically mock imported service APIs during development, allowing us to develop and test integrated applications in fast inner loops, without the need to connect to real remote services. This decouples inner-loop development from external dependencies, speeding up development and increasing resilience.
1176+
10961177
10971178
10981179
### Using `cds mock`

0 commit comments

Comments
 (0)