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' }) }) })