diff --git a/.cdsrc.yaml b/.cdsrc.yaml
new file mode 100644
index 0000000..63bea63
--- /dev/null
+++ b/.cdsrc.yaml
@@ -0,0 +1,67 @@
+features:
+ ieee754compatible: true # REVISIT: Why do we need that ??
+"[fed-srv]":
+ features:
+ with_mocks: false
+ requires:
+ sap.capire.flights.data:
+ model: "@capire/xflights-api"
+ credentials:
+ url: http://localhost:4005/hcql/data/
+"[rep-srv]":
+ features:
+ with_mocks: false
+ requires:
+ sap.capire.flights.data:
+ kind: odata
+ model: "db/rep-srv"
+ credentials:
+ url: http://localhost:4005/odata/v4/data/
+ loadOnDemand: true
+"[fed-vrt]":
+ features:
+ with_mocks: false
+ requires:
+ db: hana
+ sap.capire.flights.data:
+ kind: odata
+ credentials:
+ remote: xflights
+ schema: XFLIGHTS_XFLIGHTS
+ supportRealTimeCDC: true
+"[fed-syn]":
+ features:
+ with_mocks: false
+ requires:
+ db: hana
+ sap.capire.flights.data:
+ kind: odata
+ vcap: xflights
+ credentials:
+ schema: XFLIGHTS_XFLIGHTS
+"[fed-ds]":
+ features:
+ with_mocks: false
+ requires:
+ db: hana
+ sap.capire.flights.data:
+ kind: odata
+ vcap:
+ name: xflights-datasphere
+ credentials:
+ sql:
+ names: quoted
+"[fed-rap]":
+ features:
+ with_mocks: false
+ requires:
+ db: hana
+ rap_v2:
+ kind: odata
+ model: db/abap
+ credentials:
+ remote: sflight-rap
+ sql:
+ names: quoted
+ service: false
+ supportRealTimeCDC: false
diff --git a/.github/actions/hxe/Dockerfile b/.github/actions/hxe/Dockerfile
new file mode 100644
index 0000000..7fa3789
--- /dev/null
+++ b/.github/actions/hxe/Dockerfile
@@ -0,0 +1,7 @@
+FROM saplabs/hanaexpress:latest
+
+COPY ./start-hdi.sql /usr/sap/HXE/start-hdi.sql
+COPY ./setup.sh /setup
+
+# Do initial boot
+RUN /setup
diff --git a/.github/actions/hxe/action.yml b/.github/actions/hxe/action.yml
new file mode 100644
index 0000000..e923e05
--- /dev/null
+++ b/.github/actions/hxe/action.yml
@@ -0,0 +1,54 @@
+name: 'Start HANA'
+description: 'Starts an local HANA Express instance for isolated testing'
+inputs:
+ GITHUB_TOKEN:
+ description: 'Derivative token for using the GitHub REST API'
+ required: true
+outputs:
+ TAG:
+ description: "The Image Tag"
+ value: ${{ steps.find-hxe.outputs.TAG }}
+ IMAGE_ID:
+ description: "The Image ID"
+ value: ${{ steps.find-hxe.outputs.IMAGE_ID }}
+runs:
+ using: "composite"
+ steps:
+ - name: Find HXE image
+ id: find-hxe
+ shell: bash
+ run: |
+ TAG="$(sha1sum ${{ github.action_path }}/* | sha1sum --tag | grep '[^ ]*$' -o)";
+ IMAGE_ID=ghcr.io/${{ github.repository_owner }}/hanaexpress;
+ IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]');
+ echo "TAG=${TAG}" >> $GITHUB_OUTPUT;
+ echo "IMAGE_ID=${IMAGE_ID}" >> $GITHUB_OUTPUT;
+ GHCR_TOKEN=$(echo ${{ inputs.GITHUB_TOKEN }} | base64);
+ if
+ curl -H "Authorization: Bearer ${GHCR_TOKEN}" https://ghcr.io/v2/${{ github.repository_owner }}/hanaexpress/manifests/$TAG | grep "MANIFEST_UNKNOWN";
+ then
+ echo "BUILD_HXE=true" >> $GITHUB_OUTPUT
+ else
+ echo "BUILD_HXE=false" >> $GITHUB_OUTPUT
+ fi;
+ - name: Set up Docker Buildx
+ if: ${{ steps.find-hxe.outputs.BUILD_HXE == 'true' }}
+ uses: docker/setup-buildx-action@v3
+ - name: Build HXE image
+ if: ${{ steps.find-hxe.outputs.BUILD_HXE == 'true' }}
+ shell: bash
+ run: |
+ echo "${{ inputs.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin;
+ DOCKER_BUILDKIT=1 docker build -t $IMAGE_ID:$TAG ${{ github.action_path }}/;
+ docker push $IMAGE_ID:$TAG;
+ env:
+ TAG: ${{ steps.find-hxe.outputs.TAG }}
+ IMAGE_ID: ${{ steps.find-hxe.outputs.IMAGE_ID }}
+ - name: Start HXE image
+ shell: bash
+ run: |
+ echo "${{ inputs.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin;
+ { ${{ github.action_path }}/start.sh; } &
+ env:
+ TAG: ${{ steps.find-hxe.outputs.TAG }}
+ IMAGE_ID: ${{ steps.find-hxe.outputs.IMAGE_ID }}
diff --git a/.github/actions/hxe/ci.yml b/.github/actions/hxe/ci.yml
new file mode 100644
index 0000000..3a921dc
--- /dev/null
+++ b/.github/actions/hxe/ci.yml
@@ -0,0 +1,28 @@
+# Use postgres/example user/password credentials
+version: '3.1'
+
+services:
+ hana:
+ image: ${IMAGE_ID}:${TAG}
+ restart: always
+ hostname: buildkitsandbox
+ ulimits:
+ nofile:
+ soft: 1048576
+ hard: 1048576
+ #sysctls:
+ # - kernel.shmmax=1073741824
+ # - net.ipv4.ip_local_port_range='60000 65535'
+ # - kernel.shmmni=4096
+ # - kernel.shmall=8388608
+ ports:
+ # Currently the only port being used is 39041
+ - '30041:39041'
+ # - '30013:39013'
+ # - '30015:39015'
+ # - '30041-30045:39041-39045'
+ # - '1128-1129:1128-1129'
+ # - '50013-50014:59013-59014'
+ # - '30030-30033:39030-39033'
+ # - '51000-51060:51000-51060'
+ # - '53075:53075'
diff --git a/.github/actions/hxe/setup.sh b/.github/actions/hxe/setup.sh
new file mode 100755
index 0000000..be90062
--- /dev/null
+++ b/.github/actions/hxe/setup.sh
@@ -0,0 +1,6 @@
+/run_hana --agree-to-sap-license --dont-check-system --dont-check-mount-points --master-password Manager1 &
+until /check_hana_health -n -e ready-status > /dev/null; do sleep 1; done;
+/usr/sap/HXE/HDB90/exe/hdbsql -i 90 -d SYSTEMDB -u SYSTEM -p Manager1 -I /usr/sap/HXE/start-hdi.sql
+
+kill -TERM -- -0
+wait
diff --git a/.github/actions/hxe/start-hdi.sql b/.github/actions/hxe/start-hdi.sql
new file mode 100644
index 0000000..91c0a1f
--- /dev/null
+++ b/.github/actions/hxe/start-hdi.sql
@@ -0,0 +1,43 @@
+DO
+BEGIN
+ DECLARE dbName NVARCHAR(25) = 'HXE';
+ DECLARE diserverCount INT = 0;
+ DECLARE dpserverCount INT = 0;
+
+ -- Ensures that the HDI is enabled on the system
+ SELECT COUNT(*) INTO diserverCount FROM SYS_DATABASES.M_SERVICES WHERE SERVICE_NAME = 'diserver' AND DATABASE_NAME = :dbName AND ACTIVE_STATUS = 'YES';
+ IF diserverCount = 0 THEN
+ EXEC 'ALTER DATABASE ' || :dbName || ' ADD ''diserver''';
+ END IF;
+
+ -- Ensure that remote sources are enabled on the system
+ SELECT COUNT(*) INTO dpserverCount FROM SYS_DATABASES.M_SERVICES WHERE SERVICE_NAME = 'dpserver' AND DATABASE_NAME = :dbName AND ACTIVE_STATUS = 'YES';
+ IF dpserverCount = 0 THEN
+ EXEC 'ALTER DATABASE ' || :dbName || ' ADD ''dpserver''';
+ END IF;
+END;
+
+-- Grants HDI privileges to SYSTEM
+CREATE LOCAL TEMPORARY TABLE #PRIVILEGES LIKE _SYS_DI.TT_API_PRIVILEGES;
+INSERT INTO #PRIVILEGES (PRINCIPAL_NAME, PRIVILEGE_NAME, OBJECT_NAME) SELECT 'SYSTEM', PRIVILEGE_NAME, OBJECT_NAME FROM _SYS_DI.T_DEFAULT_DI_ADMIN_PRIVILEGES;
+CALL _SYS_DI.GRANT_CONTAINER_GROUP_API_PRIVILEGES('_SYS_DI', #PRIVILEGES, _SYS_DI.T_NO_PARAMETERS, ?, ?, ?);
+DROP TABLE #PRIVILEGES;
+
+-- Forces all statistics tables to use NSE
+CALL _SYS_STATISTICS.SHARED_ALTER_PAGE_LOADABLE;
+
+-- Selects all tables that are loaded and unloads them from memory
+DO
+BEGIN
+ DECLARE v_isbn VARCHAR(20) = '';
+ DECLARE CURSOR c_cursor1 (v_isbn VARCHAR(20)) FOR
+ SELECT schema_name,table_name FROM m_cs_tables WHERE loaded != 'NO';
+
+ FOR cur_row AS c_cursor1(v_isbn) DO
+ EXEC 'UNLOAD ' || :cur_row.schema_name || '.' || :cur_row.table_name || ' DELETE PERSISTENT MEMORY';
+ END FOR;
+END;
+
+-- Configure maximum memory allocation to 8192MiB as this does not translate to physical memory
+ALTER SYSTEM ALTER CONFIGURATION ('global.ini', 'system') SET ('memorymanager', 'global_allocation_limit') = '10240' WITH RECONFIGURE;
+ALTER SYSTEM ALTER CONFIGURATION ('indexserver.ini', 'DATABASE', 'HXE') SET ('smart_data_access', 'enable_loopback') = 'TRUE' WITH RECONFIGURE;
diff --git a/.github/actions/hxe/start.sh b/.github/actions/hxe/start.sh
new file mode 100755
index 0000000..364cbab
--- /dev/null
+++ b/.github/actions/hxe/start.sh
@@ -0,0 +1,2 @@
+docker compose -f ci.yml up -d;
+docker exec hxe-hana-1 bash -c "until /check_hana_health -n -e ready-status > /dev/null; do sleep 1; done;"
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..bcd817e
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,44 @@
+name: Tests
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ types: [opened, synchronize, reopened, auto_merge_enabled]
+
+permissions:
+ contents: read
+
+# Allow parallel jobs on `main`, so that each commit is tested. For PRs, run only the latest commit.
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ name: Tests
+ permissions:
+ packages: write
+
+ strategy:
+ fail-fast: true
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: 'npm'
+ - run: npm ci
+ - id: hxe
+ uses: ./.github/actions/hxe
+ with:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # testing
+ - run: npm test
+ env:
+ FORCE_COLOR: true
+ TAG: ${{ steps.hxe.outputs.TAG }}
+ IMAGE_ID: ${{ steps.hxe.outputs.IMAGE_ID }}
diff --git a/.plugins/abap/cds-plugin.js b/.plugins/abap/cds-plugin.js
new file mode 100644
index 0000000..dedc42e
--- /dev/null
+++ b/.plugins/abap/cds-plugin.js
@@ -0,0 +1,33 @@
+const cds = require('@sap/cds')
+
+if (cds.env.profiles.includes('fed-rap')) {
+ // Just a little hijack to inject the rap sflight model to xflights model transformation
+ const resolver = require('@sap/cds-compiler/lib/utils/moduleResolve.js')
+
+ const _makeModuleResolver = resolver.makeModuleResolver
+ resolver.makeModuleResolver = function () {
+ const ret = _makeModuleResolver.apply(this, arguments)
+ return {
+ resolveModule: async function (dep) {
+ if (dep.module in { '@capire/xflights': 1, '@capire/xflights-api': 1 }) {
+ return __dirname + '/index.cds'
+ }
+ return ret.resolveModule(dep)
+ }
+ }
+ }
+
+ const _makeModuleResolverSync = resolver.makeModuleResolverSync
+ resolver.makeModuleResolverSync = function (a, b, c, d) {
+ const ret = _makeModuleResolverSync.apply(this, arguments)
+ return {
+ resolveModule: function (dep) {
+ if (dep.module in { '@capire/xflights': 1, '@capire/xflights-api': 1 }) {
+ return __dirname + '/index.cds'
+ }
+ return ret.resolveModule(dep)
+ }
+ }
+ }
+
+}
diff --git a/.plugins/abap/compose.yml b/.plugins/abap/compose.yml
new file mode 100644
index 0000000..4a55b3f
--- /dev/null
+++ b/.plugins/abap/compose.yml
@@ -0,0 +1,19 @@
+services:
+ abap:
+ # username: SAP* / DEVELOPER
+ # password: ABAPtr2022#01
+ image: sapse/abap-cloud-developer-trial:ABAPTRIAL_2022_SP01
+ restart: always
+ hostname: vhcala4hci
+ command:
+ - -agree-to-sap-license
+ - -skip-limits-check
+ ports:
+ - 3200:3200
+ - 3300:3300
+ - 8443:8443
+ - 30213:30213
+ - 50000:50000
+ - 50001:50001
+ # volumes:
+ # - "./A4H_Multiple.txt:/opt/sap/ASABAP_license"
diff --git a/.plugins/abap/index.cds b/.plugins/abap/index.cds
new file mode 100644
index 0000000..50e6275
--- /dev/null
+++ b/.plugins/abap/index.cds
@@ -0,0 +1,71 @@
+using { sap, sap.common.CodeList } from '@sap/cds/common';
+using { rap_v2 as my } from './srv/external/rap-v2';
+
+@odata @rest @hcql @data.product
+service sap.capire.flights.data {
+
+ // Serve Flights data with inlined connection details
+ entity Flights as projection on my.Flight {
+ key ConnectionID as ID,
+ to_Connection.{
+ AirlineID as airline_ID,
+ DepartureAirport as origin_ID,
+ DestinationAirport as destination_ID,
+ DepartureTime as departure,
+ ArrivalTime as arrival,
+ Distance as distance,
+ to_DepartureAirport as origin: redirected to Airlines
+ on $self.origin.ID = $self.origin_ID,
+ to_DestinationAirport as destination: redirected to Airlines
+ on $self.destination.ID = $self.destination_ID,
+ },
+ key FlightDate as date, // preserve the flight date as a key
+ PlaneType as aircraft,
+ Price as price, CurrencyCode as currency_code,
+ MaximumSeats as maximum_seats,
+ OccupiedSeats as occupied_seats,
+ MaximumSeats - OccupiedSeats as free_seats: Integer,
+ to_Airline as airline: redirected to Airlines on $self.airline_ID = airline.ID,
+ to_Connection as flight,
+ };
+
+ // Serve Airlines, Airports, and Supplements data as is
+ entity Airlines as projection on my.Airline {
+ key AirlineID as ID,
+ null as icon: LargeBinary,
+ Name as name,
+ CurrencyCode as currency_code,
+ };
+ entity Airports as projection on my.Airport {
+ AirportID as ID,
+ Name as name,
+ City as city,
+ CountryCode as country_code,
+ };
+ entity Supplements as select from my.Supplement mixin {
+ type: Association to SupplementTypes on SupplementCategory = type.code;
+ } into {
+ key SupplementID as ID,
+ type,
+ SupplementCategory as type_code,
+ to_SupplementText[where LanguageCode = session_context('$user.locale')].Description as descr,
+ Price as price,
+ to_Currency as currency,
+ CurrencyCode as currency_code,
+ };
+
+ entity SupplementTypes : CodeList {
+ key code : String(2) enum {
+ Beverage = 'BV';
+ Meal = 'ML';
+ Luggage = 'LU';
+ Extra = 'EX';
+ }
+ }
+
+ // Serve data for common entities from @sap/cds/common
+ entity Currencies as projection on sap.common.Currencies;
+ entity Countries as projection on sap.common.Countries;
+ entity Languages as projection on sap.common.Languages;
+
+}
diff --git a/.plugins/abap/license.sh b/.plugins/abap/license.sh
new file mode 100755
index 0000000..2030501
--- /dev/null
+++ b/.plugins/abap/license.sh
@@ -0,0 +1,2 @@
+docker cp ./A4H_Multiple.txt abap-abap-1:/opt/sap/ASABAP_license
+docker exec -it abap-abap-1 /usr/local/bin/asabap_license_update
diff --git a/.plugins/abap/package.json b/.plugins/abap/package.json
new file mode 100644
index 0000000..b172bd9
--- /dev/null
+++ b/.plugins/abap/package.json
@@ -0,0 +1,4 @@
+{
+ "name": "@capire/xflights-abap-api",
+ "version": "1.0.0"
+}
diff --git a/.plugins/abap/srv/external/rap-v2.csn b/.plugins/abap/srv/external/rap-v2.csn
new file mode 100644
index 0000000..dd3a023
--- /dev/null
+++ b/.plugins/abap/srv/external/rap-v2.csn
@@ -0,0 +1,2678 @@
+{
+ "meta": {
+ "creator": "cds-dk 9.0.6"
+ },
+ "$version": "2.0",
+ "definitions": {
+ "rap_v2": {
+ "kind": "service",
+ "@cds.external": true,
+ "@m.IsDefaultEntityContainer": "true",
+ "@sap.message.scope.supported": "true",
+ "@sap.supported.formats": "atom json xlsx pdf"
+ },
+ "rap_v2.BookingSupplement": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.searchable": "true",
+ "@sap.content.version": "1",
+ "@sap.deletable.path": "Delete_mc",
+ "@sap.updatable.path": "Update_mc",
+ "@sap.label": "BookingSuppl Proj View for Draft RefScen",
+ "elements": {
+ "BookSupplUUID": {
+ "key": true,
+ "type": "cds.UUID",
+ "@sap.label": "UUID",
+ "@sap.quickinfo": "16 Byte UUID in 16 Bytes (Raw Format)",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "notNull": true
+ },
+ "Delete_mc": {
+ "type": "cds.Boolean",
+ "@sap.label": "Dyn. Method Control",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false"
+ },
+ "Update_mc": {
+ "type": "cds.Boolean",
+ "@sap.label": "Dyn. Method Control",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false"
+ },
+ "TravelUUID": {
+ "type": "cds.UUID",
+ "@sap.label": "UUID",
+ "@sap.quickinfo": "16 Byte UUID in 16 Bytes (Raw Format)",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "BookingUUID": {
+ "type": "cds.UUID",
+ "@sap.label": "UUID",
+ "@sap.quickinfo": "16 Byte UUID in 16 Bytes (Raw Format)",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "BookingSupplementID": {
+ "type": "cds.String",
+ "length": 2,
+ "@sap.display.format": "NonNegative",
+ "@sap.label": "Book. Supp. Number",
+ "@sap.quickinfo": "Flight Reference Scenario: Booking Supplement ID",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "SupplementID": {
+ "type": "cds.String",
+ "length": 10,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "SupplementDescription",
+ "@sap.label": "Product ID",
+ "@sap.quickinfo": "Flight Reference Scenario: Supplement ID",
+ "@sap.value.list": "standard"
+ },
+ "SupplementDescription": {
+ "type": "cds.String",
+ "length": 1024,
+ "@sap.label": "Product Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Supplement Description",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "BookSupplPrice": {
+ "type": "cds.Decimal",
+ "precision": 16,
+ "scale": 3,
+ "@sap.unit": "CurrencyCode",
+ "@sap.variable.scale": "true",
+ "@sap.label": "Product Price",
+ "@sap.quickinfo": "Flight Reference Scenario: Supplement Price"
+ },
+ "CurrencyCode": {
+ "type": "cds.String",
+ "length": 5,
+ "@sap.label": "Currency Code",
+ "@sap.quickinfo": "Flight Reference Scenario: Currency Code",
+ "@sap.value.list": "standard",
+ "@sap.semantics": "currency-code"
+ },
+ "LocalLastChangedAt": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.label": "Changed On",
+ "@sap.quickinfo": "Local Instance Last Change Date Time",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "to_Product": {
+ "type": "cds.Association",
+ "target": "rap_v2.Supplement",
+ "keys": []
+ },
+ "to_SupplementText": {
+ "type": "cds.Association",
+ "target": "rap_v2.SupplementText",
+ "cardinality": {
+ "max": "*"
+ },
+ "on": [{"ref":["SupplementID"]}, "=", {"ref":["to_SupplementText","SupplementID"]}]
+ },
+ "to_Travel": {
+ "type": "cds.Association",
+ "target": "rap_v2.Travel",
+ "keys": []
+ },
+ "to_Booking": {
+ "type": "cds.Association",
+ "target": "rap_v2.Booking",
+ "keys": []
+ }
+ }
+ },
+ "rap_v2.Booking": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.searchable": "true",
+ "@sap.content.version": "1",
+ "@sap.deletable.path": "Delete_mc",
+ "@sap.updatable.path": "Update_mc",
+ "@sap.label": "Booking Proj View for Draft RefScen",
+ "elements": {
+ "BookingUUID": {
+ "key": true,
+ "type": "cds.UUID",
+ "@sap.label": "UUID",
+ "@sap.quickinfo": "16 Byte UUID in 16 Bytes (Raw Format)",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "notNull": true
+ },
+ "Delete_mc": {
+ "type": "cds.Boolean",
+ "@sap.label": "Dyn. Method Control",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false"
+ },
+ "Update_mc": {
+ "type": "cds.Boolean",
+ "@sap.label": "Dyn. Method Control",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false"
+ },
+ "to_BookingSupplement_oc": {
+ "type": "cds.Boolean",
+ "@sap.label": "Dynamic CbA-Control",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false"
+ },
+ "TravelUUID": {
+ "type": "cds.UUID",
+ "@sap.label": "UUID",
+ "@sap.quickinfo": "16 Byte UUID in 16 Bytes (Raw Format)",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "BookingID": {
+ "type": "cds.String",
+ "length": 4,
+ "@sap.display.format": "NonNegative",
+ "@sap.label": "Booking Number",
+ "@sap.quickinfo": "Flight Reference Scenario: Booking ID",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "BookingDate": {
+ "type": "cds.Date",
+ "@sap.display.format": "Date",
+ "@sap.label": "Booking Date",
+ "@sap.quickinfo": "Flight Reference Scenario: Booking Date",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "CustomerID": {
+ "type": "cds.String",
+ "length": 6,
+ "@sap.display.format": "NonNegative",
+ "@sap.text": "CustomerName",
+ "@sap.label": "Customer ID",
+ "@sap.quickinfo": "Flight Reference Scenario: Customer ID",
+ "@sap.value.list": "standard"
+ },
+ "CustomerName": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "Last Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Last Name",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "AirlineID": {
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "CarrierName",
+ "@sap.label": "Airline ID",
+ "@sap.quickinfo": "Flight Reference Scenario: Carrier ID",
+ "@sap.value.list": "standard"
+ },
+ "CarrierName": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "Airline Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Carrier Name",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "ConnectionID": {
+ "type": "cds.String",
+ "length": 4,
+ "@sap.display.format": "NonNegative",
+ "@sap.label": "Flight Number",
+ "@sap.quickinfo": "Flight Reference Scenario: Connection ID",
+ "@sap.value.list": "standard"
+ },
+ "FlightDate": {
+ "type": "cds.Date",
+ "@sap.display.format": "Date",
+ "@sap.label": "Flight Date",
+ "@sap.quickinfo": "Flight Reference Scenario: Flight Date",
+ "@sap.value.list": "standard"
+ },
+ "FlightPrice": {
+ "type": "cds.Decimal",
+ "precision": 16,
+ "scale": 3,
+ "@sap.unit": "CurrencyCode",
+ "@sap.variable.scale": "true",
+ "@sap.label": "Flight Price",
+ "@sap.quickinfo": "Flight Reference Scenario: Flight Price",
+ "@sap.value.list": "standard"
+ },
+ "CurrencyCode": {
+ "type": "cds.String",
+ "length": 5,
+ "@sap.label": "Currency Code",
+ "@sap.quickinfo": "Flight Reference Scenario: Currency Code",
+ "@sap.value.list": "standard",
+ "@sap.semantics": "currency-code"
+ },
+ "BookingStatus": {
+ "type": "cds.String",
+ "length": 1,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "BookingStatusText",
+ "@sap.label": "Booking Status",
+ "@sap.quickinfo": "Flight Reference Scenario: Travel Status",
+ "@sap.value.list": "fixed-values"
+ },
+ "BookingStatusText": {
+ "type": "cds.String",
+ "length": 1024,
+ "@sap.label": "Booking Status",
+ "@sap.quickinfo": "Overall Status Text",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "LocalLastChangedAt": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.label": "Changed On",
+ "@sap.quickinfo": "Local Instance Last Change Date Time",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "to_BookingStatus": {
+ "type": "cds.Association",
+ "target": "rap_v2.BookingStatusVH",
+ "keys": []
+ },
+ "to_BookingSupplement": {
+ "type": "cds.Composition",
+ "target": "rap_v2.BookingSupplement",
+ "cardinality": {
+ "max": "*"
+ },
+ "keys": []
+ },
+ "to_Carrier": {
+ "type": "cds.Association",
+ "target": "rap_v2.Airline",
+ "keys": []
+ },
+ "to_Connection": {
+ "type": "cds.Association",
+ "target": "rap_v2.FlightConnection",
+ "keys": []
+ },
+ "to_Customer": {
+ "type": "cds.Association",
+ "target": "rap_v2.Passenger",
+ "keys": []
+ },
+ "to_Travel": {
+ "type": "cds.Association",
+ "target": "rap_v2.Travel",
+ "keys": []
+ }
+ }
+ },
+ "rap_v2.Travel": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.searchable": "true",
+ "@sap.content.version": "1",
+ "@sap.deletable.path": "Delete_mc",
+ "@sap.updatable.path": "Update_mc",
+ "@sap.label": "Travel Projection View for Draft RefScen",
+ "elements": {
+ "TravelUUID": {
+ "key": true,
+ "type": "cds.UUID",
+ "@sap.label": "UUID",
+ "@sap.quickinfo": "16 Byte UUID in 16 Bytes (Raw Format)",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "notNull": true
+ },
+ "acceptTravel_ac": {
+ "type": "cds.Boolean",
+ "@sap.label": "Dyn. Action Control",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false"
+ },
+ "deductDiscount_ac": {
+ "type": "cds.Boolean",
+ "@sap.label": "Dyn. Action Control",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false"
+ },
+ "rejectTravel_ac": {
+ "type": "cds.Boolean",
+ "@sap.label": "Dyn. Action Control",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false"
+ },
+ "BookingFee_fc": {
+ "type": "cds.Integer",
+ "@odata.Type": "Edm.Byte",
+ "@sap.label": "Dyn. Field Control",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false"
+ },
+ "Delete_mc": {
+ "type": "cds.Boolean",
+ "@sap.label": "Dyn. Method Control",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false"
+ },
+ "Update_mc": {
+ "type": "cds.Boolean",
+ "@sap.label": "Dyn. Method Control",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false"
+ },
+ "to_Booking_oc": {
+ "type": "cds.Boolean",
+ "@sap.label": "Dynamic CbA-Control",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false"
+ },
+ "TravelID": {
+ "type": "cds.String",
+ "length": 8,
+ "@sap.display.format": "NonNegative",
+ "@sap.label": "Travel ID",
+ "@sap.quickinfo": "Flight Reference Scenario: Travel ID",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "AgencyID": {
+ "type": "cds.String",
+ "length": 6,
+ "@sap.display.format": "NonNegative",
+ "@sap.text": "AgencyName",
+ "@sap.label": "Agency ID",
+ "@sap.quickinfo": "Flight Reference Scenario: Agency ID",
+ "@sap.value.list": "standard"
+ },
+ "AgencyName": {
+ "type": "cds.String",
+ "length": 80,
+ "@sap.label": "Agency Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Agency Name",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "CustomerID": {
+ "type": "cds.String",
+ "length": 6,
+ "@sap.display.format": "NonNegative",
+ "@sap.text": "CustomerName",
+ "@sap.label": "Customer ID",
+ "@sap.quickinfo": "Flight Reference Scenario: Customer ID",
+ "@sap.value.list": "standard"
+ },
+ "CustomerName": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "Last Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Last Name",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "BeginDate": {
+ "type": "cds.Date",
+ "@sap.display.format": "Date",
+ "@sap.label": "Starting Date",
+ "@sap.quickinfo": "Flight Reference Scenario: Start Date"
+ },
+ "EndDate": {
+ "type": "cds.Date",
+ "@sap.display.format": "Date",
+ "@sap.label": "End Date",
+ "@sap.quickinfo": "Flight Reference Scenario: End Date"
+ },
+ "BookingFee": {
+ "type": "cds.Decimal",
+ "precision": 16,
+ "scale": 3,
+ "@sap.field.control": "BookingFee_fc",
+ "@sap.unit": "CurrencyCode",
+ "@sap.variable.scale": "true",
+ "@sap.label": "Booking Fee",
+ "@sap.quickinfo": "Flight Reference Scenario: Booking Fee"
+ },
+ "TotalPrice": {
+ "type": "cds.Decimal",
+ "precision": 16,
+ "scale": 3,
+ "@sap.unit": "CurrencyCode",
+ "@sap.variable.scale": "true",
+ "@sap.label": "Total Price",
+ "@sap.quickinfo": "Flight Reference Scenario: Total Price",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "CurrencyCode": {
+ "type": "cds.String",
+ "length": 5,
+ "@sap.label": "Currency Code",
+ "@sap.quickinfo": "Flight Reference Scenario: Currency Code",
+ "@sap.value.list": "standard",
+ "@sap.semantics": "currency-code"
+ },
+ "Description": {
+ "type": "cds.String",
+ "length": 1024,
+ "@sap.label": "Description",
+ "@sap.quickinfo": "Flight Reference Scenario: Description"
+ },
+ "OverallStatus": {
+ "type": "cds.String",
+ "length": 1,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "OverallStatusText",
+ "@sap.label": "Overall Status",
+ "@sap.quickinfo": "Flight Reference Scenario: Travel Status",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.value.list": "fixed-values"
+ },
+ "OverallStatusText": {
+ "type": "cds.String",
+ "length": 1024,
+ "@sap.label": "Overall Status",
+ "@sap.quickinfo": "Overall Status Text",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "LocalLastChangedAt": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.label": "Changed On",
+ "@sap.quickinfo": "Local Instance Last Change Date Time",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "to_Agency": {
+ "type": "cds.Association",
+ "target": "rap_v2.TravelAgency",
+ "keys": []
+ },
+ "to_Booking": {
+ "type": "cds.Composition",
+ "target": "rap_v2.Booking",
+ "cardinality": {
+ "max": "*"
+ },
+ "keys": []
+ },
+ "to_Currency": {
+ "type": "cds.Association",
+ "target": "rap_v2.Currency",
+ "keys": []
+ },
+ "to_Customer": {
+ "type": "cds.Association",
+ "target": "rap_v2.Passenger",
+ "keys": []
+ },
+ "to_OverallStatus": {
+ "type": "cds.Association",
+ "target": "rap_v2.OverallStatusVH",
+ "keys": []
+ }
+ },
+ "actions": {
+ "acceptTravel": {
+ "kind": "action",
+ "returns": {
+ "type": "rap_v2.Travel"
+ }
+ },
+ "deductDiscount": {
+ "kind": "action",
+ "params": {
+ "discount_percent": {
+ "type": "cds.Integer",
+ "@odata.Type": "Edm.Byte"
+ }
+ },
+ "returns": {
+ "type": "rap_v2.Travel"
+ }
+ },
+ "rejectTravel": {
+ "kind": "action",
+ "returns": {
+ "type": "rap_v2.Travel"
+ }
+ }
+ }
+ },
+ "rap_v2.TravelAgency": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.searchable": "true",
+ "@sap.content.version": "1",
+ "@sap.label": "Agency View - CDS Data Model",
+ "elements": {
+ "AgencyID": {
+ "key": true,
+ "type": "cds.String",
+ "length": 6,
+ "@sap.display.format": "NonNegative",
+ "@sap.text": "Name",
+ "@sap.label": "Agency ID",
+ "@sap.quickinfo": "Flight Reference Scenario: Agency ID",
+ "notNull": true
+ },
+ "Name": {
+ "type": "cds.String",
+ "length": 80,
+ "@sap.label": "Agency Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Agency Name"
+ },
+ "Street": {
+ "type": "cds.String",
+ "length": 60,
+ "@sap.label": "Street",
+ "@sap.quickinfo": "Flight Reference Scenario: Street"
+ },
+ "PostalCode": {
+ "type": "cds.String",
+ "length": 10,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Postal Code",
+ "@sap.quickinfo": "Flight Reference Scenario: Postal Code"
+ },
+ "City": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "City",
+ "@sap.quickinfo": "Flight Reference Scenario: City"
+ },
+ "CountryCode": {
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Country/Region Key",
+ "@sap.value.list": "standard"
+ },
+ "PhoneNumber": {
+ "type": "cds.String",
+ "length": 30,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Phone No.",
+ "@sap.quickinfo": "Flight Reference Scenario: Phone Number",
+ "@sap.semantics": "tel"
+ },
+ "EMailAddress": {
+ "type": "cds.String",
+ "length": 256,
+ "@sap.label": "E-Mail Address",
+ "@sap.quickinfo": "Flight Reference Scenario: Email Address",
+ "@sap.semantics": "email"
+ },
+ "WebAddress": {
+ "type": "cds.String",
+ "length": 256,
+ "@sap.label": "Web Address",
+ "@sap.quickinfo": "Flight Reference Scenario: Web Address"
+ },
+ "MimeType": {
+ "type": "cds.String",
+ "length": 128,
+ "@sap.label": "Mime Type",
+ "@sap.quickinfo": "MimeType"
+ },
+ "Filename": {
+ "type": "cds.String",
+ "length": 128,
+ "@sap.label": "Filename"
+ },
+ "LocalCreatedBy": {
+ "type": "cds.String",
+ "length": 12,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Created By",
+ "@sap.quickinfo": "Created By User"
+ },
+ "LocalCreatedAt": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.label": "Created On",
+ "@sap.quickinfo": "Creation Date Time"
+ },
+ "LocalLastChangedBy": {
+ "type": "cds.String",
+ "length": 12,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Changed By",
+ "@sap.quickinfo": "Local Instance Last Changed By User"
+ },
+ "LocalLastChangedAt": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.label": "Changed On",
+ "@sap.quickinfo": "Local Instance Last Change Date Time"
+ },
+ "LastChangedAt": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.label": "Changed On",
+ "@sap.quickinfo": "Last Change Date Time"
+ },
+ "to_Country": {
+ "type": "cds.Association",
+ "target": "rap_v2.Country",
+ "keys": []
+ }
+ }
+ },
+ "rap_v2.xDMOxI_Agency_StdVH": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.searchable": "true",
+ "@sap.content.version": "1",
+ "@sap.label": "Agency ValueHelp",
+ "@sap.value.list": "true",
+ "elements": {
+ "AgencyID": {
+ "key": true,
+ "type": "cds.String",
+ "length": 6,
+ "@sap.display.format": "NonNegative",
+ "@sap.text": "Name",
+ "@sap.label": "Agency ID",
+ "@sap.quickinfo": "Flight Reference Scenario: Agency ID",
+ "notNull": true
+ },
+ "Name": {
+ "type": "cds.String",
+ "length": 80,
+ "@sap.label": "Agency Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Agency Name"
+ },
+ "Street": {
+ "type": "cds.String",
+ "length": 60,
+ "@sap.label": "Street",
+ "@sap.quickinfo": "Flight Reference Scenario: Street"
+ },
+ "PostalCode": {
+ "type": "cds.String",
+ "length": 10,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Postal Code",
+ "@sap.quickinfo": "Flight Reference Scenario: Postal Code"
+ },
+ "City": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "City",
+ "@sap.quickinfo": "Flight Reference Scenario: City"
+ },
+ "CountryCode": {
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "CountryCodeText",
+ "@sap.label": "Country/Region Key",
+ "@sap.value.list": "standard"
+ },
+ "CountryCodeText": {
+ "type": "cds.String",
+ "length": 50,
+ "@sap.label": "Country/Region Name"
+ },
+ "PhoneNumber": {
+ "type": "cds.String",
+ "length": 30,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Phone No.",
+ "@sap.quickinfo": "Flight Reference Scenario: Phone Number",
+ "@sap.semantics": "tel"
+ },
+ "EMailAddress": {
+ "type": "cds.String",
+ "length": 256,
+ "@sap.label": "E-Mail Address",
+ "@sap.quickinfo": "Flight Reference Scenario: Email Address",
+ "@sap.semantics": "email"
+ },
+ "WebAddress": {
+ "type": "cds.String",
+ "length": 256,
+ "@sap.label": "Web Address",
+ "@sap.quickinfo": "Flight Reference Scenario: Web Address"
+ }
+ }
+ },
+ "rap_v2.Airport": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.searchable": "true",
+ "@sap.content.version": "1",
+ "@sap.label": "Airport View - CDS Data Model",
+ "elements": {
+ "AirportID": {
+ "key": true,
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "Name",
+ "@sap.label": "Airport ID",
+ "@sap.quickinfo": "Flight Reference Scenario: Airport ID",
+ "notNull": true
+ },
+ "Name": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "Airport Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Airport Name"
+ },
+ "City": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "City",
+ "@sap.quickinfo": "Flight Reference Scenario: City"
+ },
+ "CountryCode": {
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Country/Region Key",
+ "@sap.value.list": "standard"
+ },
+ "to_Country": {
+ "type": "cds.Association",
+ "target": "rap_v2.Country",
+ "keys": []
+ }
+ }
+ },
+ "rap_v2.BookingStatusVH": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.content.version": "1",
+ "@sap.label": "Booking Status Value Help",
+ "elements": {
+ "BookingStatus": {
+ "key": true,
+ "type": "cds.String",
+ "length": 1,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "BookingStatus_Text",
+ "@sap.label": "Booking Status",
+ "@sap.quickinfo": "Flight Reference Scenario: Travel Status",
+ "notNull": true
+ },
+ "BookingStatus_Text": {
+ "type": "cds.String",
+ "length": 1024,
+ "@sap.label": "Booking Status",
+ "@sap.quickinfo": "Overall Status Text",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ }
+ }
+ },
+ "rap_v2.Airline": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.searchable": "true",
+ "@sap.content.version": "1",
+ "@sap.label": "Carrier View - CDS Data Model",
+ "elements": {
+ "AirlineID": {
+ "key": true,
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "Name",
+ "@sap.label": "Airline ID",
+ "@sap.quickinfo": "Flight Reference Scenario: Carrier ID",
+ "notNull": true
+ },
+ "Name": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "Airline Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Carrier Name"
+ },
+ "CurrencyCode": {
+ "type": "cds.String",
+ "length": 5,
+ "@sap.label": "Currency Code",
+ "@sap.quickinfo": "Flight Reference Scenario: Currency Code",
+ "@sap.semantics": "currency-code"
+ },
+ "LastChangedAt": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.label": "Changed On",
+ "@sap.quickinfo": "Last Change Date Time"
+ },
+ "LocalCreatedAt": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.label": "Created On",
+ "@sap.quickinfo": "Creation Date Time"
+ },
+ "LocalCreatedBy": {
+ "type": "cds.String",
+ "length": 12,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Created By",
+ "@sap.quickinfo": "Created By User"
+ },
+ "LocalLastChangedAt": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.label": "Changed On",
+ "@sap.quickinfo": "Local Instance Last Change Date Time"
+ },
+ "LocalLastChangedBy": {
+ "type": "cds.String",
+ "length": 12,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Changed By",
+ "@sap.quickinfo": "Local Instance Last Changed By User"
+ },
+ "to_Currency": {
+ "type": "cds.Association",
+ "target": "rap_v2.Currency",
+ "keys": []
+ }
+ }
+ },
+ "rap_v2.xDMOxI_Carrier_StdVH": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.searchable": "true",
+ "@sap.content.version": "1",
+ "@sap.label": "Carrier ValueHelp",
+ "@sap.value.list": "true",
+ "elements": {
+ "AirlineID": {
+ "key": true,
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "Name",
+ "@sap.label": "Airline ID",
+ "@sap.quickinfo": "Flight Reference Scenario: Carrier ID",
+ "notNull": true
+ },
+ "Name": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "Airline Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Carrier Name"
+ },
+ "CurrencyCode": {
+ "type": "cds.String",
+ "length": 5,
+ "@sap.text": "CurrencyName",
+ "@sap.label": "Currency Code",
+ "@sap.quickinfo": "Flight Reference Scenario: Currency Code",
+ "@sap.semantics": "currency-code"
+ },
+ "CurrencyName": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "Description"
+ }
+ }
+ },
+ "rap_v2.FlightConnection": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.searchable": "true",
+ "@sap.content.version": "1",
+ "@sap.label": "Connection View - CDS Data Model",
+ "elements": {
+ "AirlineID": {
+ "key": true,
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "AirlineID_Text",
+ "@sap.label": "Airline ID",
+ "@sap.quickinfo": "Flight Reference Scenario: Carrier ID",
+ "@sap.value.list": "standard",
+ "notNull": true
+ },
+ "ConnectionID": {
+ "key": true,
+ "type": "cds.String",
+ "length": 4,
+ "@sap.display.format": "NonNegative",
+ "@sap.label": "Flight Number",
+ "@sap.quickinfo": "Flight Reference Scenario: Connection ID",
+ "notNull": true
+ },
+ "AirlineID_Text": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "Airline Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Carrier Name",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "DepartureAirport": {
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Departure Airport",
+ "@sap.quickinfo": "Flight Reference Scenario: From Airport",
+ "@sap.value.list": "standard"
+ },
+ "DestinationAirport": {
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Destination Airport",
+ "@sap.quickinfo": "Flight Reference Scenario: To Airport",
+ "@sap.value.list": "standard"
+ },
+ "DepartureTime": {
+ "type": "cds.Time",
+ "@sap.label": "Departure",
+ "@sap.quickinfo": "Flight Reference Scenario: Departure Time"
+ },
+ "ArrivalTime": {
+ "type": "cds.Time",
+ "@sap.label": "Arrival",
+ "@sap.quickinfo": "Flight Reference Scenario: Arrival Time"
+ },
+ "Distance": {
+ "type": "cds.Integer",
+ "@sap.label": "Flight Distance",
+ "@sap.quickinfo": "Flight Reference Scenario: Flight Distance"
+ },
+ "DistanceUnit": {
+ "type": "cds.String",
+ "length": 3,
+ "@sap.label": "Internal UoM",
+ "@sap.quickinfo": "Unit of Measurement",
+ "@sap.semantics": "unit-of-measure"
+ },
+ "to_Airline": {
+ "type": "cds.Association",
+ "target": "rap_v2.Airline",
+ "on": [{"ref":["AirlineID"]},"=",{"ref":["to_Airline","AirlineID"]}]
+ },
+ "to_DepartureAirport": {
+ "type": "cds.Association",
+ "target": "rap_v2.Airport",
+ "on": [{"ref":["DepartureAirport"]},"=",{"ref":["to_DepartureAirport","AirportID"]}]
+ },
+ "to_DestinationAirport": {
+ "type": "cds.Association",
+ "target": "rap_v2.Airport",
+ "on": [{"ref":["DestinationAirport"]},"=",{"ref":["to_DestinationAirport","AirportID"]}]
+ }
+ }
+ },
+ "rap_v2.xDMOxI_Connection_StdVH": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.searchable": "true",
+ "@sap.content.version": "1",
+ "@sap.label": "Connection ValueHelp",
+ "@sap.value.list": "true",
+ "elements": {
+ "AirlineID": {
+ "key": true,
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "AirlineName",
+ "@sap.label": "Airline ID",
+ "@sap.quickinfo": "Flight Reference Scenario: Carrier ID",
+ "@sap.value.list": "standard",
+ "notNull": true
+ },
+ "ConnectionID": {
+ "key": true,
+ "type": "cds.String",
+ "length": 4,
+ "@sap.display.format": "NonNegative",
+ "@sap.label": "Flight Number",
+ "@sap.quickinfo": "Flight Reference Scenario: Connection ID",
+ "notNull": true
+ },
+ "AirlineName": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "Airline Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Carrier Name"
+ },
+ "DepartureAirport": {
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "DepartureAirportName",
+ "@sap.label": "Departure Airport",
+ "@sap.quickinfo": "Flight Reference Scenario: From Airport"
+ },
+ "DepartureAirportName": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "Airport Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Airport Name"
+ },
+ "DestinationAirport": {
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "DestinationAirportName",
+ "@sap.label": "Destination Airport",
+ "@sap.quickinfo": "Flight Reference Scenario: To Airport"
+ },
+ "DestinationAirportName": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "Airport Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Airport Name"
+ },
+ "DepartureTime": {
+ "type": "cds.Time",
+ "@sap.label": "Departure",
+ "@sap.quickinfo": "Flight Reference Scenario: Departure Time"
+ },
+ "ArrivalTime": {
+ "type": "cds.Time",
+ "@sap.label": "Arrival",
+ "@sap.quickinfo": "Flight Reference Scenario: Arrival Time"
+ },
+ "Distance": {
+ "type": "cds.Integer",
+ "@sap.label": "Flight Distance",
+ "@sap.quickinfo": "Flight Reference Scenario: Flight Distance"
+ },
+ "DistanceUnit": {
+ "type": "cds.String",
+ "length": 3,
+ "@sap.label": "Internal UoM",
+ "@sap.quickinfo": "Unit of Measurement",
+ "@sap.semantics": "unit-of-measure"
+ }
+ }
+ },
+ "rap_v2.Passenger": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.searchable": "true",
+ "@sap.content.version": "1",
+ "@sap.label": "Customer View - CDS Data Model",
+ "elements": {
+ "CustomerID": {
+ "key": true,
+ "type": "cds.String",
+ "length": 6,
+ "@sap.display.format": "NonNegative",
+ "@sap.text": "LastName",
+ "@sap.label": "Customer ID",
+ "@sap.quickinfo": "Flight Reference Scenario: Customer ID",
+ "notNull": true
+ },
+ "FirstName": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "First Name",
+ "@sap.quickinfo": "Flight Reference Scenario: First Name"
+ },
+ "LastName": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "Last Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Last Name"
+ },
+ "Title": {
+ "type": "cds.String",
+ "length": 10,
+ "@sap.label": "Title",
+ "@sap.quickinfo": "Flight Reference Scenario: Title"
+ },
+ "Street": {
+ "type": "cds.String",
+ "length": 60,
+ "@sap.label": "Street",
+ "@sap.quickinfo": "Flight Reference Scenario: Street"
+ },
+ "PostalCode": {
+ "type": "cds.String",
+ "length": 10,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Postal Code",
+ "@sap.quickinfo": "Flight Reference Scenario: Postal Code"
+ },
+ "City": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "City",
+ "@sap.quickinfo": "Flight Reference Scenario: City"
+ },
+ "CountryCode": {
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Country/Region Key",
+ "@sap.value.list": "standard"
+ },
+ "PhoneNumber": {
+ "type": "cds.String",
+ "length": 30,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Phone No.",
+ "@sap.quickinfo": "Flight Reference Scenario: Phone Number",
+ "@sap.semantics": "tel"
+ },
+ "EMailAddress": {
+ "type": "cds.String",
+ "length": 256,
+ "@sap.label": "E-Mail Address",
+ "@sap.quickinfo": "Flight Reference Scenario: Email Address",
+ "@sap.semantics": "email"
+ },
+ "to_Country": {
+ "type": "cds.Association",
+ "target": "rap_v2.Country",
+ "keys": []
+ }
+ }
+ },
+ "rap_v2.xDMOxI_Customer_StdVH": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.searchable": "true",
+ "@sap.content.version": "1",
+ "@sap.label": "Customer ValueHelp",
+ "@sap.value.list": "true",
+ "elements": {
+ "CustomerID": {
+ "key": true,
+ "type": "cds.String",
+ "length": 6,
+ "@sap.display.format": "NonNegative",
+ "@sap.text": "LastName",
+ "@sap.label": "Customer ID",
+ "@sap.quickinfo": "Flight Reference Scenario: Customer ID",
+ "notNull": true
+ },
+ "FirstName": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "First Name",
+ "@sap.quickinfo": "Flight Reference Scenario: First Name"
+ },
+ "LastName": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "Last Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Last Name"
+ },
+ "Title": {
+ "type": "cds.String",
+ "length": 10,
+ "@sap.label": "Title",
+ "@sap.quickinfo": "Flight Reference Scenario: Title"
+ },
+ "Street": {
+ "type": "cds.String",
+ "length": 60,
+ "@sap.label": "Street",
+ "@sap.quickinfo": "Flight Reference Scenario: Street"
+ },
+ "PostalCode": {
+ "type": "cds.String",
+ "length": 10,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Postal Code",
+ "@sap.quickinfo": "Flight Reference Scenario: Postal Code"
+ },
+ "City": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "City",
+ "@sap.quickinfo": "Flight Reference Scenario: City"
+ },
+ "CountryCode": {
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "CountryCodeText",
+ "@sap.label": "Country/Region Key",
+ "@sap.value.list": "standard"
+ },
+ "CountryCodeText": {
+ "type": "cds.String",
+ "length": 50,
+ "@sap.label": "Country/Region Name"
+ },
+ "PhoneNumber": {
+ "type": "cds.String",
+ "length": 30,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Phone No.",
+ "@sap.quickinfo": "Flight Reference Scenario: Phone Number",
+ "@sap.semantics": "tel"
+ },
+ "EMailAddress": {
+ "type": "cds.String",
+ "length": 256,
+ "@sap.label": "E-Mail Address",
+ "@sap.quickinfo": "Flight Reference Scenario: Email Address",
+ "@sap.semantics": "email"
+ }
+ }
+ },
+ "rap_v2.Flight": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.searchable": "true",
+ "@sap.content.version": "1",
+ "@sap.label": "Flight View - CDS Data Model",
+ "elements": {
+ "AirlineID": {
+ "key": true,
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "AirlineID_Text",
+ "@sap.label": "Airline ID",
+ "@sap.quickinfo": "Flight Reference Scenario: Carrier ID",
+ "@sap.value.list": "standard",
+ "notNull": true
+ },
+ "ConnectionID": {
+ "key": true,
+ "type": "cds.String",
+ "length": 16,
+ "@sap.display.format": "NonNegative",
+ "@sap.label": "Flight Number",
+ "@sap.quickinfo": "Flight Reference Scenario: Connection ID",
+ "@sap.value.list": "standard",
+ "notNull": true
+ },
+ "FlightDate": {
+ "key": true,
+ "type": "cds.Date",
+ "@sap.display.format": "Date",
+ "@sap.label": "Flight Date",
+ "@sap.quickinfo": "Flight Reference Scenario: Flight Date",
+ "notNull": true
+ },
+ "AirlineID_Text": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "Airline Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Carrier Name",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "Price": {
+ "type": "cds.Decimal",
+ "precision": 16,
+ "scale": 3,
+ "@sap.unit": "CurrencyCode",
+ "@sap.variable.scale": "true",
+ "@sap.label": "Flight Price",
+ "@sap.quickinfo": "Flight Reference Scenario: Flight Price"
+ },
+ "CurrencyCode": {
+ "type": "cds.String",
+ "length": 5,
+ "@sap.label": "Currency Code",
+ "@sap.quickinfo": "Flight Reference Scenario: Currency Code",
+ "@sap.semantics": "currency-code"
+ },
+ "PlaneType": {
+ "type": "cds.String",
+ "length": 10,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Plane Type",
+ "@sap.quickinfo": "Flight Reference Scenario: Plane Type ID"
+ },
+ "MaximumSeats": {
+ "type": "cds.Integer",
+ "@sap.label": "Maximum Capacity",
+ "@sap.quickinfo": "Flight Reference Scenario: Plane Seats Max"
+ },
+ "OccupiedSeats": {
+ "type": "cds.Integer",
+ "@sap.label": "Occupied Seats",
+ "@sap.quickinfo": "Flight Reference Scenario: Plane Seats Occupied"
+ },
+ "to_Airline": {
+ "type": "cds.Association",
+ "target": "rap_v2.Airline",
+ "on": [{"ref": ["AirlineID"]},"=",{"ref":["to_Airline","AirlineID"]}]
+ },
+ "to_Connection": {
+ "type": "cds.Association",
+ "target": "rap_v2.FlightConnection",
+ "on": [{"ref": ["ConnectionID"]},"=",{"ref":["to_Connection","ConnectionID"]}]
+ },
+ "to_Currency": {
+ "type": "cds.Association",
+ "target": "rap_v2.Currency",
+ "on": [{"ref": ["CurrencyCode"]},"=",{"ref":["to_Currency","Currency"]}]
+ }
+ }
+ },
+ "rap_v2.xDMOxI_Flight_StdVH": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.searchable": "true",
+ "@sap.content.version": "1",
+ "@sap.label": "Flight ValueHelp",
+ "@sap.value.list": "true",
+ "elements": {
+ "AirlineID": {
+ "key": true,
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "AirlineName",
+ "@sap.label": "Airline ID",
+ "@sap.quickinfo": "Flight Reference Scenario: Carrier ID",
+ "@sap.value.list": "standard",
+ "notNull": true
+ },
+ "ConnectionID": {
+ "key": true,
+ "type": "cds.String",
+ "length": 4,
+ "@sap.display.format": "NonNegative",
+ "@sap.label": "Flight Number",
+ "@sap.quickinfo": "Flight Reference Scenario: Connection ID",
+ "@sap.value.list": "standard",
+ "notNull": true
+ },
+ "FlightDate": {
+ "key": true,
+ "type": "cds.Date",
+ "@sap.display.format": "Date",
+ "@sap.label": "Flight Date",
+ "@sap.quickinfo": "Flight Reference Scenario: Flight Date",
+ "notNull": true
+ },
+ "Price": {
+ "type": "cds.Decimal",
+ "precision": 16,
+ "scale": 3,
+ "@sap.unit": "CurrencyCode",
+ "@sap.variable.scale": "true",
+ "@sap.label": "Flight Price",
+ "@sap.quickinfo": "Flight Reference Scenario: Flight Price"
+ },
+ "CurrencyCode": {
+ "type": "cds.String",
+ "length": 5,
+ "@sap.label": "Currency Code",
+ "@sap.quickinfo": "Flight Reference Scenario: Currency Code",
+ "@sap.semantics": "currency-code"
+ },
+ "PlaneType": {
+ "type": "cds.String",
+ "length": 10,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Plane Type",
+ "@sap.quickinfo": "Flight Reference Scenario: Plane Type ID"
+ },
+ "MaximumSeats": {
+ "type": "cds.Integer",
+ "@sap.label": "Maximum Capacity",
+ "@sap.quickinfo": "Flight Reference Scenario: Plane Seats Max"
+ },
+ "OccupiedSeats": {
+ "type": "cds.Integer",
+ "@sap.label": "Occupied Seats",
+ "@sap.quickinfo": "Flight Reference Scenario: Plane Seats Occupied"
+ },
+ "AirlineName": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "Airline Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Carrier Name"
+ }
+ }
+ },
+ "rap_v2.OverallStatusVH": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.content.version": "1",
+ "@sap.label": "Overall Status Value Help",
+ "elements": {
+ "OverallStatus": {
+ "key": true,
+ "type": "cds.String",
+ "length": 1,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "OverallStatus_Text",
+ "@sap.label": "Overall Status",
+ "@sap.quickinfo": "Flight Reference Scenario: Travel Status",
+ "notNull": true
+ },
+ "OverallStatus_Text": {
+ "type": "cds.String",
+ "length": 1024,
+ "@sap.label": "Overall Status",
+ "@sap.quickinfo": "Overall Status Text",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ }
+ }
+ },
+ "rap_v2.Supplement": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.searchable": "true",
+ "@sap.content.version": "1",
+ "@sap.deletable.path": "Delete_mc",
+ "@sap.updatable.path": "Update_mc",
+ "@sap.label": "supplement view - cds data model",
+ "elements": {
+ "SupplementID": {
+ "key": true,
+ "type": "cds.String",
+ "length": 10,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Product ID",
+ "@sap.quickinfo": "Flight Reference Scenario: Supplement ID",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "notNull": true
+ },
+ "IsActiveEntity": {
+ "key": true,
+ "type": "cds.Boolean",
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Is active",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "notNull": true
+ },
+ "Edit_ac": {
+ "type": "cds.Boolean",
+ "@sap.label": "Dyn. Action Control",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false"
+ },
+ "Delete_mc": {
+ "type": "cds.Boolean",
+ "@sap.label": "Dyn. Method Control",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false"
+ },
+ "Update_mc": {
+ "type": "cds.Boolean",
+ "@sap.label": "Dyn. Method Control",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false"
+ },
+ "to_SupplementText_oc": {
+ "type": "cds.Boolean",
+ "@sap.label": "Dynamic CbA-Control",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false"
+ },
+ "SupplementCategory": {
+ "type": "cds.String",
+ "length": 2,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Supplement Category",
+ "@sap.quickinfo": "Supplement Type",
+ "@sap.updatable": "false",
+ "notNull": true
+ },
+ "Price": {
+ "type": "cds.Decimal",
+ "precision": 16,
+ "scale": 3,
+ "@sap.unit": "CurrencyCode",
+ "@sap.variable.scale": "true",
+ "@sap.label": "Product Price",
+ "@sap.quickinfo": "Flight Reference Scenario: Supplement Price"
+ },
+ "CurrencyCode": {
+ "type": "cds.String",
+ "length": 5,
+ "@sap.label": "Currency Code",
+ "@sap.quickinfo": "Flight Reference Scenario: Currency Code",
+ "@sap.semantics": "currency-code"
+ },
+ "LocalCreatedBy": {
+ "type": "cds.String",
+ "length": 12,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Created By",
+ "@sap.quickinfo": "Created By User"
+ },
+ "LocalCreatedAt": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.label": "Created On",
+ "@sap.quickinfo": "Creation Date Time"
+ },
+ "LocalLastChangedBy": {
+ "type": "cds.String",
+ "length": 12,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Changed By",
+ "@sap.quickinfo": "Local Instance Last Changed By User"
+ },
+ "LocalLastChangedAt": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.label": "Changed On",
+ "@sap.quickinfo": "Local Instance Last Change Date Time"
+ },
+ "LastChangedAt": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.label": "Changed On",
+ "@sap.quickinfo": "Last Change Date Time"
+ },
+ "HasDraftEntity": {
+ "type": "cds.Boolean",
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Has Draft",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "DraftEntityCreationDateTime": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.label": "Draft Created On",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "DraftEntityLastChangeDateTime": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.label": "Draft Last Changed On",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "HasActiveEntity": {
+ "type": "cds.Boolean",
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Has active",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "DraftAdministrativeData": {
+ "type": "cds.Association",
+ "target": "rap_v2.I_DraftAdministrativeData",
+ "keys": []
+ },
+ "SiblingEntity": {
+ "type": "cds.Association",
+ "target": "rap_v2.Supplement",
+ "keys": []
+ },
+ "to_Currency": {
+ "type": "cds.Association",
+ "target": "rap_v2.Currency",
+ "keys": []
+ },
+ "to_SupplementText": {
+ "type": "cds.Composition",
+ "target": "rap_v2.SupplementText",
+ "cardinality": {
+ "max": "*"
+ },
+ "on": [{"ref":["SupplementID"]},"=",{"ref":["to_SupplementText","SupplementID"]}]
+ }
+ },
+ "actions": {
+ "SupplementActivate": {
+ "kind": "action",
+ "returns": {
+ "type": "rap_v2.Supplement"
+ }
+ },
+ "SupplementDiscard": {
+ "kind": "action",
+ "returns": {
+ "type": "rap_v2.DummyFunctionImportResult"
+ }
+ },
+ "SupplementEdit": {
+ "kind": "action",
+ "params": {
+ "PreserveChanges": {
+ "type": "cds.Boolean",
+ "@sap.label": "TRUE"
+ }
+ },
+ "returns": {
+ "type": "rap_v2.Supplement"
+ }
+ },
+ "SupplementPrepare": {
+ "kind": "action",
+ "returns": {
+ "type": "rap_v2.DummyFunctionImportResult"
+ }
+ },
+ "SupplementResume": {
+ "kind": "action",
+ "returns": {
+ "type": "rap_v2.DummyFunctionImportResult"
+ }
+ }
+ }
+ },
+ "rap_v2.SupplementText": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.searchable": "true",
+ "@sap.content.version": "1",
+ "@sap.deletable.path": "Delete_mc",
+ "@sap.updatable.path": "Update_mc",
+ "@sap.label": "Supplement Text View - CDS Data Model",
+ "elements": {
+ "SupplementID": {
+ "key": true,
+ "type": "cds.String",
+ "length": 10,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "Description",
+ "@sap.label": "Product ID",
+ "@sap.quickinfo": "Flight Reference Scenario: Supplement ID",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "notNull": true
+ },
+ "LanguageCode": {
+ "key": true,
+ "type": "cds.String",
+ "length": 2,
+ "@sap.label": "Language Key",
+ "@sap.updatable": "false",
+ "notNull": true
+ },
+ "IsActiveEntity": {
+ "key": true,
+ "type": "cds.Boolean",
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Is active",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "notNull": true
+ },
+ "Delete_mc": {
+ "type": "cds.Boolean",
+ "@sap.label": "Dyn. Method Control",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false"
+ },
+ "Update_mc": {
+ "type": "cds.Boolean",
+ "@sap.label": "Dyn. Method Control",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false"
+ },
+ "Description": {
+ "type": "cds.String",
+ "length": 1024,
+ "@sap.label": "Product Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Supplement Description"
+ },
+ "LocalLastChangedAt": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.label": "Changed On",
+ "@sap.quickinfo": "Local Instance Last Change Date Time"
+ },
+ "HasDraftEntity": {
+ "type": "cds.Boolean",
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Has Draft",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "DraftEntityCreationDateTime": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.label": "Draft Created On",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "DraftEntityLastChangeDateTime": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.label": "Draft Last Changed On",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "HasActiveEntity": {
+ "type": "cds.Boolean",
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Has active",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "to_Supplement": {
+ "type": "cds.Association",
+ "target": "rap_v2.Supplement",
+ "keys": []
+ },
+ "DraftAdministrativeData": {
+ "type": "cds.Association",
+ "target": "rap_v2.I_DraftAdministrativeData",
+ "keys": []
+ },
+ "SiblingEntity": {
+ "type": "cds.Association",
+ "target": "rap_v2.SupplementText",
+ "on": [{"ref":["SupplementID"]},"=",{"ref":["SiblingEntity","SupplementID"]}]
+ }
+ }
+ },
+ "rap_v2.xDMOxI_Supplement_StdVH": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.searchable": "true",
+ "@sap.content.version": "1",
+ "@sap.label": "Supplement ValueHelp",
+ "@sap.value.list": "true",
+ "elements": {
+ "SupplementID": {
+ "key": true,
+ "type": "cds.String",
+ "length": 10,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "SupplementText",
+ "@sap.label": "Product ID",
+ "@sap.quickinfo": "Flight Reference Scenario: Supplement ID",
+ "notNull": true
+ },
+ "SupplementText": {
+ "type": "cds.String",
+ "length": 1024,
+ "@sap.label": "Product Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Supplement Description"
+ },
+ "SupplementCategory": {
+ "type": "cds.String",
+ "length": 2,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "SupplementCategoryText",
+ "@sap.label": "Supplement Category",
+ "@sap.quickinfo": "Supplement Type"
+ },
+ "SupplementCategoryText": {
+ "type": "cds.String",
+ "length": 1024,
+ "@sap.label": "Product Name",
+ "@sap.quickinfo": "Flight Reference Scenario: Supplement Description"
+ },
+ "Price": {
+ "type": "cds.Decimal",
+ "precision": 16,
+ "scale": 3,
+ "@sap.unit": "CurrencyCode",
+ "@sap.variable.scale": "true",
+ "@sap.label": "Product Price",
+ "@sap.quickinfo": "Flight Reference Scenario: Supplement Price"
+ },
+ "CurrencyCode": {
+ "type": "cds.String",
+ "length": 5,
+ "@sap.label": "Currency Code",
+ "@sap.quickinfo": "Flight Reference Scenario: Currency Code",
+ "@sap.semantics": "currency-code"
+ }
+ }
+ },
+ "rap_v2.Country": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.searchable": "true",
+ "@sap.content.version": "1",
+ "@sap.label": "Country/Region",
+ "elements": {
+ "Country": {
+ "key": true,
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "Country_Text",
+ "@sap.label": "Country/Region Key",
+ "notNull": true
+ },
+ "Country_Text": {
+ "type": "cds.String",
+ "length": 50,
+ "@sap.label": "Country/Region Name",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "CountryThreeLetterISOCode": {
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "ISO Code 3 Char",
+ "@sap.quickinfo": "ISO Country/Region Code 3 Characters"
+ },
+ "CountryThreeDigitISOCode": {
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "NonNegative",
+ "@sap.label": "ISO Code Num. 3",
+ "@sap.quickinfo": "ISO Country/Region Code Numeric 3-Characters"
+ },
+ "CountryISOCode": {
+ "type": "cds.String",
+ "length": 2,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "ISO Code",
+ "@sap.quickinfo": "ISO Code of the Country/Region"
+ },
+ "IsEuropeanUnionMember": {
+ "type": "cds.Boolean",
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "EU Member",
+ "@sap.quickinfo": "Indicator: European Union Member?"
+ }
+ }
+ },
+ "rap_v2.Currency": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.searchable": "true",
+ "@sap.content.version": "1",
+ "@sap.label": "Currency",
+ "elements": {
+ "Currency": {
+ "key": true,
+ "type": "cds.String",
+ "length": 5,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "Currency_Text",
+ "@sap.label": "Currency",
+ "@sap.quickinfo": "Currency Key",
+ "@sap.semantics": "currency-code",
+ "notNull": true
+ },
+ "Currency_Text": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "Description",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ },
+ "Decimals": {
+ "type": "cds.Integer",
+ "@odata.Type": "Edm.Byte",
+ "@sap.label": "Decimal Places",
+ "@sap.quickinfo": "Number of decimal places"
+ },
+ "CurrencyISOCode": {
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "ISO Code",
+ "@sap.quickinfo": "ISO Currency Code"
+ },
+ "AlternativeCurrencyKey": {
+ "type": "cds.String",
+ "length": 3,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Alternative Key"
+ },
+ "IsPrimaryCurrencyForISOCrcy": {
+ "type": "cds.Boolean",
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Primary",
+ "@sap.quickinfo": "Primary SAP Currency Code for ISO Code"
+ }
+ }
+ },
+ "rap_v2.I_CurrencyStdVH": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.searchable": "true",
+ "@sap.content.version": "1",
+ "@sap.label": "Currency",
+ "@sap.value.list": "true",
+ "elements": {
+ "Currency": {
+ "key": true,
+ "type": "cds.String",
+ "length": 5,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "Currency_Text",
+ "@sap.label": "Currency",
+ "@sap.quickinfo": "Currency Key",
+ "@sap.semantics": "currency-code",
+ "notNull": true
+ },
+ "Currency_Text": {
+ "type": "cds.String",
+ "length": 40,
+ "@sap.label": "Description",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false"
+ }
+ }
+ },
+ "rap_v2.I_DraftAdministrativeData": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.searchable": "true",
+ "@sap.addressable": "false",
+ "@sap.content.version": "1",
+ "@sap.label": "Draft Administration Data",
+ "elements": {
+ "DraftUUID": {
+ "key": true,
+ "type": "cds.UUID",
+ "@sap.label": "Draft (Technical ID)",
+ "notNull": true
+ },
+ "DraftEntityType": {
+ "key": true,
+ "type": "cds.String",
+ "length": 30,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Draft Entity ID",
+ "notNull": true
+ },
+ "CreationDateTime": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.filter.restriction": "interval",
+ "@sap.label": "Draft Created On"
+ },
+ "CreatedByUser": {
+ "type": "cds.String",
+ "length": 12,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "CreatedByUserDescription",
+ "@sap.label": "Draft Created By"
+ },
+ "LastChangeDateTime": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.filter.restriction": "interval",
+ "@sap.label": "Draft Last Changed On"
+ },
+ "LastChangedByUser": {
+ "type": "cds.String",
+ "length": 12,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "LastChangedByUserDescription",
+ "@sap.label": "Draft Last Changed By"
+ },
+ "DraftAccessType": {
+ "type": "cds.String",
+ "length": 1,
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Draft Access Type"
+ },
+ "ProcessingStartDateTime": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.label": "Draft In Process Since"
+ },
+ "InProcessByUser": {
+ "type": "cds.String",
+ "length": 12,
+ "@sap.display.format": "UpperCase",
+ "@sap.text": "InProcessByUserDescription",
+ "@sap.label": "Draft In Process By"
+ },
+ "DraftIsKeptByUser": {
+ "type": "cds.Boolean",
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Draft Is Kept By User"
+ },
+ "EnqueueStartDateTime": {
+ "type": "cds.Timestamp",
+ "@odata.Type": "Edm.DateTimeOffset",
+ "@odata.Precision": 7,
+ "@sap.label": "Draft Locked Since"
+ },
+ "DraftIsCreatedByMe": {
+ "type": "cds.Boolean",
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Draft Created By Me"
+ },
+ "DraftIsLastChangedByMe": {
+ "type": "cds.Boolean",
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Draft Last Changed By Me"
+ },
+ "DraftIsProcessedByMe": {
+ "type": "cds.Boolean",
+ "@sap.display.format": "UpperCase",
+ "@sap.label": "Draft In Process By Me"
+ },
+ "CreatedByUserDescription": {
+ "type": "cds.String",
+ "length": 80,
+ "@sap.label": "Draft Created By (Description)"
+ },
+ "LastChangedByUserDescription": {
+ "type": "cds.String",
+ "length": 80,
+ "@sap.label": "Draft Last Changed By (Description)"
+ },
+ "InProcessByUserDescription": {
+ "type": "cds.String",
+ "length": 80,
+ "@sap.label": "Draft In Process By (Description)"
+ }
+ }
+ },
+ "rap_v2.SAP__Currencies": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.content.version": "1",
+ "elements": {
+ "CurrencyCode": {
+ "key": true,
+ "type": "cds.String",
+ "length": 5,
+ "@sap.label": "Currency",
+ "@sap.semantics": "currency-code",
+ "notNull": true
+ },
+ "ISOCode": {
+ "type": "cds.String",
+ "length": 3,
+ "@sap.label": "ISO code",
+ "notNull": true
+ },
+ "Text": {
+ "type": "cds.String",
+ "length": 15,
+ "@sap.label": "Short text",
+ "notNull": true
+ },
+ "DecimalPlaces": {
+ "type": "cds.Integer",
+ "@odata.Type": "Edm.Byte",
+ "@sap.label": "Decimals",
+ "notNull": true
+ }
+ }
+ },
+ "rap_v2.SAP__UnitsOfMeasure": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.content.version": "1",
+ "elements": {
+ "UnitCode": {
+ "key": true,
+ "type": "cds.String",
+ "length": 3,
+ "@sap.label": "Internal UoM",
+ "@sap.semantics": "unit-of-measure",
+ "notNull": true
+ },
+ "ISOCode": {
+ "type": "cds.String",
+ "length": 3,
+ "@sap.label": "ISO Code",
+ "notNull": true
+ },
+ "ExternalCode": {
+ "type": "cds.String",
+ "length": 3,
+ "@sap.label": "Commercial",
+ "notNull": true
+ },
+ "Text": {
+ "type": "cds.String",
+ "length": 30,
+ "@sap.label": "Measurement Unit Txt",
+ "notNull": true
+ },
+ "DecimalPlaces": {
+ "type": "cds.Integer",
+ "@sap.label": "Decimal Places"
+ }
+ }
+ },
+ "rap_v2.SAP__MyDocumentDescriptions": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.content.version": "1",
+ "elements": {
+ "Id": {
+ "key": true,
+ "type": "cds.UUID",
+ "@sap.label": "UUID",
+ "notNull": true
+ },
+ "CreatedBy": {
+ "type": "cds.String",
+ "length": 12,
+ "notNull": true
+ },
+ "CreatedAt": {
+ "type": "cds.DateTime",
+ "@odata.Type": "Edm.DateTime",
+ "@sap.label": "Time Stamp",
+ "notNull": true
+ },
+ "FileName": {
+ "type": "cds.String",
+ "length": 256,
+ "notNull": true
+ },
+ "Title": {
+ "type": "cds.String",
+ "length": 256,
+ "notNull": true
+ },
+ "Format": {
+ "type": "cds.Association",
+ "target": "rap_v2.SAP__FormatSet",
+ "keys": []
+ },
+ "TableColumns": {
+ "type": "cds.Association",
+ "target": "rap_v2.SAP__TableColumnsSet",
+ "cardinality": {
+ "max": "*"
+ },
+ "keys": []
+ },
+ "CoverPage": {
+ "type": "cds.Association",
+ "target": "rap_v2.SAP__CoverPageSet",
+ "cardinality": {
+ "max": "*"
+ },
+ "keys": []
+ },
+ "Signature": {
+ "type": "cds.Association",
+ "target": "rap_v2.SAP__SignatureSet",
+ "keys": []
+ },
+ "PDFStandard": {
+ "type": "cds.Association",
+ "target": "rap_v2.SAP__PDFStandardSet",
+ "keys": []
+ }
+ }
+ },
+ "rap_v2.SAP__FormatSet": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.pageable": "false",
+ "@sap.addressable": "false",
+ "@sap.content.version": "1",
+ "elements": {
+ "Id": {
+ "key": true,
+ "type": "cds.UUID",
+ "@sap.label": "UUID",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ },
+ "FitToPage": {
+ "type": "rap_v2.SAP__FitToPage",
+ "notNull": true
+ },
+ "FontSize": {
+ "type": "cds.Integer",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ },
+ "Orientation": {
+ "type": "cds.String",
+ "length": 10,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ },
+ "PaperSize": {
+ "type": "cds.String",
+ "length": 10,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ },
+ "BorderSize": {
+ "type": "cds.Integer",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ },
+ "MarginSize": {
+ "type": "cds.Integer",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ },
+ "FontName": {
+ "type": "cds.String",
+ "length": 255,
+ "@sap.label": "Font Name",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ }
+ }
+ },
+ "rap_v2.SAP__PDFStandardSet": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.pageable": "false",
+ "@sap.addressable": "false",
+ "@sap.content.version": "1",
+ "elements": {
+ "Id": {
+ "key": true,
+ "type": "cds.UUID",
+ "@sap.label": "UUID",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ },
+ "UsePDFAConformance": {
+ "type": "cds.Boolean",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ },
+ "DoEnableAccessibility": {
+ "type": "cds.Boolean",
+ "@sap.label": "Indicator",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ }
+ }
+ },
+ "rap_v2.SAP__TableColumnsSet": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.pageable": "false",
+ "@sap.addressable": "false",
+ "@sap.content.version": "1",
+ "elements": {
+ "Id": {
+ "key": true,
+ "type": "cds.UUID",
+ "@sap.label": "UUID",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ },
+ "Name": {
+ "key": true,
+ "type": "cds.String",
+ "length": 256,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ },
+ "Header": {
+ "key": true,
+ "type": "cds.String",
+ "length": 256,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ },
+ "HorizontalAlignment": {
+ "type": "cds.String",
+ "length": 10,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ }
+ }
+ },
+ "rap_v2.SAP__CoverPageSet": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.pageable": "false",
+ "@sap.addressable": "false",
+ "@sap.content.version": "1",
+ "elements": {
+ "Title": {
+ "key": true,
+ "type": "cds.String",
+ "length": 256,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ },
+ "Id": {
+ "key": true,
+ "type": "cds.UUID",
+ "@sap.label": "UUID",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ },
+ "Name": {
+ "key": true,
+ "type": "cds.String",
+ "length": 256,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ },
+ "Value": {
+ "type": "cds.String",
+ "length": 256,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ }
+ }
+ },
+ "rap_v2.SAP__SignatureSet": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.deletable": "false",
+ "@sap.pageable": "false",
+ "@sap.addressable": "false",
+ "@sap.content.version": "1",
+ "elements": {
+ "Id": {
+ "key": true,
+ "type": "cds.UUID",
+ "@sap.label": "UUID",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ },
+ "DoSign": {
+ "type": "cds.Boolean",
+ "@sap.label": "Indicator",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ },
+ "Reason": {
+ "type": "cds.String",
+ "length": 256,
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ }
+ }
+ },
+ "rap_v2.SAP__ValueHelpSet": {
+ "kind": "entity",
+ "@cds.external": true,
+ "@cds.persistence.skip": false,
+ "@sap.content.version": "1",
+ "elements": {
+ "VALUEHELP": {
+ "key": true,
+ "type": "cds.String",
+ "length": 5000,
+ "notNull": true
+ },
+ "FIELD_VALUE": {
+ "type": "cds.String",
+ "length": 10,
+ "notNull": true
+ },
+ "DESCRIPTION": {
+ "type": "cds.LargeString"
+ }
+ }
+ },
+ "rap_v2.DummyFunctionImportResult": {
+ "kind": "type",
+ "@cds.external": true,
+ "elements": {
+ "IsInvalid": {
+ "type": "cds.Boolean",
+ "@sap.label": "TRUE"
+ }
+ }
+ },
+ "rap_v2.SAP__FitToPage": {
+ "kind": "type",
+ "@cds.external": true,
+ "elements": {
+ "ErrorRecoveryBehavior": {
+ "type": "cds.String",
+ "length": 8,
+ "@sap.label": "Error behavior",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ },
+ "IsEnabled": {
+ "type": "cds.Boolean",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ },
+ "MinimumFontSize": {
+ "type": "cds.Integer",
+ "@sap.creatable": "false",
+ "@sap.updatable": "false",
+ "@sap.sortable": "false",
+ "@sap.filterable": "false",
+ "notNull": true
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.plugins/abap/srv/external/rap-v2.edmx b/.plugins/abap/srv/external/rap-v2.edmx
new file mode 100644
index 0000000..7b30216
--- /dev/null
+++ b/.plugins/abap/srv/external/rap-v2.edmx
@@ -0,0 +1,3038 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Edit_ac
+ Delete_mc
+ Update_mc
+ to_SupplementText_oc
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SupplementCategory
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ LocalLastChangedAt
+
+
+
+
+
+
+ Edit_ac
+ Delete_mc
+ Update_mc
+ to_SupplementText_oc
+
+
+
+
+
+
+
+
+
+
+
+ cds_xdmoxui_travel_a_d.cds_xdmoxui_travel_a_d_Entities/Supplement
+
+
+
+
+
+
+ Delete_mc
+ Update_mc
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ LanguageCode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ LocalLastChangedAt
+
+
+
+
+
+
+ Delete_mc
+ Update_mc
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Delete_mc
+ Update_mc
+ to_BookingSupplement_oc
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ LocalLastChangedAt
+
+
+
+
+
+
+ Delete_mc
+ Update_mc
+ to_BookingSupplement_oc
+
+
+
+
+
+
+
+
+
+
+ Delete_mc
+ Update_mc
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ LocalLastChangedAt
+
+
+
+
+
+
+ Delete_mc
+ Update_mc
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ EMailAddress
+
+
+
+
+
+
+
+
+
+
+ acceptTravel_ac
+ deductDiscount_ac
+ rejectTravel_ac
+ BookingFee_fc
+ Delete_mc
+ Update_mc
+ to_Booking_oc
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ LocalLastChangedAt
+
+
+
+
+
+
+ acceptTravel_ac
+ deductDiscount_ac
+ rejectTravel_ac
+ BookingFee_fc
+ Delete_mc
+ Update_mc
+ to_Booking_oc
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ EMailAddress
+ WebAddress
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ EMailAddress
+ WebAddress
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ EMailAddress
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SupplementID
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.plugins/fed-db/README.md b/.plugins/fed-db/README.md
new file mode 100644
index 0000000..d73b8e9
--- /dev/null
+++ b/.plugins/fed-db/README.md
@@ -0,0 +1 @@
+# db-level-federation
diff --git a/.plugins/fed-db/cds-plugin.js b/.plugins/fed-db/cds-plugin.js
new file mode 100644
index 0000000..53fe975
--- /dev/null
+++ b/.plugins/fed-db/cds-plugin.js
@@ -0,0 +1,228 @@
+const cds = require("@sap/cds");
+
+const { isExternalEntity, time2Cron, time2Seconds } = require('./lib/utils.js');
+
+const log = cds.log("db-federation");
+
+const _hdi_migration = cds.compiler.to.hdi.migration;
+
+cds.compiler.to.hdi.migration = function (csn, options, beforeImage) {
+ const remoteEntities = [];
+ const remoteSourcesUsed = new Set();
+ const additions = [];
+ const grants = {};
+
+ // Add persistence.table and remove persistence.skip from external entities
+ for (let [name, def] of Object.entries(csn.definitions)) {
+ // Sync filenames for texts entities
+ if (name.endsWith('.texts') && def.includes.includes('sap.common.TextsAspect')) name = name.replace('.texts', '_texts')
+ if (!isExternalEntity(csn, def)) {
+ // REVISIT: find out when the skip annotation is added
+ delete def["@cds.persistence.skip"]
+ // prevent prototype chain for the table annotation
+ def["@cds.persistence.table"] = undefined;
+
+ // Create cache views for entities with ttl
+ if (def?.query && def['@cds.replicate.ttl'] && !Object.getPrototypeOf(def)['@cds.replicate.ttl']) {
+
+ const retention = time2Seconds(def['@cds.replicate.ttl']);
+ if (!retention) continue;
+
+ additions.push({
+ name,
+ sql: `RESULT CACHE "_SYS_CACHE#${entry.name.replace(/\./g, '_')}"
+ ON VIEW "${entry.name.replace(/\./g, '_')}" WITH RETENTION ${retention}`,
+ suffix: '.hdbresultcache'
+ })
+ }
+ // Skip non-external entities
+ continue;
+ }
+
+ delete def["@cds.persistence.skip"];
+ def["@cds.persistence.table"] = true;
+
+ const srvName = csn.services.find(srv => name.indexOf(srv.name) === 0)?.name;
+ remoteSourcesUsed.add(srvName);
+ const remote = cds.env.requires[srvName];
+ if (!remote || !(remote.credentials?.remote || remote.vcap)) {
+ // Mocking: Create .hdtable for remote tables
+ continue;
+ }
+
+ const creds = remote.credentials ?? {}
+
+ // Use quoted mode names when in quoted mode
+ const localQuoted = cds.env?.sql?.names === 'quoted'
+ const remoteQuoted = creds.sql?.names === 'quoted'
+ const removeService = creds.service === false
+
+ const database = creds.database || '';
+ const schema = creds.schema || '';
+ const reduceName = removeService ? name.replace(srvName + '.', '') : name
+ const remoteEntityName = remoteQuoted ? reduceName : reduceName.replace(/\./g, '_').toUpperCase()
+ const localEntityName = localQuoted ? `"${name}"` : name.replace(/\./g, '_').toUpperCase()
+
+ let srcEntityName = localEntityName;
+
+ if (remoteQuoted && !localQuoted) {
+ srcEntityName = `${localEntityName}_SRC`
+ additions.push({
+ name,
+ suffix: '.hdbview',
+ sql: `VIEW ${localEntityName} AS SELECT
+ ${Object.keys(def.elements)
+ .filter(e => !def.elements[e]?.virtual && !(def.elements[e]?.isAssociation && !def.elements[e].keys) && !def.elements[e]?.elements)
+ .map(e => {
+ if (def.elements[e]?.isAssociation) {
+ return def.elements[e].keys.map(k => {
+ e = `${e}_${k.ref[0]}`
+ return `"${e}" AS ${e}`
+ })
+ }
+ return `"${e}" AS ${e}`
+ })
+ .flat()
+ .join(',\n ')
+ }
+FROM ${srcEntityName}`,
+ })
+ }
+
+ // Use synonyms when no remote is specified
+ if (!creds.remote) {
+ const service = remote.vcap?.name || remote.vcap
+ additions.push({
+ name,
+ suffix: '.hdbsynonym',
+ sql: JSON.stringify({
+ [srcEntityName]: {
+ target: {
+ object: remoteEntityName,
+ schema: creds.schema,
+ }
+ }
+ }),
+ })
+
+ // additions.push({
+ // name: '../cfg/' + name, // .hdbsynonymconfig files don't work properly unless put inside the cfg dir "invalid xpath [8258506]"
+ // suffix: '.hdbsynonymconfig',
+ // sql: JSON.stringify({
+ // [remoteEntityName]: {
+ // target: {
+ // object: remoteEntityName,
+ // "*.configure": service,
+ // }
+ // }
+ // }),
+ // });
+
+ // Only OO grant role is required as it can grant SELECT to the application user
+ if (creds.type === 'procedure') { // Indicates procedure based granting object level privileges based
+ grants[service] ??= {
+ object_owner: {
+ roles: [],
+ container_roles: [],
+ object_privileges: [],
+ }
+ }
+ grants[service].object_owner.object_privileges.push({
+ type: 'VIEW',
+ name: remoteEntityName,
+ privileges_with_grant_option: [
+ "SELECT"
+ ]
+ })
+ } else { // Default is HDI binding granting which only allows roles
+ grants[service] ??= {
+ object_owner: { schema_roles: ['PUBLIC#'] },
+ // application_user: { schema_roles: ['PUBLIC'] }, // REVISIT: keep the application user role or rely on #OO grants ?
+ }
+ }
+ }
+ // Use hana virtual tables
+ else if (creds.remote) {
+ additions.push({
+ name,
+ suffix: '.hdbvirtualtable',
+ sql: `VIRTUAL TABLE ${srcEntityName} AT "${creds.remote}"."${database}"."${schema}"."${remoteEntityName}"`,
+ })
+
+ // Entity names provided in the role are case sensitive even when the definition is not quoted
+ remoteEntities.push(localQuoted ? name : localEntityName.toUpperCase());
+
+ if (typeof def['@cds.replicate.ttl'] === 'string') {
+ const time = time2Cron(def['@cds.replicate.ttl']);
+
+ if (!time) continue
+
+ // Create snapshot replica with manual refresh
+ additions.push({
+ name,
+ sql: `SHARED SNAPSHOT REPLICA ON ${srcEntityName}`,
+ suffix: '.hdbfabricvirtualtable'
+ });
+
+ // Create procedure and job schedular to refresh snapshot
+ const procedureName = localEntityName + '_procedure';
+ additions.push({
+ name,
+ sql:
+ `PROCEDURE ${procedureName}
+LANGUAGE SQLSCRIPT
+SQL SECURITY DEFINER
+AUTOCOMMIT DDL ON
+AS BEGIN
+ EXEC 'ALTER VIRTUAL TABLE ${srcEntityName} REFRESH SNAPSHOT REPLICA ASYNC';
+END`,
+ suffix: '.hdbprocedure'
+ });
+
+ additions.push({
+ name,
+ sql: `SCHEDULER JOB ${remoteEntityName + 'SCHEDULAR_JOB'} CRON '${time}' ENABLE PROCEDURE ${procedureName}`,
+ suffix: '.hdbschedulerjob'
+ });
+ } else if (creds.supportRealTimeCDC && def['@cds.replicate'] && !def.query) {
+ additions.push({
+ name,
+ sql: `SHARED REPLICA ON ${srcEntityName}`,
+ suffix: '.hdbfabricvirtualtable'
+ });
+ continue;
+ }
+ }
+ }
+
+ const ret = _hdi_migration(csn, options, beforeImage);
+
+ // create roles for synonyms
+ const appRole = {
+ name: 'application',
+ suffix: '.hdbrole',
+ sql: {
+ role: {
+ name: 'application',
+ }
+ }
+ }
+
+ if (Object.keys(grants).length) {
+ additions.push({
+ name: '',
+ suffix: '.hdbgrants',
+ sql: JSON.stringify(grants, null, 2)
+ })
+ }
+
+ if (remoteEntities.length) {
+ appRole.sql.role.object_privileges = remoteEntities.map(name => ({ name, type: "TABLE", privileges: ["SELECT"] }))
+ }
+
+ appRole.sql = JSON.stringify(appRole.sql, null, 2)
+ additions.push(appRole)
+
+ ret.definitions = [...ret.definitions.filter(e => !additions.some(a => a.name === e.name)), ...additions];
+ return ret;
+}
diff --git a/.plugins/fed-db/lib/utils.js b/.plugins/fed-db/lib/utils.js
new file mode 100644
index 0000000..07ddd9a
--- /dev/null
+++ b/.plugins/fed-db/lib/utils.js
@@ -0,0 +1,80 @@
+function isExternalEntity(model, entry) {
+ const remoteServices = Object.keys(model.definitions)
+ .filter(name => model.definitions[name]?.kind === 'service' && model.definitions[name]['@cds.external'])
+ return entry && entry.kind === "entity" &&
+ remoteServices.some(srv => entry.name.indexOf(srv) === 0)
+}
+
+const hanaCronRegex = new RegExp('^' + new Array(7).fill(`(\\d+|\\*)`).join(' +') + '$');
+/**
+ * ::= { | | | | | | }
+ * @param {*} time
+ * @returns {string | null}
+ */
+function time2Cron(time) {
+ time = time.trim();
+ if (hanaCronRegex.test(time)) {
+ return time;
+ }
+
+ const match = time.match(/^([0-9]+)(w|d|h|hrs|m|min|s)$/);
+ if (!match) return;
+
+ let cronParts = ['*', '*', '*', '*', '*', '*', '*'];
+
+ const [, val, t] = match
+ switch (t) {
+ case 'w':
+ cronParts[2] = `*/${val * 7}`;
+ cronParts[3] = '0';
+ cronParts[4] = '0';
+ cronParts[5] = '0';
+ cronParts[6] = '0';
+ break;
+ case 'd':
+ cronParts[2] = `*/${val}`;
+ cronParts[3] = '0';
+ cronParts[4] = '0';
+ cronParts[5] = '0';
+ cronParts[6] = '0';
+ break;
+ case 'h':
+ case 'hrs':
+ cronParts[4] = `*/${val}`;
+ cronParts[5] = '0';
+ cronParts[6] = '0';
+ break;
+ case 'm':
+ case 'min':
+ cronParts[5] = `*/${val}`;
+ cronParts[6] = '0';
+ break;
+ case 's':
+ cronParts[6] = `*/${val}`;
+ break;
+ }
+
+ return cronParts.join(' ');
+}
+
+function time2Seconds(time) {
+ time = time.trim();
+ const match = time.match(/^([0-9]+)(w|d|h|hrs|min|s)$/);
+ if (!match) return;
+
+ const [, val, t] = match
+ switch (t) {
+ case 'w': return val * 7 * 24 * 60 * 60;
+ case 'd': return val * 24 * 60 * 60;
+ case 'h':
+ case 'hrs': return val * 60 * 60;
+ case 'min': return val * 60;
+ case 's': return val;
+ }
+}
+
+module.exports = {
+ isExternalEntity,
+ time2Cron,
+ time2Seconds
+};
diff --git a/.plugins/fed-db/package.json b/.plugins/fed-db/package.json
new file mode 100644
index 0000000..7bb5da1
--- /dev/null
+++ b/.plugins/fed-db/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@cap-js/database-federation",
+ "version": "0.0.1",
+ "main": "./cds-plugin.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "peerDependencies": {
+ "@sap/cds": ">=9"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "description": ""
+}
\ No newline at end of file
diff --git a/.plugins/fed-srv/README.md b/.plugins/fed-srv/README.md
new file mode 100644
index 0000000..f8352cd
--- /dev/null
+++ b/.plugins/fed-srv/README.md
@@ -0,0 +1 @@
+# srv-level-federation
diff --git a/.plugins/fed-srv/cds-plugin.js b/.plugins/fed-srv/cds-plugin.js
new file mode 100644
index 0000000..0cf10bd
--- /dev/null
+++ b/.plugins/fed-srv/cds-plugin.js
@@ -0,0 +1,184 @@
+const cds = require('@sap/cds');
+const log = cds.log('srv-federation');
+
+const registerHandler = require('./lib/registerEventHandlers');
+const utils = require('./lib/utils');
+
+let replicationSetups = {};
+
+cds.on('loaded', model => {
+ const remoteServices = utils.getAllRemoteServices(model);
+
+ for (const name in model.definitions) {
+ const def = model.definitions[name];
+
+ if (def.kind === 'entity') {
+ const target = def.projection?.from?.ref?.[0];
+ if (!target) continue;
+
+ const isTargetRemote = remoteServices.some(srv => target.indexOf(srv) === 0);
+ if (isTargetRemote) def['@cds.persistence.table'] = true;
+ }
+ }
+});
+
+cds.on('serving', srv => {
+ const model = cds.model.definitions;
+ const remoteServices = utils.getAllRemoteServices(cds.model);
+ if (remoteServices.length === 0) return;
+
+ // Strategy collections
+ const conditionalRead = [];
+ const readDelegationMap = new Map();
+ const expandMap = new Map();
+ const navigationTargets = new Set();
+ const initialLoadEntities = [];
+ const replicationPairs = [];
+
+ // Process all entities and associations
+ for (const entity of srv.entities) {
+ handleEntity(entity);
+ handleAssociations(entity);
+ }
+ registerHandlers();
+
+ function handleEntity(entity) {
+ if (!utils.isTargetRemote(entity)) return;
+
+ const targetEntity = utils.getRemoteTargetEntity(entity);
+ const remoteSrvName = utils.getServiceNameFromEntity(targetEntity);
+
+ if (!remoteServices.includes(remoteSrvName)) return;
+
+ // check inital load flag
+ const creds = cds.requires[remoteSrvName]?.credentials || {};
+ const initialLoadFlag = creds.loadOnDemand !== true;
+
+ if (entity['@cds.replicate'] || entity['@cds.replicate.ttl']) {
+ if (initialLoadFlag) {
+ initialLoadEntities.push({ remoteSrvName, entity: entity.name });
+ } else {
+ conditionalRead.push(entity.name);
+ utils.addToMap(readDelegationMap, remoteSrvName, entity.name);
+ }
+ } else {
+ utils.addToMap(readDelegationMap, remoteSrvName, entity.name);
+ }
+ }
+
+ // REVISIT: Traverse nested associations
+ function handleAssociations(entity) {
+ if (!entity.associations) return;
+
+ for (const assoc of entity.associations) {
+ const assocTarget = model[assoc.target];
+ if (!assocTarget) continue;
+
+ const isEntityRemote = utils.isTargetRemote(entity);
+ const isTargetRemote = utils.isTargetRemote(assocTarget);
+
+ if (!isEntityRemote && isTargetRemote) {
+ const remoteTargetEntity = utils.getRemoteTargetEntity(assocTarget);
+ const remoteSrvName = utils.getServiceNameFromEntity(remoteTargetEntity);
+
+ if (!remoteServices.includes(remoteSrvName)) continue;
+
+ if (assocTarget['@cds.replicate'] || entity['@cds.replicate.ttl']) {
+ replicationPairs.push({ remoteSrvName, localEntity: entity.name, remoteEntity: assocTarget.name });
+ initialLoadEntities.push({ remoteSrvName, entity: assocTarget.name });
+ } else {
+ utils.addToMap(readDelegationMap, remoteSrvName, assocTarget.name);
+ }
+ addExpandNavigation(entity.name, assocTarget.name);
+ } else if (isEntityRemote && !isTargetRemote) {
+ addExpandNavigation(entity.name, assocTarget.name);
+ }
+ }
+ }
+
+ function addExpandNavigation(entity, target) {
+ if (!expandMap.has(entity)) expandMap.set(entity, new Set());
+ expandMap.get(entity).add(target);
+ navigationTargets.add(target);
+ }
+
+ function registerHandlers() {
+ // Register readDelegation handlers for external entities
+ for (const [remoteSrvName, entities] of readDelegationMap.entries()) {
+ registerHandler.readDelegation(srv, remoteSrvName, Array.from(entities));
+ }
+
+ // Register conditionalReadDelegation for external entities (on-demand replication)
+ if (conditionalRead.length > 0) {
+ registerHandler.conditionalReadDelegation(srv, conditionalRead);
+ }
+
+ // Register initial load event listeners (initial replication)
+ replicationSetups = {};
+ for (const { remoteSrvName, entity } of initialLoadEntities) {
+ if (!replicationSetups[remoteSrvName]) replicationSetups[remoteSrvName] = [];
+
+ replicationSetups[remoteSrvName].push(entity);
+ registerHandler.eventListenerForReplication(srv, remoteSrvName, entity);
+ }
+
+ // Register expand and navigation handlers
+ for (const [expandTarget, expandEntities] of expandMap.entries()) {
+ registerHandler.expand(srv, expandTarget, Array.from(expandEntities));
+ }
+ for (const navTarget of navigationTargets) {
+ registerHandler.navigation(srv, navTarget);
+ }
+
+ // Register demand replication and update replication on event
+ for (const { remoteSrvName, localEntity, remoteEntity } of replicationPairs) {
+ registerHandler.demandReplication(srv, remoteSrvName, localEntity, remoteEntity);
+
+ const remoteEvents = cds.services[remoteSrvName].events;
+ if (!remoteEvents) continue;
+
+ for (const event of remoteEvents) {
+ const targetEntityName = Object.getPrototypeOf(event).name;
+ if (utils.getRemoteDBTable(model[remoteEntity]) === targetEntityName) {
+ log.info(`Register handler for update replication Event: ${event}, Service: ${remoteSrvName}, Entity: ${remoteEntity}`);
+ registerHandler.updateReplicationOnEvent(remoteSrvName, event, remoteEntity);
+ }
+ }
+ }
+ }
+});
+
+cds.on('listening', async ({ server, url }) => {
+ for (const [remoteSrvName, entities] of Object.entries(replicationSetups)) {
+ // Find the application service that needs replication
+ const appService = Object.values(cds.services).find(srv =>
+ srv.name && entities.some(entity =>
+ Object.keys(srv.entities || {}).includes(entity.split('.').pop())
+ )
+ );
+
+ if (!appService) continue;
+
+ // scheduling for each replication entity
+ for (const entity of entities) {
+ const eventName = `replicate_${entity}`;
+ const entityDef = cds.model.definitions[entity];
+ const ttl = entityDef['@cds.replicate.ttl'];
+
+ const creds = cds.requires[remoteSrvName]?.credentials || {};
+ const initialLoadFlag = creds.loadOnDemand !== true;
+
+ try {
+ if (ttl) {
+ await appService.schedule(eventName).every(ttl);
+ log.info(`Scheduled recurring replication for ${entity} every ${ttl}`);
+ } else if (initialLoadFlag) {
+ await appService.schedule(eventName);
+ log.info(`Scheduled initial load for ${entity}`);
+ }
+ } catch (error) {
+ log.error(`Failed to schedule replication for ${entity}:`, error);
+ }
+ }
+ }
+});
\ No newline at end of file
diff --git a/.plugins/fed-srv/lib/registerEventHandlers.js b/.plugins/fed-srv/lib/registerEventHandlers.js
new file mode 100644
index 0000000..008ab62
--- /dev/null
+++ b/.plugins/fed-srv/lib/registerEventHandlers.js
@@ -0,0 +1,276 @@
+const cds = require('@sap/cds');
+const { mapAndFilterKeysToEntityModel, getProjectionFieldMapping, mapFlattenAssoc } = require('./utils');
+
+function readDelegation(service, remoteSrvName, entities) {
+ const remoteSrvProm = cds.connect.to(remoteSrvName);
+ // REVISIT: This prevents other event handlers from being registered
+ service.prepend(() => {
+ // REVISIT: Handle where clause in projection for remote service
+
+ // Read delegation to remote service
+ service.on('READ', entities, async req => (await remoteSrvProm).run(req.query));
+
+ // Flatten association fields in results
+ service.after('READ', entities, async (results, req) => {
+ const flatAssocMap = getProjectionFieldMapping(req.target.name);
+ if (!flatAssocMap) return;
+ for (let result of results) result = mapFlattenAssoc(result, flatAssocMap);
+ });
+ });
+}
+
+function conditionalReadDelegation(service, entities) {
+ const dbProm = cds.connect.to('db');
+
+ service.prepend(() => {
+ service.on('READ', entities, async (req, next) => {
+ // try to access local replica first
+ try {
+ const db = await dbProm;
+ const localResults = await db.run(req.query);
+
+ const shouldDelegate = (() => {
+ if (!localResults || localResults.length === 0) return true;
+
+ // Handle $count queries
+ if (localResults.length === 1 && localResults[0].$count === 0) return true;
+
+ return false;
+ })();
+
+ if (shouldDelegate) return next();
+ else return localResults;
+
+ } catch (error) {
+ cds.log('srv-fed').error(`Error accessing local replica for ${req.target.name}:`, error);
+ }
+
+ // If not found locally, delegate to remote
+ return next();
+ });
+ });
+}
+
+function expand(service, baseEntity, expandTargets) {
+ service.prepend(() => {
+ service.on('READ', baseEntity, async (req, next) => {
+ const select = req.query.SELECT;
+ if (!select.columns) return next();
+
+ const baseEntityDef = cds.model.definitions[baseEntity];
+ const expandableAssocs = Object.values(baseEntityDef.elements)
+ .filter(e => e.type === 'cds.Association' && expandTargets.includes(e.target))
+ .reduce((acc, e) => ({ ...acc, [e.name]: e }), {});
+
+ const expandOps = select.columns
+ .map((col, index) => ({ ...col, index }))
+ .filter(({ expand, ref }) => expand && expandableAssocs[ref?.[0]])
+ .reverse(); // necessary to maintain order of indices
+
+ if (expandOps.length === 0) return next();
+
+ let fKeys = [];
+ // remove all expands from query
+ for (const op of expandOps) {
+ req.query.SELECT.columns.splice(op.index, 1);
+ const assoc = expandableAssocs[op.ref[0]];
+ fKeys = assoc.keys
+ ? assoc.keys.map(key => key.$generatedFieldName)
+ : [assoc.on[2].ref[0]]; // unmanaged association
+
+ fKeys.forEach(fk => {
+ if (fk) ensureForeignKeySelected(req.query.SELECT, fk);
+ });
+ }
+
+ // execute the base query
+ const baseEntityResult = await next();
+ if (!baseEntityResult) return;
+
+ // Handle both single result and array of results
+ const results = Array.isArray(baseEntityResult) ? baseEntityResult : [baseEntityResult];
+
+ // process each expand operation
+ await Promise.all(expandOps.map(async (op) => {
+ const assoc = expandableAssocs[op.ref[0]];
+ const fKeys = assoc.keys
+ ? assoc.keys.map(key => key.$generatedFieldName)
+ : [assoc.on[2].ref[0]];
+ const targetKeys = assoc.keys ? fKeys.map(k => k.split('_').pop()) : assoc.on[0].ref[1];
+
+ const whereClause = results.map(r => {
+ const where = {};
+ fKeys.forEach((fKey, index) => {
+ const targetKey = targetKeys[index];
+ where[targetKey] = r[fKey];
+ });
+ return where;
+ });
+
+ const allExpandedData = await Promise.all(
+ whereClause.map(async (where) => {
+ return await service.run(
+ SELECT(op.expand).from(assoc.target).where(where)
+ );
+ })
+ );
+
+
+ results.forEach((result, index) => {
+ const expand = allExpandedData[index];
+ result[op.ref[0]] = isToOneAssociation(assoc) && expand.length === 1
+ ? expand[0]
+ : expand;
+ });
+ }));
+
+ return Array.isArray(results) ? results : results[0];
+ });
+ });
+}
+
+function ensureForeignKeySelected(selectQuery, foreignKey) {
+ const hasFK = selectQuery.columns.some(col =>
+ col.ref?.[0] === foreignKey ||
+ (col.ref?.[0] === '*')
+ );
+
+ if (!hasFK) {
+ selectQuery.columns.push({ ref: [foreignKey] });
+ }
+}
+
+function isToOneAssociation(association) {
+ return association.is2one && !association.is2many;
+}
+
+function navigation(service, navTarget) {
+ service.prepend(() => {
+ service.on('READ', navTarget, async (req, next) => {
+ const select = req.query.SELECT;
+ if (select.from.ref.length !== 2) return next();
+
+ const [baseEntityName, assocName] = req.path.split('/');
+ const baseEntityRef = req.subject.ref[0];
+
+ // get association
+ const baseEntityDef = cds.model.definitions[baseEntityName];
+ const assoc = baseEntityDef.elements[assocName];
+
+ if (!assoc || assoc.type !== 'cds.Association' || assoc.target !== navTarget) next();
+
+ // extract foreign and target keys
+ const foreignKey = assoc.keys ? assoc.keys[0].$generatedFieldName : assoc.on[2].ref[0];
+ const targetKey = assoc.keys ? foreignKey.split('_').pop() : assoc.on[0].ref[1];
+
+ // get foreign key value from base entity
+ const baseEntity = await service.run(
+ SELECT.one(foreignKey).from(baseEntityRef.id).where(baseEntityRef.where)
+ );
+
+ if (!baseEntity || baseEntity[foreignKey] == null) {
+ return [];
+ }
+
+ // query the target entity
+ const result = await service.run(
+ SELECT(select.columns).from(navTarget).where(targetKey, '=', baseEntity[foreignKey])
+ .limit(select.limit?.rows?.val, select.limit?.offset?.val));
+
+ return result;
+ })
+ });
+}
+
+function demandReplication(service, remoteSrvName, localEntity, assocTarget) {
+
+ service.prepend(() => {
+ const dbProm = cds.connect.to('db');
+ const remoteSrvProm = cds.connect.to(remoteSrvName);
+
+ service.on(['CREATE', 'UPDATE', 'UPSERT'], localEntity, async (req, next) => {
+ const db = await dbProm;
+ const remoteService = await remoteSrvProm;
+
+ const targetEntity = cds.model.definitions[assocTarget];
+
+ const elements = req.target.elements;
+ const associationName = Object.entries(elements)
+ .filter(([key, def]) => def.target === assocTarget)
+ .map(([key]) => key);
+
+ // req.target.elements[aName].foreignKeys
+ const keys = {};
+ for (const key in targetEntity.keys) {
+ keys[key] = req.data[associationName[0] + '_' + key];
+ }
+
+ let replica = await db.exists(targetEntity, keys);
+ if (!replica) {
+ replica = await remoteService.read(targetEntity, keys);
+ await UPSERT.into(targetEntity).entries(replica);
+ }
+ return next();
+ });
+ });
+}
+
+async function updateReplicationOnEvent(remoteSrvName, eventDef, entity) {
+ const dbProm = cds.connect.to('db');
+ const rs = await cds.connect.to(remoteSrvName);
+
+ let { name } = eventDef;
+ name = name.substring(name.indexOf('.') + 1);
+
+ rs.prepend(() => {
+ rs.on(name, async msg => {
+ const db = await dbProm;
+ // REVISIT: If msg just contains one value, then keys = msg.data;
+ let keys = {};
+ for (const key in eventDef.keys) {
+ keys[key] = msg.data[key];
+ }
+
+ keys = mapAndFilterKeysToEntityModel(remoteSrvName, entity, keys);
+ let replica = await db.exists(entity, keys);
+ if (replica) {
+ replica = await rs.read(entity, keys);
+ await UPDATE(entity).with(replica);
+ }
+ });
+ });
+}
+
+function eventListenerForReplication(service, remoteSrvName, entity) {
+ const eventName = `replicate_${entity}`;
+
+ service.on(eventName, async (msg) => {
+ try {
+ const db = await cds.connect.to('db');
+ const remoteService = await cds.connect.to(remoteSrvName);
+
+ // Fetch all data from remote
+ const remoteData = await remoteService.read(entity);
+
+ if (remoteData && remoteData.length > 0) {
+ // Clear existing data and insert new data
+ await db.run(DELETE.from(entity));
+ await db.run(INSERT.into(entity).entries(remoteData));
+
+ console.log(`Replicated ${remoteData.length} records for ${entity}`);
+ }
+ } catch (error) {
+ console.error(`Replication failed for ${entity}:`, error);
+ }
+ });
+}
+
+module.exports = {
+ readDelegation,
+ conditionalReadDelegation,
+ demandReplication,
+ updateReplicationOnEvent,
+ navigation,
+ expand,
+ eventListenerForReplication
+};
diff --git a/.plugins/fed-srv/lib/utils.js b/.plugins/fed-srv/lib/utils.js
new file mode 100644
index 0000000..cddffcb
--- /dev/null
+++ b/.plugins/fed-srv/lib/utils.js
@@ -0,0 +1,147 @@
+const cds = require('@sap/cds');
+
+// Function to get all external services
+function getAllRemoteServices(model) {
+ const services = Object.keys(model.definitions).filter(name => model.definitions[name]?.kind === 'service');
+ return services.filter(srv => {
+ const creds = cds.requires[srv]?.credentials
+ // RemoteService only functions if the url or destination credentials are provided
+ return model.definitions[srv]['@cds.external'] && (creds?.url || creds?.destination)
+ });
+}
+
+function searchRemoteQuery(target, srvNameToFind, previousQuery = null) {
+ const srvName = getServiceNameFromEntity(target);
+ if (srvName === srvNameToFind) {
+ return previousQuery;
+ }
+ if (target.query && target.query._target) {
+ return searchRemoteQuery(target.query._target, srvNameToFind, target.query);
+ } else {
+ return null;
+ }
+}
+
+function mapAndFilterKeysToEntityModel(remoteSrv, entityName, keys) {
+ const entity = cds.model.definitions[entityName];
+ const entityKeys = Object.getOwnPropertyNames(entity.keys);
+
+ const query = searchRemoteQuery(entity, remoteSrv);
+
+ for (const key in keys) {
+ const matchingCol = query.SELECT.columns.find(col => col.ref[0] === key);
+ if (matchingCol && entityKeys.includes(matchingCol.as)) {
+ const value = keys[key];
+ delete keys[key];
+ keys[matchingCol.as] = value;
+ } else {
+ delete keys[key]; // Delete the entry if it doesn't match any entity keys
+ }
+ }
+ return keys;
+}
+
+function getServiceNameFromEntity(entity) {
+ return entity['_service'] ? entity._service.name : null;
+}
+
+function searchRemoteTargetEntity(target) {
+ if (target.hasOwnProperty('@cds.external')) return target;
+ if (target.query && target.query._target) {
+ return searchRemoteTargetEntity(target.query._target, target.query);
+ } else {
+ return null;
+ }
+}
+
+function getRemoteTargetEntity(def) {
+ const targetDBTable = cds.db.resolve.table(def);
+ const remoteTargetEntity = !targetDBTable['@cds.external'] ? targetDBTable.query._target : searchRemoteTargetEntity(def.query._target);
+
+ return remoteTargetEntity;
+}
+
+function getRemoteDBTable(def) {
+ const targetDBTable = cds.ql.resolve.table(def);
+ if (targetDBTable.query && targetDBTable.query._target) {
+ return getRemoteDBTable(targetDBTable.query._target);
+ }
+ return targetDBTable.name;
+}
+
+function isTargetRemote(def) {
+ if (def.hasOwnProperty('@federated')) return true;
+ if (def.query && def.query._target) {
+ return isTargetRemote(def.query._target);
+ }
+ return false;
+}
+
+function isCrossBoundary(association, baseEntity) {
+ const baseEntityDef = cds.model.definitions[baseEntity];
+ const targetEntityDef = cds.model.definitions[association.target];
+
+ if (!targetEntityDef) return false;
+
+ const isBaseRemote = utils.isTargetRemote(baseEntityDef);
+ const isTargetRemote = utils.isTargetRemote(targetEntityDef);
+
+ return isBaseRemote !== isTargetRemote; // Cross-boundary if different
+}
+
+function addToMap(map, key, value) {
+ if (!map.has(key)) map.set(key, new Set());
+ map.get(key).add(value);
+}
+
+function getProjectionFieldMapping(target) {
+ const targetDef = cds.model.definitions[target];
+ if (!targetDef?.projection) return {};
+
+ const projection = targetDef.projection;
+ const projectionTraget = cds.model.definitions[projection.from.ref[0]];
+
+ if (projectionTraget?._service) {
+ return getProjectionFieldMapping(projectionTraget.name)
+ }
+
+ const projectionCols = projectionTraget.projection.columns;
+ const mapping = projectionCols.reduce((acc, col) => {
+ if (col === '*' || !col.ref || col.ref.length < 2) return acc;
+
+ acc[col.as] = col.ref.join('_');
+ return acc;
+ }, {});
+
+ return mapping;
+}
+
+function mapFlattenAssoc(data, map) {
+ if (Array.isArray(data)) {
+ return data.map(item => mapFlattenAssoc(item, map));
+ }
+
+ for (const [col, value] of Object.entries(data)) {
+ const targetField = Object.keys(map).find(target => map[target] === col);
+
+ if (targetField && targetField !== col) {
+ data[targetField] = value;
+ delete data[col]; // remove old field
+ }
+ }
+
+ return data;
+}
+
+
+module.exports = {
+ getAllRemoteServices,
+ mapAndFilterKeysToEntityModel,
+ getRemoteTargetEntity,
+ getServiceNameFromEntity,
+ isTargetRemote,
+ getRemoteDBTable,
+ addToMap,
+ getProjectionFieldMapping,
+ mapFlattenAssoc
+};
\ No newline at end of file
diff --git a/.plugins/fed-srv/package.json b/.plugins/fed-srv/package.json
new file mode 100644
index 0000000..df64a3e
--- /dev/null
+++ b/.plugins/fed-srv/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@cap-js/srv-federation",
+ "version": "0.0.1",
+ "main": "./cds-plugin.js",
+ "scripts": {
+ "test": "exit 1"
+ },
+ "peerDependencies": {
+ "@sap/cds": ">=9"
+ }
+}
\ No newline at end of file
diff --git a/.plugins/fed-xrv/lib/federation-service.js b/.plugins/fed-xrv/lib/federation-service.js
index 360c4e6..daa8b16 100644
--- a/.plugins/fed-xrv/lib/federation-service.js
+++ b/.plugins/fed-xrv/lib/federation-service.js
@@ -18,13 +18,18 @@ module.exports = class DataFederationService extends cds.Service {
if (!conf?.credentials) return // not bound to remote, skipping
entity['@cds.persistence.table'] = true
entity['@cds.persistence.skip'] = false
- return { entity: entity.name, from: srv.name }
+ const ttl = entity['@cds.replicate.ttl']
+ return { entity: entity.name, from: srv.name, ttl }
})
if (!todos.length) return
cds.once ('served', async () => {
const dfs = await cds.connect.to (DataFederationService)
- return Promise.all (todos.map (todo => dfs.replicate (todo)))
+ return Promise.all (todos.reduce ((acc, todo) => {
+ if (!todo.ttl) acc.push(dfs.replicate (todo))
+ else acc.push(dfs.periodicRefresh (todo))
+ return acc;
+ }, []))
})
})}
@@ -37,12 +42,24 @@ module.exports = class DataFederationService extends cds.Service {
await INSERT.into(entity).entries(rows)
log.info ('initially loaded:', { entity, from: remote, via: srv.kind })
})
+
+ this.on ('refresh', async req => {
+ const { entity, from: remote } = req.data
+ const srv = await cds.connect.to (remote)
+ const rows = await srv.read (entity)
+ await UPSERT.into(entity).entries(rows)
+ log.info ('periodically loaded:', { entity, from: remote, via: srv.kind })
+ })
}
replicate ({ entity, from: remote }) {
return this.schedule ('initial-load', { entity, from: remote })
.after ('11ms')
}
+
+ periodicRefresh ({ entity, from: remote, ttl }) {
+ return this.schedule ('refresh', { entity, from: remote }).after ('11ms').every (ttl);
+ }
}
diff --git a/db/abap/index.cds b/db/abap/index.cds
new file mode 100644
index 0000000..a8df835
--- /dev/null
+++ b/db/abap/index.cds
@@ -0,0 +1,8 @@
+using { sap.capire.travels.masterdata.federated.Supplements } from '../master-data';
+
+// It is not possible to define type association as a managed association
+// because the rap sflight model doesn't match expected default foreign key pattern
+// therefor it is required to explicitly expose the foreign key onto the projections
+extend projection Supplements with {
+ type_code
+};
diff --git a/db/rep-srv/index.cds b/db/rep-srv/index.cds
new file mode 100644
index 0000000..f9fd8ec
--- /dev/null
+++ b/db/rep-srv/index.cds
@@ -0,0 +1,4 @@
+using { sap.capire.flights.data as external } from '@capire/xflights-data';
+
+annotate external.Flights with @cds.replicate;
+annotate external.Supplements with @cds.replicate.ttl: '10s';
\ No newline at end of file
diff --git a/db/undeploy.json b/db/undeploy.json
new file mode 100644
index 0000000..7888cb5
--- /dev/null
+++ b/db/undeploy.json
@@ -0,0 +1,11 @@
+[
+ "**/*.hdbview",
+ "**/*.hdbvirtualtable",
+ "**/*.hdbfabricvirtualtable",
+ "**/*.hdbprocedure",
+ "**/*.hdbschedulerjob",
+ "**/*.hdbsynonym",
+ "**/*.hdbsynonymconfig",
+ "**/*.hdbrole",
+ "**/sap.capire.flights.data.*.hdbtable"
+]
\ No newline at end of file
diff --git a/package.json b/package.json
index 5de2c21..b679937 100644
--- a/package.json
+++ b/package.json
@@ -6,17 +6,34 @@
"lint": "npx eslint .",
"start": "cds-serve",
"test": "chest test",
+ "setup": "node ./test/hana.mjs || node ./test/cf.mjs || exit 0",
+ "remote": "cd $(node -e \"console.log(require('node:path').dirname(require.resolve('@capire/xflights/package.json')))\") && ( fuser 4005/tcp -k || true ) && cds w --port 4005",
+ "test:all": "npm run setup && { npm run remote & } && npm run test && npm run test:srv && npm run test:syn && npm run test:vrt && npm run test:ds && npm run test:rap",
+ "test:srv": "CDS_ENV=fed-srv npm t",
+ "test:srv-rep": "CDS_ENV=rep-srv npm t",
+ "test:vrt": "echo 'HDI deploy' && npm run deploy:vrt >/dev/null && cds bind --exec --profile fed-vrt -- npm t",
+ "test:syn": "echo 'HDI deploy' && npm run deploy:syn >/dev/null && cds bind --exec --profile fed-syn -- npm t",
+ "test:ds": "echo 'HDI deploy' && npm run deploy:ds >/dev/null && cds bind --exec --profile fed-ds -- npm t",
+ "test:rap": "echo 'HDI deploy' && npm run deploy:rap >/dev/null && cds bind --exec --profile fed-rap -- npm t",
+ "deploy:vrt": "cds bind --exec --profile fed-vrt -- cds deploy -2 hana",
+ "deploy:syn": "cds bind --exec --profile fed-syn -- cds deploy -2 hana",
+ "deploy:ds": "cds bind --exec --profile fed-ds -- cds deploy -2 hana",
+ "deploy:rap": "cds bind --exec --profile fed-rap -- cds deploy -2 hana",
"jest": "npx jest"
},
"dependencies": {
"@capire/common": "*",
"@capire/xflights-data": "*",
+ "@capire/xflights-abap-api": ".plugins/abap",
+ "@cap-js/database-federation": ".plugins/fed-db",
+ "@cap-js/hana": "^2.1.2",
"@cap-js/xsrv-federation": ".plugins/fed-xrv",
"@sap-cloud-sdk/http-client": "^4",
"@sap/cds": ">=9",
"express": "^4"
},
"devDependencies": {
+ "@sap/cds-dk": "^9.2.0",
"@cap-js/cds-test": "*",
"@cap-js/sqlite": ">=2"
},
diff --git a/test/cf.mjs b/test/cf.mjs
new file mode 100644
index 0000000..5ef7122
--- /dev/null
+++ b/test/cf.mjs
@@ -0,0 +1,44 @@
+import fs from 'node:fs'
+import cp from 'node:child_process'
+import path from 'node:path'
+
+
+const xtravelsDir = path.resolve(import.meta.dirname + '/../')
+const xflightsDir = path.dirname(import.meta.resolve('@capire/xflights/package.json')).replace('file:', '')
+
+// if .env files exist they will prevent binding generation
+try { fs.rmSync(path.resolve(xtravelsDir, '.env')) } catch {}
+try { fs.rmSync(path.resolve(xflightsDir, '.env')) } catch {}
+
+const xtravels = await deploy(xtravelsDir)
+const xflights = await deploy(xflightsDir)
+
+fs.writeFileSync(path.resolve(xtravelsDir, '.cdsrc-private.json'), JSON.stringify(
+ {
+ "[fed-syn]": {
+ requires: {
+ db: xtravels,
+ 'sap.capire.flights.data': {
+ ...xflights,
+ vcap: { name: 'sap.capire.flights.data' }
+ }
+ }
+ }
+ },
+ null,
+ 2
+))
+
+function deploy(cwd) {
+ return new Promise((resolve, reject) => {
+ const deploy = cp.spawn('cds', ['deploy', '-2', 'hana'], {
+ cwd,
+ stdio: 'pipe', // for debugging switch to 'inherit'
+ })
+ deploy.on('exit', code => {
+ if (code) return reject(new Error(`Failed to deploy xflight (${code})`))
+ const binding = JSON.parse(fs.readFileSync(path.resolve(cwd, '.cdsrc-private.json')))
+ return resolve(binding.requires['[hybrid]'].db)
+ })
+ })
+}
diff --git a/test/hana.mjs b/test/hana.mjs
new file mode 100644
index 0000000..e5c416d
--- /dev/null
+++ b/test/hana.mjs
@@ -0,0 +1,238 @@
+import fs from 'node:fs'
+import cp from 'node:child_process'
+import cds from '@sap/cds'
+
+const database = 'xflights'
+const credentials = {
+ "user": "SYSTEM",
+ "password": "Manager1",
+ "host": "localhost",
+ "port": "30041",
+ "useTLS": true,
+ "encrypt": true,
+ "sslValidateCertificate": false,
+ "rejectUnauthorized": false,
+ "disableCloudRedirect": true,
+ "driver": "hdb"
+}
+
+console.log('HDI setup')
+const [travels, xflights] = await Promise.all(
+ [
+ hdiContainer({ database, tenant: 'travels' }),
+ hdiContainer({ database, tenant: 'xflights' }),
+ ]
+)
+
+const xtravelsDir = cds.utils.path.resolve(import.meta.dirname + '/../')
+const xflightsDir = cds.utils.path.dirname(import.meta.resolve('@capire/xflights/package.json')).replace('file:', '')
+
+// if .cdsrc-private.json files exist they will pollute the services
+try { fs.rmSync(cds.utils.path.resolve(xtravelsDir, '.cdsrc-private.json')) } catch { }
+try { fs.rmSync(cds.utils.path.resolve(xflightsDir, '.cdsrc-private.json')) } catch { }
+
+console.log('Data Sphere Setup')
+const xflightsDataSphere = await ensureDataSphere()
+
+console.log('HDI + Data Sphere stored')
+fs.writeFileSync(cds.utils.path.resolve(xflightsDir, '.env'), `
+VCAP_SERVICES=${JSON.stringify({ hana: [xflights] })}
+`)
+
+fs.writeFileSync(cds.utils.path.resolve(import.meta.dirname, '../.env'), `
+TARGET_CONTAINER=${travels.name}
+VCAP_SERVICES=${JSON.stringify({ hana: [travels, xflights], 'user-provided': [xflightsDataSphere] })}
+`)
+
+console.log('remote setup')
+const hana2hana = {
+ name: 'xflights',
+ adapter: 'hanaodbc',
+ configuration: `Driver=libodbcHDB.so;ServerNode=${xflights.credentials.host}:${xflights.credentials.port.replace('00', '00') /* hxe internal port is 39041 */};trustall=TRUE;encrypt=TRUE;sslValidateCertificate=FALSE`,
+ credential: `user=${xflights.credentials.user};password=${xflights.credentials.password}`,
+}
+
+const hana2rap = {
+ name: 'sflight-rap',
+ adapter: 'ODataAdapter',
+ configuration: `
+
+ https://abap:50001/sap/opu/odata/DMO/UI_TRAVEL_A_D_O2/
+`,
+ credential: `DEVELOPERABAPtr2022#01`,
+ cert: `-----BEGIN CERTIFICATE-----
+MIICWDCCAcECCAogFwQEBlMBMA0GCSqGSIb3DQEBCwUAMHExCzAJBgNVBAYTAkRF
+MRwwGgYDVQQKExNTQVAgVHJ1c3QgQ29tbXVuaXR5MRMwEQYDVQQLEwpTQVAgV2Vi
+IEFTMRQwEgYDVQQLEwtJREVNT1NZU1RFTTEZMBcGA1UEAwwQKi5kdW1teS5ub2Rv
+bWFpbjAeFw0xNzA0MDQwNjUzMDFaFw0zODAxMDEwMDAwMDFaMHExCzAJBgNVBAYT
+AkRFMRwwGgYDVQQKExNTQVAgVHJ1c3QgQ29tbXVuaXR5MRMwEQYDVQQLEwpTQVAg
+V2ViIEFTMRQwEgYDVQQLEwtJREVNT1NZU1RFTTEZMBcGA1UEAwwQKi5kdW1teS5u
+b2RvbWFpbjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAsnkwjakB2kFfTD1/
+uce4jSxs/0ufkXuHZhPNQ5No4vysqslhOcgIJBtYfEGtCzzFWPcY3PdzwK0pMPYF
+nZDlAwrU8W1YqIqtlarFS1QsUMXqUpISgihUf9QOY8FEuivgZfk6qVwdauSY9SMr
+5gRaJiI0ZjcabRtokZgD/P3H5usCAwEAATANBgkqhkiG9w0BAQsFAAOBgQBECAqq
+UGP21ujdaoPxls7D0hyycPO4ZUkJ6OzDpv1gul6IhM559LFWdJigHaKJDeK4GOo4
+Koj8UqYCYLKr/iUkzn4T1cQSUUPAFknn+HNE4hho2Qk/pmUCkT8jV6NLHg0phtjS
+Jjp0ztLH/VlXfiveTZvizl/FqA4k2RpnSVtQgQ==
+-----END CERTIFICATE-----
+`
+}
+
+await ensureRemoteSource(hana2hana)
+await grantRemoteSource(hana2hana, travels.credentials)
+
+await ensureRemoteSource(hana2rap)
+await grantRemoteSource(hana2rap, travels.credentials)
+
+console.log('xflights deploy')
+await new Promise((resolve, reject) => {
+ const deploy = cp.spawn('cds', ['deploy', '-2', 'hana'], {
+ cwd: xflightsDir,
+ stdio: 'pipe', // for debugging switch to 'inherit'
+ env: {
+ PATH: process.env.PATH,
+ },
+ })
+ deploy.on('exit', code => {
+ if (code) return reject(new Error(`Failed to deploy xflight (${code})`))
+ return resolve()
+ })
+})
+
+async function hdiContainer(isolate) {
+ const connector = await cds.connect.to('db-hdi', { kind: 'hana', credentials })
+ await connector.run(`CREATE ROLE "${(isolate.database + '_' + isolate.tenant).toUpperCase()}::access_role"`).catch(() => { })
+ await connector.database(isolate)
+ await connector.tenant(isolate)
+ const creds = { ...connector.options.credentials }
+ delete creds.__system__
+ delete creds.__database__
+ const hdi_credentials = {
+ "binding_guid": cds.utils.uuid(),
+ "binding_name": null,
+ "credentials": {
+ ...creds,
+ "database_id": database,
+ "host": creds.host,
+ "port": creds.port,
+ "schema": creds.schema,
+ "user": creds.user,
+ "password": creds.password,
+ "hdi_user": creds.user,
+ "hdi_password": creds.password,
+ "driver": "com.sap.db.jdbc.Driver",
+ "url": `jdbc:sap://hana:${creds.port}?encrypt=true&validateCertificate=false¤tschema=${creds.schema}&disableCloudRedirect=true`,
+ },
+ "instance_guid": cds.utils.uuid(),
+ "instance_name": isolate.tenant,
+ "label": "hana",
+ "name": isolate.tenant,
+ "plan": "hdi-shared",
+ "provider": null,
+ "syslog_drain_url": null,
+ "tags": [
+ "hana",
+ "database",
+ "relational"
+ ],
+ "volume_mounts": []
+ }
+
+ return hdi_credentials
+}
+
+/**
+ * Creates a remote source
+ * @param {object} remote Remote source configurations
+ * @param {string} remote.name Remote source name
+ */
+async function ensureRemoteSource(remote) {
+ const connector = await cds.connect.to('db-connector', { kind: "hana", credentials })
+
+ await connector.tx(async tx => {
+ const { name } = remote
+
+ const adapterExists = await tx.run(`SELECT TO_BOOLEAN(IS_SYSTEM_ADAPTER) AS "native" FROM SYS.ADAPTERS WHERE ADAPTER_NAME='${remote.adapter}'`)
+ // For new HANA systems it is required to "create" the adapter once that is done all adapters show up
+ if (!adapterExists.length) await tx.run(`CREATE ADAPTER "${remote.adapter}" PROPERTIES '' AT LOCATION DPSERVER;`).catch((err) => { debugger })
+ const isNative = adapterExists[0]?.native
+
+ // Check if realtime replication is supported
+ const rtrSupported = await tx.run(`SELECT TO_BOOLEAN(IS_CDC_SUPPORTED) FROM SYS.ADAPTER_CAPABILITIES WHERE ADAPTER_NAME='${remote.adapter}'`)
+
+ const remoteSourceExists = await tx.run(`SELECT * FROM SYS.REMOTE_SOURCES WHERE REMOTE_SOURCE_NAME='${name}'`)
+ if (remoteSourceExists.length) {
+ await tx.run(`ALTER REMOTE SOURCE "${name}" ADAPTER "${remote.adapter}"${isNative ? '' : ' AT LOCATION DPSERVER'}
+ CONFIGURATION '${remote.configuration.replaceAll("'", "''")}'
+ WITH CREDENTIAL TYPE 'PASSWORD' USING '${remote.credential.replaceAll("'", "''")}'`)
+ } else {
+ await tx.run(`CREATE REMOTE SOURCE "${name}" ADAPTER "${remote.adapter}"${isNative ? '' : ' AT LOCATION DPSERVER'}
+ CONFIGURATION '${remote.configuration.replaceAll("'", "''")}'
+ WITH CREDENTIAL TYPE 'PASSWORD' USING '${remote.credential.replaceAll("'", "''")}'`)
+ }
+
+ if (remote.cert) {
+ const certName = `"${name}_CERT"`
+ await tx.run(`CREATE PSE REMOTE_SOURCES`).catch(err => err)
+ await tx.run(`CREATE CERTIFICATE ${certName} FROM '${remote.cert}'`).catch(err => err)
+ await tx.run(`ALTER PSE REMOTE_SOURCES ADD CERTIFICATE ${certName};`).catch(err => err)
+ await tx.run(`SET PSE REMOTE_SOURCES PURPOSE REMOTE SOURCE;`).catch(err => err)
+ }
+
+ await tx.run(`CALL CHECK_REMOTE_SOURCE('${name}')`)
+ })
+ await connector.disconnect()
+}
+
+async function grantRemoteSource(remote, creds) {
+ const connector = await cds.connect.to('db-connector', { kind: "hana", credentials })
+
+ await connector.tx(async tx => {
+ const { name } = remote
+ await tx.run(`GRANT CREATE VIRTUAL TABLE ON REMOTE SOURCE "${name}" TO ${creds.schema}#OO`)
+ await tx.run(`GRANT CREATE REMOTE SUBSCRIPTION ON REMOTE SOURCE "${name}" TO ${creds.schema}#OO`)
+ await tx.run(`GRANT LINKED DATABASE ON REMOTE SOURCE "${name}" TO ${creds.schema}#OO`)
+
+ await tx.run(`GRANT CREATE VIRTUAL TABLE ON REMOTE SOURCE "${name}" TO ${creds.schema}`)
+ })
+
+ await connector.disconnect()
+}
+
+async function ensureDataSphere(space = 'TESTS_SPACE_0') {
+ const res = await fetch(`http://localhost:15002/dwaas-core/space/${space}/databaseusers/reset`, {
+ "method": 'POST',
+ "headers": {
+ "content-type": "application/json",
+ "x-csrf-token": "null",
+ },
+ "body": JSON.stringify({ username: 'TESTS_SPACE_0#CAFE' }),
+ })
+
+ const creds = await res.json()
+
+ return {
+ "binding_guid": cds.utils.uuid(),
+ "binding_name": null,
+ "credentials": {
+ ...credentials,
+ ...creds,
+ user: creds.username,
+ schema: space,
+ type: 'procedure',
+ procedure_schema: `${space}$TEC`,
+ procedure: 'HDI_GRANTOR_FOR_CUPS',
+ },
+ "instance_guid": cds.utils.uuid(),
+ "instance_name": "xflights-datasphere",
+ "label": "user-provided",
+ "name": "xflights-datasphere",
+ "plan": "hdi-shared",
+ "provider": null,
+ "syslog_drain_url": null,
+ "tags": [
+ "hana",
+ ],
+ "volume_mounts": []
+ }
+}
\ No newline at end of file
diff --git a/test/odata.test.js b/test/odata.test.js
index 0e37d77..64913c1 100644
--- a/test/odata.test.js
+++ b/test/odata.test.js
@@ -1,5 +1,6 @@
const cds = require('@sap/cds')
-const { GET, POST, PATCH, axios, expect } = cds.test(__dirname+'/..')
+
+const { GET, POST, PATCH, axios, expect } = cds.test(__dirname+'/..', '--with-mocks', '--with-bindings')
const EDIT = (url) => POST (url+'/TravelService.draftEdit',{})
const SAVE = (url) => POST (url+'/TravelService.draftActivate')
axios.defaults.auth = { username: 'alice', password: 'admin' }
@@ -8,6 +9,11 @@ const ID = '1'
describe ("Basic Querying", () => {
+ it("should read from federated data", async() => {
+ const flights = await GET `/odata/v4/travel/Flights/$count`
+ expect(flights) .property('data') .to.eql(44)
+ })
+
it ("should read from row references", async()=>{
const travel = await SELECT.one `TravelService.Travels` .where ({ ID })
expect (travel) .to.exist
@@ -47,7 +53,7 @@ describe('Basic OData', () => {
const { data } = await GET `/odata/v4/travel/Travels?$filter=ID eq 175`
expect(data.value).to.containSubset([{
BeginDate: '2023-08-02',
- BookingFee: 60,
+ BookingFee: '60.0000',
createdAt: expectedValue => /2023-07-16T18:42:07\.000(0000)?Z/.test(expectedValue), // timestamp precision increase with cds^7
createdBy: 'Hansmann',
Currency_code: 'SGD',
@@ -60,7 +66,7 @@ describe('Basic OData', () => {
modifiedBy: 'Deichgraeber',
Agency_ID: '070029',
Customer_ID: '000318',
- TotalPrice: 23439,
+ TotalPrice: '23439.0000',
ID: 175,
Status_code: 'A',
}])
@@ -122,7 +128,7 @@ describe('Basic OData', () => {
})
const { data: newTravel } = await SAVE (`/odata/v4/travel/Travels(ID='${newDraft.ID}',IsActiveEntity=false)`)
- expect(newTravel).to.contain({ ID: 4134, TotalPrice: 11 })
+ expect(newTravel).to.contain({ ID: 4134, TotalPrice: '11.0000' })
})
it ('re-calculates totals after booking fee changed', async ()=>{
@@ -151,7 +157,7 @@ describe('Basic OData', () => {
// console.log (TotalPrice,total)
// }
expect(draft).to.containSubset({
- TotalPrice: 7375,
+ TotalPrice: '7375.0000',
ID: 4133,
})
@@ -193,14 +199,14 @@ describe('Basic OData', () => {
it('deduct discount multiple times does not end up in error', async () => {
const { data: res1 } = await GET `/odata/v4/travel/Travels(ID=1,IsActiveEntity=true)`
- expect(res1).to.contain({ TotalPrice: 900, BookingFee: 20 })
+ expect(res1).to.contain({ TotalPrice: '900.0000', BookingFee: '20.0000' })
await POST(
`/odata/v4/travel/Travels(ID=1,IsActiveEntity=true)/TravelService.deductDiscount`,
{ percent: 11 }
)
const { data: res2 } = await GET `/odata/v4/travel/Travels(ID=1,IsActiveEntity=true)`
- expect(res2).to.contain({ TotalPrice: 897.8, BookingFee: 17.8 })
+ expect(res2).to.contain({ TotalPrice: '897.8000', BookingFee: '17.8000' })
await POST(
`/odata/v4/travel/Travels(ID=1,IsActiveEntity=true)/TravelService.deductDiscount`,
@@ -208,7 +214,7 @@ describe('Basic OData', () => {
)
const { data: res3 } = await GET `/odata/v4/travel/Travels(ID=1,IsActiveEntity=true)`
- expect(res3).to.contain({ TotalPrice: 895.842, BookingFee: 15.842 })
+ expect(res3).to.contain({ TotalPrice: '895.8420', BookingFee: '15.8420' })
await POST(
`/odata/v4/travel/Travels(ID=1,IsActiveEntity=true)/TravelService.deductDiscount`,
@@ -216,7 +222,7 @@ describe('Basic OData', () => {
)
// rounded to 3 decimals
const { data: res4 } = await GET `/odata/v4/travel/Travels(ID=1,IsActiveEntity=true)`
- expect(res4).to.contain({ TotalPrice: 894.099, BookingFee: 14.099 })
+ expect(res4).to.contain({ TotalPrice: '894.0990', BookingFee: '14.0990' })
})
it('allows deducting discounts on drafts as well', async ()=>{
@@ -224,10 +230,10 @@ describe('Basic OData', () => {
const Draft = `/odata/v4/travel/Travels(ID=66,IsActiveEntity=false)`
const { data:res0 } = await GET (Active)
- expect(res0).to.contain({ ID:66, TotalPrice: 729, BookingFee: 10 })
+ expect(res0).to.contain({ ID:66, TotalPrice: '729.0000', BookingFee: '10.0000' })
const { data:res1 } = await EDIT (Active)
- expect(res1).to.contain({ TotalPrice: 729, BookingFee: 10 })
+ expect(res1).to.contain({ TotalPrice: '729.0000', BookingFee: '10.0000' })
// Change the Travel's dates to avoid validation errors
const today = new Date, tomorrow = new Date; tomorrow.setDate(today.getDate()+1)
@@ -236,15 +242,15 @@ describe('Basic OData', () => {
await POST (`${Draft}/TravelService.deductDiscount`, { percent: 50 })
const { data:res2 } = await GET `/odata/v4/travel/Travels(ID=66,IsActiveEntity=false)`
- expect(res2).to.contain({ TotalPrice: 724, BookingFee: 5 })
+ expect(res2).to.contain({ TotalPrice: '724.0000', BookingFee: '5.0000' })
const { data:res3 } = await GET (Draft)
- expect(res3).to.contain({ TotalPrice: 724, BookingFee: 5 })
+ expect(res3).to.contain({ TotalPrice: '724.0000', BookingFee: '5.0000' })
await SAVE (Draft)
const { data:res4 } = await GET (Active)
- expect(res4).to.contain({ TotalPrice: 724, BookingFee: 5 })
+ expect(res4).to.contain({ TotalPrice: '724.0000', BookingFee: '5.0000' })
})
})