Skip to content

Commit ad8cd33

Browse files
committed
Update calesi.md
1 parent fe0a70f commit ad8cd33

File tree

1 file changed

+67
-33
lines changed

1 file changed

+67
-33
lines changed

guides/integration/calesi.md

Lines changed: 67 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -618,14 +618,14 @@ namespace sap.capire.xflights;
618618
:::
619619
::: code-group
620620
```cds :line-numbers [apis/capire/s4.cds]
621-
using { API_BUSINESS_PARTNER as s4 } from '@capire/s4';
621+
using { API_BUSINESS_PARTNER as S4 } from '@capire/s4';
622622
namespace sap.capire.s4;
623623
624-
@federated entity Customers as projection on s4.A_BusinessPartner {
624+
@federated entity Customers as projection on S4.A_BusinessPartner {
625625
BusinessPartner as ID,
626626
PersonFullName as Name,
627627
LastChangeDate as modifiedAt,
628-
}
628+
} where BusinessPartnerCategory == 1; // 1 = Person
629629
```
630630
:::
631631
@@ -885,14 +885,14 @@ MaxListeners is 10. Use emitter.setMaxListeners() to increase limit
885885
So, let's go on and fill this gap with required custom code...
886886
887887
888-
#### Service-level Data Federation
888+
### CAP-level Data Federation
889889
890890
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.
891891
892892
We did implement a simple, yet quite effective, solution in `srv/data-federation.js` in the Xtravels app as shown below:
893893
894894
::: code-group
895-
```js:line-numbers {16,24,25,26} [srv/data-federation.js]
895+
```js:line-numbers {9,16,24,25,26} [srv/data-federation.js]
896896
const cds = require ('@sap/cds')
897897
const feed = []
898898
@@ -924,53 +924,87 @@ async function replicate_entity (req) {
924924
```
925925
:::
926926
927-
The actual replication is accomplished by the highlighted lines – extracted below –, which implement a simple polling-based data federation mechanism, based on `modifiedAt` timestamps:
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:
928928
929-
```js
930-
const remote = await cds.connect.to (each.remote) // done once
929+
```cds :line-numbers=4
930+
@federated entity Customers as projection on S4.A_BusinessPartner { ... }
931931
```
932-
```js
933-
let { t0 } = await SELECT.one `max(modifiedAt) as t0` .from (entity)
934-
let rows = await remote.read (entity) .where `modifiedAt > ${t0}`
935-
if (rows.length) await UPSERT (rows) .into (entity); else return
932+
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).
934+
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.
939+
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:
941+
942+
```js :line-numbers=16
943+
const remote = await cds.connect.to (each.remote) // done once
944+
```
945+
Establishes a connection to the remote service (like the S/4 Business Partner service or XFlights service).
946+
947+
```js :line-numbers=24
948+
let { t0 } = await SELECT.one `max(modifiedAt) as t0` .from (entity)
936949
```
937950
938-
#### Delegation to Remote Services
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.
939953
954+
```js :line-numbers=25
955+
let rows = await remote.read (entity) .where `modifiedAt > ${t0}`
956+
```
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`.
940959
941-
#### Event-Driven Integration
960+
```js :line-numbers=26
961+
if (rows.length) await UPSERT (rows) .into (entity)
962+
```
963+
For all received rows, `UPSERT` automatically inserts new records or updates existing ones in the local replica table.
942964
965+
Noteworthy:
943966
944-
#### Claude says...
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.
945968
946-
##### 3. **Service Implementation**
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+
```
947973
948-
- **Line 22**: Runtime integration
949-
- Subscribes to `Flights.Updated` events from the external XFlights service
950-
- Updates local replica data when occupied seats change
951-
- Emits `Booking.Created` events back to XFlights when new bookings are made
952-
- Implements bidirectional data synchronization
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.
953975
954-
##### 4. **Data Federation**
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.
955978
956-
The `@federated` annotation on the Flights entity triggers automatic replication:
957979
958-
- Creates local replica tables for offline access
959-
- Schedules periodic synchronization (every 3 seconds in development)
960-
- Replicates changes based on `modifiedAt` timestamps
980+
### Requests to Remote
961981
962-
##### Architecture Pattern
982+
In addition to replication, there are also use cases where we need to send requests directly to the remote services. For example, when creating new bookings, we need to inform the external xflights service so it can reduce its records of free seats. This is accomplished by the following code in `srv/travel-service.js`:
983+
984+
::: code-group
985+
```js :line-numbers=28 [srv/travel-service.js]
986+
const xflights = await cds.connect.to ('sap.capire.flights.data')
987+
this.after ('SAVE', Travels, ({ Bookings=[] }) => Promise.all (
988+
Bookings.map (({ Flight_ID: flight, Flight_date: date }) => {
989+
return xflights.send ('POST', 'BookingCreated', { flight, date })
990+
})
991+
))
992+
```
993+
> Note: `this` is an instance of `TravelService`.
963994
964-
This implements a **federated data architecture**:
965995
966-
- External Flights data is consumed via projection
967-
- Local replicas enable offline/cached access
968-
- Event-driven sync keeps both sides consistent
969-
- The domain model integrates external data seamlessly with local entities
996+
#### Delegating Value Help Requests
970997
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:
971999
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))
1004+
```
1005+
:::
9721006
973-
### Agnostic Service APIs
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.
9741008
9751009
9761010
## Service Bindings

0 commit comments

Comments
 (0)