Remove notes app
This commit is contained in:
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"extends": "eslint:recommended",
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true,
|
||||
"jest": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"globals": {
|
||||
"SELECT": true,
|
||||
"INSERT": true,
|
||||
"UPDATE": true,
|
||||
"DELETE": true,
|
||||
"CREATE": true,
|
||||
"DROP": true,
|
||||
"cds": true
|
||||
},
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"require-atomic-updates": "off"
|
||||
}
|
||||
}
|
||||
29
notes/.gitignore
vendored
29
notes/.gitignore
vendored
@@ -1,29 +0,0 @@
|
||||
# CAP s4-cap-ext
|
||||
_out
|
||||
*.db
|
||||
connection.properties
|
||||
default-*.json
|
||||
gen/
|
||||
node_modules/
|
||||
target/
|
||||
|
||||
# Web IDE, App Studio
|
||||
.che/
|
||||
.gen/
|
||||
|
||||
# MTA
|
||||
*_mta_build_tmp
|
||||
*.mtar
|
||||
mta_archives/
|
||||
|
||||
# Other
|
||||
.DS_Store
|
||||
*.orig
|
||||
*.log
|
||||
|
||||
*.iml
|
||||
*.flattened-pom.xml
|
||||
|
||||
# IDEs
|
||||
# .vscode
|
||||
# .idea
|
||||
@@ -1,29 +0,0 @@
|
||||
# S4 Extension with CAP
|
||||
|
||||
## Scenario
|
||||
|
||||
This sample applications shows how to extend an existing oData service on BTP using CAP.
|
||||
|
||||
In our scenario, we want to extend the entity `A_BusinessPartner` of an external service ([BusinessPartner API](https://api.sap.com/api/API_BUSINESS_PARTNER/resource)) with a `note` field so that our end-users of the applictions can maintain notes for each business partner
|
||||
## Diagramm
|
||||
|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
### Business Partner Local Mock
|
||||
|
||||
Start a CAP process for the local mock server for Business Partner and a second one for the CustomerService:
|
||||
|
||||
1. Run: `cds mock API_BUSINESS_PARTNER -p 5001`
|
||||
2. Wait until startup is completed
|
||||
3. Run in a 2nd terminal: `cds serve all --with-mocks --in-memory`
|
||||
4. Now, you can issues the requests listed in `requests.http`
|
||||
|
||||
### Business Partner from Sandbox Server
|
||||
|
||||
1. Goto https://api.sap.com/api/API_BUSINESS_PARTNER/resource
|
||||
2. Get service key
|
||||
3. Export env var `S4_APIKEY` with the service key (`export S4_APIKEY="<your-service-key>"`)
|
||||
4. Run: `CDS_ENV=sandbox cds watch`
|
||||
5. Now, you can issues the requests listed in `requests.http`
|
||||
@@ -1,81 +0,0 @@
|
||||
<mxfile>
|
||||
<diagram id="f63MNExEARHemv8uKXmC" name="Page-1">
|
||||
<mxGraphModel dx="874" dy="414" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="jFfBIeNJgKm7YW3a5aRN-12" value="API_BUSINESS_PARTNER" style="shape=umlFrame;whiteSpace=wrap;html=1;width=190;height=20;" parent="1" vertex="1">
|
||||
<mxGeometry x="200" y="50" width="360" height="130" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="jFfBIeNJgKm7YW3a5aRN-13" value="com.sap.notes" style="shape=umlFrame;whiteSpace=wrap;html=1;width=190;height=20;" parent="1" vertex="1">
|
||||
<mxGeometry x="200" y="220" width="360" height="170" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="jFfBIeNJgKm7YW3a5aRN-2" value="A_BusinessPartner" style="html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="410" y="100" width="110" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="jFfBIeNJgKm7YW3a5aRN-3" value="Notes" style="html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="230" y="320" width="110" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="jFfBIeNJgKm7YW3a5aRN-5" value="Suppliers" style="html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="410" y="230" width="110" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="jFfBIeNJgKm7YW3a5aRN-7" value="projects" style="endArrow=block;endSize=16;endFill=0;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;" parent="1" source="jFfBIeNJgKm7YW3a5aRN-5" target="jFfBIeNJgKm7YW3a5aRN-2" edge="1">
|
||||
<mxGeometry x="-0.25" width="160" relative="1" as="geometry">
|
||||
<mxPoint x="580" y="360" as="sourcePoint"/>
|
||||
<mxPoint x="740" y="360" as="targetPoint"/>
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jFfBIeNJgKm7YW3a5aRN-14" value="NotesService" style="shape=umlFrame;whiteSpace=wrap;html=1;width=100;height=20;" parent="1" vertex="1">
|
||||
<mxGeometry x="200" y="440" width="360" height="120" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="jFfBIeNJgKm7YW3a5aRN-15" value="Notes" style="html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="230" y="480" width="110" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="jFfBIeNJgKm7YW3a5aRN-16" value="Suppliers" style="html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="410" y="480" width="110" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="jFfBIeNJgKm7YW3a5aRN-18" value="projects" style="endArrow=block;endSize=16;endFill=0;html=1;" parent="1" target="jFfBIeNJgKm7YW3a5aRN-3" edge="1">
|
||||
<mxGeometry x="0.091" width="160" relative="1" as="geometry">
|
||||
<mxPoint x="284" y="480" as="sourcePoint"/>
|
||||
<mxPoint x="530" y="265" as="targetPoint"/>
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="jFfBIeNJgKm7YW3a5aRN-19" value="projects" style="endArrow=block;endSize=16;endFill=0;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" source="jFfBIeNJgKm7YW3a5aRN-16" target="jFfBIeNJgKm7YW3a5aRN-5" edge="1">
|
||||
<mxGeometry x="-0.4" width="160" relative="1" as="geometry">
|
||||
<mxPoint x="294" y="490" as="sourcePoint"/>
|
||||
<mxPoint x="294.8148148148148" y="380" as="targetPoint"/>
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="w1YGFUIT9ERIj0VLSvAl-2" value="1" style="endArrow=open;html=1;endSize=12;startArrow=diamondThin;startSize=14;startFill=1;edgeStyle=orthogonalEdgeStyle;align=left;verticalAlign=bottom;rounded=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="jFfBIeNJgKm7YW3a5aRN-5" target="jFfBIeNJgKm7YW3a5aRN-3" edge="1">
|
||||
<mxGeometry x="-1" y="3" relative="1" as="geometry">
|
||||
<mxPoint x="420" y="320" as="sourcePoint"/>
|
||||
<mxPoint x="580" y="320" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="2" value="requests" style="html=1;verticalAlign=bottom;endArrow=block;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="jFfBIeNJgKm7YW3a5aRN-3" edge="1">
|
||||
<mxGeometry x="0.2727" y="-5" width="80" relative="1" as="geometry">
|
||||
<mxPoint x="70" y="310" as="sourcePoint"/>
|
||||
<mxPoint x="120" y="345" as="targetPoint"/>
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="Database" style="html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="10" y="320" width="110" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="S/4 System" style="html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="10" y="100" width="110" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="requests" style="html=1;verticalAlign=bottom;endArrow=block;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="jFfBIeNJgKm7YW3a5aRN-2" target="4" edge="1">
|
||||
<mxGeometry x="0.7241" y="-5" width="80" relative="1" as="geometry">
|
||||
<mxPoint x="240" y="355" as="sourcePoint"/>
|
||||
<mxPoint x="130" y="355" as="targetPoint"/>
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 210 KiB |
@@ -1,8 +0,0 @@
|
||||
namespace sap.capire.notes;
|
||||
|
||||
using { cuid } from '@sap/cds/common';
|
||||
|
||||
entity Notes: cuid {
|
||||
note: String;
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
ID,note,supplier_ID
|
||||
D632D4EE-E772-454A-913E-26A7B8DAA7FB,note1 for 11,11
|
||||
545A3CF9-84CF-46C8-93DC-E29F0F2BC6BE,note2 for 11,11
|
||||
24B58115-E394-423B-BEAB-53419A32B927,note3,9980000082
|
||||
|
@@ -1,33 +0,0 @@
|
||||
using { API_BUSINESS_PARTNER as BusinessPartner } from '../srv/external/API_BUSINESS_PARTNER.csn';
|
||||
|
||||
/**
|
||||
* Supplier data from S/4
|
||||
*/
|
||||
@readonly
|
||||
entity Suppliers as projection on BusinessPartner.A_BusinessPartner {
|
||||
*,
|
||||
key BusinessPartner as ID,
|
||||
BusinessPartnerFullName as fullName,
|
||||
BusinessPartnerType as customerType,
|
||||
// TODO: Add issue
|
||||
// virtual notes: Composition of many Notes on notes.supplier.ID = $self.BusinessPartner;
|
||||
} excluding {
|
||||
OrganizationBPName1, OrganizationBPName2,OrganizationBPName3, OrganizationBPName4, to_BuPaIdentification, to_BuPaIndustry, to_BusinessPartnerAddress, to_BusinessPartnerBank, to_BusinessPartnerContact, to_BusinessPartnerRole, to_BusinessPartnerTax, to_Customer, to_Supplier
|
||||
}
|
||||
|
||||
using { sap.capire.notes.Notes } from './data-model';
|
||||
|
||||
extend Notes {
|
||||
/**
|
||||
* Supplier data from S/4
|
||||
*/
|
||||
supplier: Association to Suppliers;
|
||||
}
|
||||
|
||||
// We'cant add the association to the Suppliers projection yet, so we need to put it to the external entity definition
|
||||
// TODO: https://github.wdf.sap.corp/cap/matters/projects/44#card-195456
|
||||
extend BusinessPartner.A_BusinessPartner {
|
||||
// [ERROR] db/mashup.cds:28:5: Only an association that points back to this artifact can be compared to "$self" (in entity:"API_BUSINESS_PARTNER.A_BusinessPartner"/element:"note"/on)
|
||||
// notes: Composition of many Notes on notes.supplier = $self;
|
||||
notes: Composition of many Notes on notes.supplier.ID = $self.BusinessPartner;
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
{
|
||||
"file_suffixes": {
|
||||
"csv": {
|
||||
"plugin_name": "com.sap.hana.di.tabledata.source"
|
||||
},
|
||||
"hdbafllangprocedure": {
|
||||
"plugin_name": "com.sap.hana.di.afllangprocedure"
|
||||
},
|
||||
"hdbanalyticprivilege": {
|
||||
"plugin_name": "com.sap.hana.di.analyticprivilege"
|
||||
},
|
||||
"hdbcalculationview": {
|
||||
"plugin_name": "com.sap.hana.di.calculationview"
|
||||
},
|
||||
"hdbcollection": {
|
||||
"plugin_name": "com.sap.hana.di.collection"
|
||||
},
|
||||
"hdbconstraint": {
|
||||
"plugin_name": "com.sap.hana.di.constraint"
|
||||
},
|
||||
"hdbdropcreatetable": {
|
||||
"plugin_name": "com.sap.hana.di.dropcreatetable"
|
||||
},
|
||||
"hdbflowgraph": {
|
||||
"plugin_name": "com.sap.hana.di.flowgraph"
|
||||
},
|
||||
"hdbfunction": {
|
||||
"plugin_name": "com.sap.hana.di.function"
|
||||
},
|
||||
"hdbgraphworkspace": {
|
||||
"plugin_name": "com.sap.hana.di.graphworkspace"
|
||||
},
|
||||
"hdbhadoopmrjob": {
|
||||
"plugin_name": "com.sap.hana.di.virtualfunctionpackage.hadoop"
|
||||
},
|
||||
"hdbindex": {
|
||||
"plugin_name": "com.sap.hana.di.index"
|
||||
},
|
||||
"hdblibrary": {
|
||||
"plugin_name": "com.sap.hana.di.library"
|
||||
},
|
||||
"hdbmigrationtable": {
|
||||
"plugin_name": "com.sap.hana.di.table.migration"
|
||||
},
|
||||
"hdbprocedure": {
|
||||
"plugin_name": "com.sap.hana.di.procedure"
|
||||
},
|
||||
"hdbprojectionview": {
|
||||
"plugin_name": "com.sap.hana.di.projectionview"
|
||||
},
|
||||
"hdbprojectionviewconfig": {
|
||||
"plugin_name": "com.sap.hana.di.projectionview.config"
|
||||
},
|
||||
"hdbreptask": {
|
||||
"plugin_name": "com.sap.hana.di.reptask"
|
||||
},
|
||||
"hdbresultcache": {
|
||||
"plugin_name": "com.sap.hana.di.resultcache"
|
||||
},
|
||||
"hdbrole": {
|
||||
"plugin_name": "com.sap.hana.di.role"
|
||||
},
|
||||
"hdbroleconfig": {
|
||||
"plugin_name": "com.sap.hana.di.role.config"
|
||||
},
|
||||
"hdbsearchruleset": {
|
||||
"plugin_name": "com.sap.hana.di.searchruleset"
|
||||
},
|
||||
"hdbsequence": {
|
||||
"plugin_name": "com.sap.hana.di.sequence"
|
||||
},
|
||||
"hdbstatistics": {
|
||||
"plugin_name": "com.sap.hana.di.statistics"
|
||||
},
|
||||
"hdbstructuredprivilege": {
|
||||
"plugin_name": "com.sap.hana.di.structuredprivilege"
|
||||
},
|
||||
"hdbsynonym": {
|
||||
"plugin_name": "com.sap.hana.di.synonym"
|
||||
},
|
||||
"hdbsynonymconfig": {
|
||||
"plugin_name": "com.sap.hana.di.synonym.config"
|
||||
},
|
||||
"hdbsystemversioning": {
|
||||
"plugin_name": "com.sap.hana.di.systemversioning"
|
||||
},
|
||||
"hdbtable": {
|
||||
"plugin_name": "com.sap.hana.di.table"
|
||||
},
|
||||
"hdbtabledata": {
|
||||
"plugin_name": "com.sap.hana.di.tabledata"
|
||||
},
|
||||
"hdbtabletype": {
|
||||
"plugin_name": "com.sap.hana.di.tabletype"
|
||||
},
|
||||
"hdbtrigger": {
|
||||
"plugin_name": "com.sap.hana.di.trigger"
|
||||
},
|
||||
"hdbview": {
|
||||
"plugin_name": "com.sap.hana.di.view"
|
||||
},
|
||||
"hdbvirtualfunction": {
|
||||
"plugin_name": "com.sap.hana.di.virtualfunction"
|
||||
},
|
||||
"hdbvirtualfunctionconfig": {
|
||||
"plugin_name": "com.sap.hana.di.virtualfunction.config"
|
||||
},
|
||||
"hdbvirtualpackagehadoop": {
|
||||
"plugin_name": "com.sap.hana.di.virtualpackage.hadoop"
|
||||
},
|
||||
"hdbvirtualpackagesparksql": {
|
||||
"plugin_name": "com.sap.hana.di.virtualpackage.sparksql"
|
||||
},
|
||||
"hdbvirtualprocedure": {
|
||||
"plugin_name": "com.sap.hana.di.virtualprocedure"
|
||||
},
|
||||
"hdbvirtualprocedureconfig": {
|
||||
"plugin_name": "com.sap.hana.di.virtualprocedure.config"
|
||||
},
|
||||
"hdbvirtualtable": {
|
||||
"plugin_name": "com.sap.hana.di.virtualtable"
|
||||
},
|
||||
"hdbvirtualtableconfig": {
|
||||
"plugin_name": "com.sap.hana.di.virtualtable.config"
|
||||
},
|
||||
"properties": {
|
||||
"plugin_name": "com.sap.hana.di.tabledata.properties"
|
||||
},
|
||||
"tags": {
|
||||
"plugin_name": "com.sap.hana.di.tabledata.properties"
|
||||
},
|
||||
"txt": {
|
||||
"plugin_name": "com.sap.hana.di.copyonly"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
## Generated mta.yaml based on template version 0.4.0
|
||||
## appName = s4-cap-ext
|
||||
## language=nodejs; multiTenant=false
|
||||
## approuter=
|
||||
_schema-version: '3.1'
|
||||
ID: s4-cap-ext
|
||||
version: 1.0.0
|
||||
description: "A simple CAP project."
|
||||
parameters:
|
||||
enable-parallel-deployments: true
|
||||
|
||||
build-parameters:
|
||||
before-all:
|
||||
- builder: custom
|
||||
commands:
|
||||
- npm install --production
|
||||
- npx -p @sap/cds-dk cds build --production
|
||||
|
||||
modules:
|
||||
# --------------------- SERVER MODULE ------------------------
|
||||
- name: s4-cap-ext-srv
|
||||
# ------------------------------------------------------------
|
||||
type: nodejs
|
||||
path: gen/srv
|
||||
requires:
|
||||
# Resources extracted from CAP configuration
|
||||
- name: s4-cap-ext-db
|
||||
provides:
|
||||
- name: srv-api # required by consumers of CAP services (e.g. approuter)
|
||||
properties:
|
||||
srv-url: ${default-url}
|
||||
|
||||
# -------------------- SIDECAR MODULE ------------------------
|
||||
- name: s4-cap-ext-db-deployer
|
||||
# ------------------------------------------------------------
|
||||
type: hdb
|
||||
path: gen/db
|
||||
parameters:
|
||||
buildpack: nodejs_buildpack
|
||||
requires:
|
||||
# 'hana' and 'xsuaa' resources extracted from CAP configuration
|
||||
- name: s4-cap-ext-db
|
||||
|
||||
|
||||
resources:
|
||||
# services extracted from CAP configuration
|
||||
# 'service-plan' can be configured via 'cds.requires.<name>.vcap.plan'
|
||||
# ------------------------------------------------------------
|
||||
- name: s4-cap-ext-db
|
||||
# ------------------------------------------------------------
|
||||
type: com.sap.xs.hdi-container
|
||||
parameters:
|
||||
service: hana # or 'hanatrial' on trial landscapes
|
||||
service-plan: hdi-shared
|
||||
properties:
|
||||
hdi-service-name: ${service-name}
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"name": "@capire/notes",
|
||||
"version": "1.0.0",
|
||||
"description": "Maintain notes for a Supplier",
|
||||
"repository": "<Add your repository here>",
|
||||
"license": "UNLICENSED",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@sap/cds": ">=5",
|
||||
"@sap/cds-compiler": "^2",
|
||||
"@sap/hana-client": "^2.6.61",
|
||||
"express": "^4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sqlite3": "5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cds run"
|
||||
},
|
||||
"cds": {
|
||||
"requires": {
|
||||
"[sandbox]": {
|
||||
"API_BUSINESS_PARTNER": {
|
||||
"kind": "odata",
|
||||
"model": "srv/external/API_BUSINESS_PARTNER",
|
||||
"credentials": {
|
||||
"url": "https://sandbox.api.sap.com/s4hanacloud/sap/opu/odata/sap/API_BUSINESS_PARTNER/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
### Get remote suppliers
|
||||
|
||||
GET http://localhost:4004/notes/Suppliers?$top=11
|
||||
|
||||
### Get remote suppliers with expanded notes
|
||||
|
||||
GET http://localhost:4004/notes/Suppliers?$top=11&$expand=notes
|
||||
|
||||
### Suppliers set to readonly --> fails with 405
|
||||
|
||||
PATCH http://localhost:4004/notes/Suppliers('11')
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"BusinessPartnerFullName": "Can't change!"
|
||||
}
|
||||
@@ -1,376 +0,0 @@
|
||||
function fixColumnName(entity, name) {
|
||||
const fullName = `${entity.name}.${name}`;
|
||||
return RemoteHandler.columnNameFixes[fullName] || name;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} msg
|
||||
* @returns {never}
|
||||
*/
|
||||
function throwError(msg) {
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{name: string}|string} entity
|
||||
* @param {{name: string}|string} association
|
||||
* @param {string} msg
|
||||
* @returns {never}
|
||||
*/
|
||||
function throwAssocError(entity, association, msg) {
|
||||
throw new Error(`Error with association "${association.name || association}" of entity "${entity.name || entity}": ${msg}`);
|
||||
}
|
||||
|
||||
function getEntity(absoluteName) {
|
||||
const [serviceName, entityName] = absoluteName.split(".");
|
||||
return cds.services[serviceName]?.entities[entityName] || throwError(`Unknown entity "${absoluteName}"`);
|
||||
}
|
||||
|
||||
|
||||
class RemoteHandler {
|
||||
constructor(service, remoteEntities) {
|
||||
|
||||
this.service = service;
|
||||
this.remoteEntities = remoteEntities;
|
||||
}
|
||||
|
||||
serviceFor(entityName) {
|
||||
return this.remoteEntities[entityName] || cds.db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand "to one" associations with a single key field
|
||||
*
|
||||
* @param {*} req
|
||||
* @param {*} next
|
||||
* @param {*} associationName
|
||||
* @param {*} targetService
|
||||
* @param {*} headers
|
||||
* @returns
|
||||
*/
|
||||
async mixinExpand(req, result, expand) {
|
||||
const associationName = expand.ref[0];
|
||||
|
||||
// Get association target
|
||||
const {keyFieldName, targetKeyFieldName, target, is2many, is2one} =
|
||||
this.association(req.target, associationName);
|
||||
|
||||
// Request all associated entities
|
||||
// REVISIT: Still needed?
|
||||
//const mock = !cds.env.requires.API_BUSINESS_PARTNER.credentials;
|
||||
//const tx = mock ? BupaService.tx(req) : BupaService;
|
||||
let ids = [];
|
||||
if (Array.isArray(result)) {
|
||||
ids = result.map((entry) => entry[keyFieldName]);
|
||||
} else {
|
||||
ids = [result[keyFieldName]];
|
||||
}
|
||||
|
||||
// Take over columns from original query
|
||||
const expandColumns = expand.expand.map((entry) => entry.ref[0]);
|
||||
if (expandColumns.indexOf(targetKeyFieldName) < 0)
|
||||
expandColumns.push(targetKeyFieldName);
|
||||
|
||||
const targetService = this.serviceFor(target.name);
|
||||
|
||||
// Select target
|
||||
// REVISIT: const targetResult = await targetService.read(target.name).where({ [targetKeyFieldName]: ids }).columns(expandColumns);
|
||||
const targetQuery = SELECT.from(target.name)
|
||||
.where({ [targetKeyFieldName]: ids })
|
||||
.columns(expandColumns);
|
||||
const targetResult = await targetService.run(targetQuery);
|
||||
|
||||
let targetResultMap;
|
||||
|
||||
if (is2one) {
|
||||
targetResultMap = this.mixinExpand_to_1(targetResult, targetKeyFieldName);
|
||||
} else if (is2many) {
|
||||
targetResultMap = this.mixinExpand_to_many(targetResult, targetKeyFieldName);
|
||||
} else {
|
||||
throwAssocError(req.target, associationName, `Unsupported cardinality.`);
|
||||
}
|
||||
|
||||
const resultArray = Array.isArray(result) ? result : [ result ];
|
||||
for (const entry of resultArray) {
|
||||
const id = entry[keyFieldName];
|
||||
const targetEntry = targetResultMap[id];
|
||||
if (targetEntry) entry[associationName] = targetEntry;
|
||||
}
|
||||
}
|
||||
|
||||
mixinExpand_to_1(targetResult, targetKeyFieldName) {
|
||||
const targetResultMap = {};
|
||||
for (const targetEntry of targetResult) {
|
||||
const id = targetEntry[targetKeyFieldName];
|
||||
targetResultMap[id] = targetEntry;
|
||||
}
|
||||
|
||||
return targetResultMap;
|
||||
}
|
||||
|
||||
mixinExpand_to_many(targetResult, targetKeyFieldName) {
|
||||
const targetResultMap = {};
|
||||
for (const targetEntry of targetResult) {
|
||||
const id = targetEntry[targetKeyFieldName];
|
||||
if (!targetResultMap[id]) targetResultMap[id] = [];
|
||||
targetResultMap[id].push(targetEntry);
|
||||
}
|
||||
|
||||
return targetResultMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @example
|
||||
* Notes(24B58115-E394-423B-BEAB-53419A32B927)/supplier
|
||||
*
|
||||
* -->
|
||||
* { SELECT: { from: { ref: {[
|
||||
* [ id: 'NotesService.Notes',
|
||||
* where: [
|
||||
* ref: [ 'ID' ],
|
||||
* '=',
|
||||
* val: ''545A3CF9-84CF-46C8-93DC-E29F0F2BC6BE'
|
||||
* ],
|
||||
* ],
|
||||
* [ 'supplier' ]
|
||||
* ]}}}
|
||||
*
|
||||
*
|
||||
* @param {*} req
|
||||
* @param {*} next
|
||||
* @returns
|
||||
*/
|
||||
async resolveNavigation(req, next) {
|
||||
const select = req.query.SELECT;
|
||||
if (select.from.ref.length !== 2) {
|
||||
throw new Error(
|
||||
`Unsupported navigation query with different than 2 entities in FROM clause.`
|
||||
);
|
||||
}
|
||||
|
||||
const entityName = select.from.ref[0].id || throwError(`Missing source entity name for navigation`);
|
||||
const entity = getEntity(entityName);
|
||||
const associationName = select.from.ref[1] || throwError(`Missing association name for navigation`);
|
||||
|
||||
const {keyFieldName, targetKeyFieldName, target, is2many, is2one} = this.association(entity, associationName);
|
||||
|
||||
const sourceService = this.serviceFor(entityName);
|
||||
const targetService = this.serviceFor(target.name);
|
||||
|
||||
if (sourceService === targetService) return await next();
|
||||
|
||||
// REVISIT: How to call service datasource w/o handlers
|
||||
const selectEntry = SELECT.one([keyFieldName])
|
||||
.from(entityName)
|
||||
.where(select.from.ref[0].where);
|
||||
const entry = await sourceService.run(selectEntry);
|
||||
|
||||
// REVISIT: How to call service datasource w/o handlers
|
||||
// REVISIT: const result = await targetService.read(target).columns(req.query.SELECT.columns).where({ [targetKeyFieldName]: entry[keyFieldName] });
|
||||
const selectTarget = SELECT(req.query.SELECT.columns)
|
||||
.from(target)
|
||||
.where({ [targetKeyFieldName]: entry[keyFieldName] });
|
||||
const result = await targetService.run(selectTarget);
|
||||
if (is2many) {
|
||||
return result;
|
||||
} else if (is2one) {
|
||||
return result?.[0];
|
||||
} else {
|
||||
throw new Error('Unsupported association cardinality');
|
||||
}
|
||||
}
|
||||
|
||||
async handle(req, next) {
|
||||
let doRequest;
|
||||
|
||||
if (
|
||||
req.query.SELECT.from.ref.length > 1 &&
|
||||
req.target.name !== req.query.SELECT.from.ref[0]
|
||||
) {
|
||||
doRequest = () => this.resolveNavigation(req, next)
|
||||
} else {
|
||||
doRequest = this.isRemote(req.target.name) ?
|
||||
() => this.serviceFor(req.target.name).run(req.query) : next;
|
||||
}
|
||||
|
||||
return this.resolveExpands(req, doRequest);
|
||||
}
|
||||
|
||||
isRemote(entityName) {
|
||||
return this.serviceFor(entityName) !== cds.db;
|
||||
}
|
||||
|
||||
isSeparated(entityNameA, entityNameB) {
|
||||
return this.serviceFor(entityNameA) !== this.serviceFor(entityNameB);
|
||||
}
|
||||
|
||||
async resolveExpands(req, next) {
|
||||
const select = req.query.SELECT;
|
||||
const expandFilter = (column) => {
|
||||
if (!column.expand) return false;
|
||||
const associationName = column.ref[0];
|
||||
|
||||
return this.isSeparated(req.target.name, req.target.associations[associationName].target);
|
||||
};
|
||||
|
||||
const expands = select.columns.filter(expandFilter);
|
||||
select.columns = select.columns.filter((column) => !expandFilter(column));
|
||||
|
||||
if (expands.length === 0) return next();
|
||||
|
||||
const temporaryKeyFieldNames = [];
|
||||
for (const expand of expands) {
|
||||
const associationName = expand.ref[0];
|
||||
const {keyFieldName} = this.association(req.target, associationName);
|
||||
|
||||
// Make sure id property is contained in select
|
||||
if (
|
||||
!select.columns.find((column) =>
|
||||
column.ref.find((ref) => ref == keyFieldName)
|
||||
)
|
||||
) {
|
||||
select.columns.push({ ref: keyFieldName });
|
||||
temporaryKeyFieldNames.push(keyFieldName);
|
||||
}
|
||||
}
|
||||
|
||||
// Call service implementation
|
||||
const result = await next();
|
||||
|
||||
await Promise.all(
|
||||
expands.map((expand) => this.mixinExpand(req, result, expand))
|
||||
);
|
||||
|
||||
if (temporaryKeyFieldNames.length > 0) {
|
||||
for (const entry of result) {
|
||||
for (const name of temporaryKeyFieldNames)
|
||||
delete entry[name];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
association(entity, associationName, recursion = 0) {
|
||||
let associationMetaData;
|
||||
|
||||
if (++recursion > 2) throwAssocError(entity, association, "Association has recursive definition.");
|
||||
|
||||
const association = entity.associations[associationName] || throwAssocError(entity, associationName, `Association does not exists`);
|
||||
|
||||
associationMetaData = this.associationKey(entity, association);
|
||||
if (!associationMetaData) {
|
||||
associationMetaData = this.associationOn(entity, association);
|
||||
}
|
||||
if (!associationMetaData) {
|
||||
associationMetaData = this.associationOnSelf(entity, association, recursion);
|
||||
}
|
||||
|
||||
if (!association) throwAssocError(entity, association, "Only associations with one key field or on conidition with one field are supported.");
|
||||
|
||||
associationMetaData.is2many = association.is2many;
|
||||
associationMetaData.is2one = association.is2one;
|
||||
associationMetaData.entity = entity;
|
||||
associationMetaData.target = getEntity(association.target);
|
||||
return associationMetaData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Association with "on" condition
|
||||
*
|
||||
* @example
|
||||
* entity Notes {
|
||||
* supplier_ID : Suppliers:ID;
|
||||
* supplier: Association to Suppliers on supplier.ID = supplier_ID;
|
||||
* }
|
||||
*
|
||||
* -->
|
||||
*
|
||||
* { association: { on: { [
|
||||
* ref: [ 'supplier', 'ID' ], // <association>.<target-field>
|
||||
* '=',
|
||||
* ref: [ 'supplier_ID' ] // <source-field>
|
||||
* ] }}}
|
||||
*
|
||||
* @param {*} entity
|
||||
* @param {*} association
|
||||
* @returns
|
||||
*/
|
||||
associationOn(entity, association) {
|
||||
const onLength = association.on?.length ?? 0;
|
||||
if (onLength === 0) return;
|
||||
|
||||
const on = association.on;
|
||||
if (!(onLength === 3 && on[0]?.ref?.[0] === association.name && on[1] === "=" && on[2]?.ref[0] !== "$self")) return; //throwAssocError(entity, association, "Association on condition must compare to $self");
|
||||
|
||||
return {
|
||||
keyFieldName: fixColumnName(entity, association.on[2].ref[0]),
|
||||
targetKeyFieldName: association.on[0].ref.slice(1).join("_")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @example
|
||||
* extend entity BusinessPartner {
|
||||
* notes: Composition of many Notes on notes = $self;
|
||||
* }
|
||||
*
|
||||
* @param {*} entity
|
||||
* @param {*} association
|
||||
* @returns
|
||||
*/
|
||||
|
||||
associationOnSelf(entity, association, recursion) {
|
||||
const onLength = association.on?.length ?? 0;
|
||||
if (onLength === 0) return;
|
||||
|
||||
const on = association.on;
|
||||
if (!(onLength === 3 && on[0]?.ref && on[1] === "=" && on[2]?.ref[0] === "$self")) return; //throwAssocError(entity, association, "Association on condition must compare to $self");
|
||||
|
||||
const reverseAssociationName = association.on[0].ref[1];
|
||||
const reverseAssociationMetaData = this.association(targetEntity, reverseAssociationName, false);
|
||||
|
||||
return {
|
||||
keyFieldName: reverseAssociationMetaData.targetKeyFieldName,
|
||||
targetKeyFieldName: reverseAssociationMetaData.keyFieldName
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Association with keys
|
||||
*
|
||||
* @example
|
||||
* entity Notes {
|
||||
* supplier: Association to Suppliers;
|
||||
* }
|
||||
*
|
||||
* -->
|
||||
*
|
||||
* { association: keys: [ {
|
||||
* $generatedFieldName: 'supplier_ID',
|
||||
* ref: [ 'ID' ]
|
||||
* } ]
|
||||
* }
|
||||
*
|
||||
* @param {*} entity
|
||||
* @param {*} association
|
||||
* @returns
|
||||
*/
|
||||
associationKey(entity, association) {
|
||||
const keyLength = association.keys?.length ?? 0;
|
||||
if (keyLength === 0) return;
|
||||
if (keyLength > 1) throwAssocError(entity, association, `Association has ${keyLength} key fields, but only 1 is supported.`);
|
||||
const key = association.keys[0];
|
||||
|
||||
return {
|
||||
keyFieldName: key["$generatedFieldName"] || throwError(entity, association, "Missing $generatedFieldName"),
|
||||
targetKeyFieldName: key.ref[0] || throwError(entity, association, "Missing key ref")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RemoteHandler.columnNameFixes = {};
|
||||
module.exports = RemoteHandler;
|
||||
2847
notes/srv/external/API_BUSINESS_PARTNER.csn
vendored
2847
notes/srv/external/API_BUSINESS_PARTNER.csn
vendored
File diff suppressed because it is too large
Load Diff
6614
notes/srv/external/API_BUSINESS_PARTNER.edmx
vendored
6614
notes/srv/external/API_BUSINESS_PARTNER.edmx
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
||||
BusinessPartner;BusinessPartnerFullName;BusinessPartnerType
|
||||
11;Alice Wonder;CUSTOMER;
|
||||
9980000082;Hugo Hollandaise;CUSTOMER
|
||||
|
@@ -1,13 +0,0 @@
|
||||
using sap.capire.notes from '../db/data-model';
|
||||
using { Suppliers as MashupSuppliers } from '../db/mashup';
|
||||
using { API_BUSINESS_PARTNER as BusinessPartner } from './external/API_BUSINESS_PARTNER.csn';
|
||||
|
||||
/**
|
||||
* Notes Service
|
||||
*
|
||||
* Maintain notes for suppliers
|
||||
*/
|
||||
service NotesService {
|
||||
entity Notes as projection on notes.Notes;
|
||||
entity Suppliers as projection on MashupSuppliers;
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
const cds = require("@sap/cds");
|
||||
|
||||
const RemoteHandler = require('./RemoteHandler');
|
||||
|
||||
const s4apiKey = process.env.S4_APIKEY;
|
||||
if (!s4apiKey && cds.env.profiles.indexOf("sandbox") >= 0) {
|
||||
console.error(
|
||||
"[ERROR] Provide API Key in env var S4_APIKEY for S/4 Sandbox: https://api.sap.com/api/API_BUSINESS_PARTNER/resource -> Show API Key"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
module.exports = cds.service.impl(async function () {
|
||||
const { Notes, Suppliers } = this.entities;
|
||||
|
||||
const bpService = await cds.connect.to("API_BUSINESS_PARTNER");
|
||||
|
||||
// TODO: This seems to be a bug in the compiler
|
||||
RemoteHandler.columnNameFixes = { ["NotesService.Suppliers.BusinessPartner"]: "ID" };
|
||||
|
||||
// REVISIT: This is a workaround for the missing capability to add headers to the service
|
||||
const bpServiceDelegate = {
|
||||
run: query => bpService.send({ query, headers: { APIKey: s4apiKey } })
|
||||
};
|
||||
|
||||
const remoteHandler = new RemoteHandler(this, { [Suppliers.name]: bpServiceDelegate });
|
||||
|
||||
this.on("READ", Suppliers, (req, next) => remoteHandler.handle(req, next) );
|
||||
this.on("READ", Notes, (req, next) => remoteHandler.handle(req, next) );
|
||||
|
||||
|
||||
/*
|
||||
// Suppliers?$expand=notes
|
||||
this.on("READ", Suppliers, async (req, next) => {
|
||||
const expandIndex = req.query.SELECT.columns.findIndex(
|
||||
({ expand, ref }) => expand && ref[0] === "notes"
|
||||
);
|
||||
if (expandIndex < 0) return next();
|
||||
|
||||
req.query.SELECT.columns.splice(expandIndex, 1);
|
||||
if (!req.query.SELECT.columns.find(
|
||||
column => column.ref.find((ref) => ref == "ID"))
|
||||
)
|
||||
req.query.SELECT.columns.push({ ref: ["ID"] });
|
||||
|
||||
const suppliers = await next();
|
||||
|
||||
// Request all associated notes
|
||||
const supplierIds = suppliers.map((supplier) => supplier.ID);
|
||||
const notes = await this.run(SELECT.from(Notes).where({ supplier_ID: supplierIds }));
|
||||
|
||||
// Convert in a map for easier lookup
|
||||
const notesForSuppliers = {};
|
||||
for (const note of notes) {
|
||||
if (!notesForSuppliers[note.supplier_ID]) notesForSuppliers[note.supplier_ID] = [];
|
||||
notesForSuppliers[note.supplier_ID].push(note);
|
||||
}
|
||||
|
||||
// Add notes to result
|
||||
for (const supplier of suppliers) {
|
||||
const notesForSupplier = notesForSuppliers[supplier.ID];
|
||||
if (notesForSupplier) supplier.notes = notesForSupplier;
|
||||
}
|
||||
|
||||
return suppliers;
|
||||
});
|
||||
|
||||
|
||||
this.on("READ", Suppliers, async req => {
|
||||
return bpServiceDelegate.run(req.query);
|
||||
});
|
||||
|
||||
*/
|
||||
|
||||
});
|
||||
@@ -12,8 +12,7 @@
|
||||
"@capire/media": "./media",
|
||||
"@capire/orders": "./orders",
|
||||
"@capire/reviews": "./reviews",
|
||||
"@capire/suppliers": "./suppliers",
|
||||
"@capire/notes": "./notes"
|
||||
"@capire/suppliers": "./suppliers"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.2.0",
|
||||
|
||||
Reference in New Issue
Block a user