Add suppliers notes app
This commit is contained in:
1
notes/.cdsrc.json
Normal file
1
notes/.cdsrc.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
24
notes/.eslintrc
Normal file
24
notes/.eslintrc
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"extends": "eslint:recommended",
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"es6": true,
|
||||||
|
"jest": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2017
|
||||||
|
},
|
||||||
|
"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
Normal file
29
notes/.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# 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
|
||||||
29
notes/README.md
Normal file
29
notes/README.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# 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`
|
||||||
81
notes/assets/NotesService.drawio
Normal file
81
notes/assets/NotesService.drawio
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<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>
|
||||||
BIN
notes/assets/NotesService.png
Normal file
BIN
notes/assets/NotesService.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 210 KiB |
8
notes/db/data-model.cds
Normal file
8
notes/db/data-model.cds
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace sap.capire.notes;
|
||||||
|
|
||||||
|
using { cuid } from '@sap/cds/common';
|
||||||
|
|
||||||
|
entity Notes: cuid {
|
||||||
|
note: String;
|
||||||
|
}
|
||||||
|
|
||||||
4
notes/db/data/sap.capire.notes-Notes.csv
Normal file
4
notes/db/data/sap.capire.notes-Notes.csv
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
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
|
||||||
|
33
notes/db/mashup.cds
Normal file
33
notes/db/mashup.cds
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
136
notes/db/src/.hdiconfig
Normal file
136
notes/db/src/.hdiconfig
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
notes/mta.yaml
Normal file
58
notes/mta.yaml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
## 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}
|
||||||
|
|
||||||
|
|
||||||
33
notes/package.json
Normal file
33
notes/package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"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/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
notes/requests.http
Normal file
16
notes/requests.http
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
### 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!"
|
||||||
|
}
|
||||||
2847
notes/srv/external/API_BUSINESS_PARTNER.csn
vendored
Normal file
2847
notes/srv/external/API_BUSINESS_PARTNER.csn
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6614
notes/srv/external/API_BUSINESS_PARTNER.edmx
vendored
Normal file
6614
notes/srv/external/API_BUSINESS_PARTNER.edmx
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3
notes/srv/external/data/API_BUSINESS_PARTNER-A_BusinessPartner.csv
vendored
Normal file
3
notes/srv/external/data/API_BUSINESS_PARTNER-A_BusinessPartner.csv
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
BusinessPartner;BusinessPartnerFullName;BusinessPartnerType
|
||||||
|
11;Alice Wonder;CUSTOMER;
|
||||||
|
9980000082;Hugo Hollandaise;CUSTOMER
|
||||||
|
13
notes/srv/notes-service.cds
Normal file
13
notes/srv/notes-service.cds
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
61
notes/srv/notes-service.js
Normal file
61
notes/srv/notes-service.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
const cds = require("@sap/cds");
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
// 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 } })
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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,7 +12,8 @@
|
|||||||
"@capire/media": "./media",
|
"@capire/media": "./media",
|
||||||
"@capire/orders": "./orders",
|
"@capire/orders": "./orders",
|
||||||
"@capire/reviews": "./reviews",
|
"@capire/reviews": "./reviews",
|
||||||
"@capire/suppliers": "./suppliers"
|
"@capire/suppliers": "./suppliers",
|
||||||
|
"@capire/notes": "./notes"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
|
|||||||
176
test/notes.test.js
Normal file
176
test/notes.test.js
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
const cds = require("@sap/cds/lib");
|
||||||
|
const express = require("express");
|
||||||
|
if (cds.User.default) cds.User.default = cds.User.Privileged;
|
||||||
|
// hard core monkey patch
|
||||||
|
else cds.User = cds.User.Privileged; // hard core monkey patch for older cds releases
|
||||||
|
|
||||||
|
process.env.S4_APIKEY = "-";
|
||||||
|
|
||||||
|
|
||||||
|
const BPs = {
|
||||||
|
"@odata.context": "$metadata#Suppliers",
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
BusinessPartner: "11",
|
||||||
|
BusinessPartnerFullName: "Alice Wonder",
|
||||||
|
BusinessPartnerType: "CUSTOMER",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BusinessPartner: "9980000082",
|
||||||
|
BusinessPartnerFullName: "Hugo Hollandaise",
|
||||||
|
BusinessPartnerType: "CUSTOMER",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const Suppliers = {
|
||||||
|
"@odata.context": "$metadata#Suppliers",
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
ID: "11",
|
||||||
|
fullName: "Alice Wonder",
|
||||||
|
customerType: "CUSTOMER",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "9980000082",
|
||||||
|
fullName: "Hugo Hollandaise",
|
||||||
|
customerType: "CUSTOMER",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const SuppliersExpandNotes = {
|
||||||
|
"@odata.context": "$metadata#Suppliers",
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
ID: "11",
|
||||||
|
fullName: "Alice Wonder",
|
||||||
|
customerType: "CUSTOMER",
|
||||||
|
notes: [
|
||||||
|
{
|
||||||
|
ID: "545A3CF9-84CF-46C8-93DC-E29F0F2BC6BE",
|
||||||
|
note: "note2 for 11",
|
||||||
|
supplier_ID: "11",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "D632D4EE-E772-454A-913E-26A7B8DAA7FB",
|
||||||
|
note: "note1 for 11",
|
||||||
|
supplier_ID: "11",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "9980000082",
|
||||||
|
fullName: "Hugo Hollandaise",
|
||||||
|
customerType: "CUSTOMER",
|
||||||
|
notes: [
|
||||||
|
{
|
||||||
|
ID: "24B58115-E394-423B-BEAB-53419A32B927",
|
||||||
|
note: "note3",
|
||||||
|
supplier_ID: "9980000082",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
class MockServer {
|
||||||
|
async start() {
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
this.app = express();
|
||||||
|
this.server = http.createServer(this.app).listen();
|
||||||
|
this.app.set('port', this.server.address().port);
|
||||||
|
|
||||||
|
this.app.get("*", (req, res) => {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end(JSON.stringify(BPs));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
url() {
|
||||||
|
return `http://localhost:${this.server.address().port}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.server.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Notes", () => {
|
||||||
|
const mockServer = new MockServer();
|
||||||
|
|
||||||
|
before( async () => {
|
||||||
|
mockServer.start();
|
||||||
|
|
||||||
|
cds.env.add({
|
||||||
|
requires: {
|
||||||
|
API_BUSINESS_PARTNER: {
|
||||||
|
kind: "odata",
|
||||||
|
model: "srv/external/API_BUSINESS_PARTNER",
|
||||||
|
credentials: {
|
||||||
|
url: mockServer.url()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const { expect, GET, PATCH } = require(".").run(
|
||||||
|
"serve",
|
||||||
|
"--project",
|
||||||
|
"notes",
|
||||||
|
"--with-mocks",
|
||||||
|
"--in-memory"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
it("get notes", async () => {
|
||||||
|
const { status, data } = await GET("/notes/Notes");
|
||||||
|
|
||||||
|
expect({ status, data }).to.containSubset({
|
||||||
|
status: 200,
|
||||||
|
data: {
|
||||||
|
"@odata.context": "$metadata#Notes",
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
ID: "24B58115-E394-423B-BEAB-53419A32B927",
|
||||||
|
note: "note3",
|
||||||
|
supplier_ID: "9980000082",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "545A3CF9-84CF-46C8-93DC-E29F0F2BC6BE",
|
||||||
|
note: "note2 for 11",
|
||||||
|
supplier_ID: "11",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "D632D4EE-E772-454A-913E-26A7B8DAA7FB",
|
||||||
|
note: "note1 for 11",
|
||||||
|
supplier_ID: "11",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("get remote suppliers", async () => {
|
||||||
|
const { status, data } = await GET("/notes/Suppliers");
|
||||||
|
|
||||||
|
expect({ status, data }).to.containSubset({
|
||||||
|
status: 200,
|
||||||
|
data: Suppliers,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("get remote suppliers with notes", async () => {
|
||||||
|
const { status, data } = await GET("/notes/Suppliers?$expand=notes");
|
||||||
|
|
||||||
|
expect({ status, data }).to.containSubset({
|
||||||
|
status: 200,
|
||||||
|
data: SuppliersExpandNotes,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => mockServer.close());
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user