This commit is contained in:
Daniel
2021-06-09 10:22:30 +02:00
10 changed files with 185 additions and 52 deletions

View File

@@ -13,5 +13,5 @@
"**/cds/lib/req/cls.js",
"**/odata-v4/okra/**"
]
},
}
}

View File

@@ -84,3 +84,35 @@ In case you've a question, find a bug, or otherwise need support, use our [commu
## License
Copyright (c) 2021 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, version 2.0 except as noted otherwise in the [LICENSE](LICENSE.txt) file.
# Suppliers - in progress for Messaging & Service Consumption -
## TODOs
1. Fix issues when running in same process
2. Automated tests
## Usage
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 `suppliers/requests.http`
## Request Sequence
* TODO
## URLs
* Get books with their replicated supplier: http://localhost:4004/browse/Books?$expand=supplier
* Get remote suppliers: http://localhost:4004/admin/Suppliers?$top=11

View File

@@ -7,7 +7,7 @@
"@capire/orders": "*",
"@capire/reviews": "*",
"@capire/suppliers": "*",
"@sap/cds": "^5",
"@sap/cds": ">=5",
"express": "^4.17.1",
"passport": "^0.4.1"
},

View File

@@ -15,12 +15,19 @@
},
"cds": {
"requires": {
"db": {
"kind": "sql"
},
"API_BUSINESS_PARTNER": {
"kind": "odata",
"model": "srv/external/API_BUSINESS_PARTNER"
"model": "srv/external/API_BUSINESS_PARTNER",
"[with-destination]": {
"credentials": {
"destination": "<change to destination>"
}
}
},
"[development]": {
"messaging": {
"kind": "file-based-messaging"
}
}
}
}

View File

@@ -1,14 +1,51 @@
@S4bupa = http://localhost:4006/api-business-partner
#################################################
#
# Suppliers Service (-> S/4)
#
PATCH {{S4bupa}}/A_BusinessPartner('ACME')
Content-Type: application/json
{ "FirstName":"ACME" }
@server = http://localhost:4006
@bpServer = http://localhost:5001
@authAlice = Authorization: Basic alice:
###
### Replication on changed Business Partner
###
### 1. Check supplier name before update ("A Company Making Everything")
GET {{server}}/admin/Books(299)?$expand=supplier
{{authAlice}}
### 2. Change Business Partner -> Triggers event that updates supplier replication
PATCH {{bpServer}}/api-business-partner/A_BusinessPartner('ACME')
Content-Type: application/json
{
"BusinessPartnerFullName": "A Company Making Everything *better*"
}
### 3. Check supplier name after update ("A Company Making Everything *better*")
GET {{server}}/admin/Books(299)?$expand=supplier
{{authAlice}}
###
### Replication on new assigned supplier
###
### 1. No supplier is assigned to "Wuthering Heights"
GET {{server}}/admin/Books(201)?$expand=supplier
{{authAlice}}
### 2. Assign supplier ID "PNG"
PATCH {{server}}/admin/Books(201)
{{authAlice}}
Content-Type: application/json
{
"supplier_ID": "PNG"
}
### 3. Supplier information is replicated
GET {{server}}/admin/Books(201)?$expand=supplier
{{authAlice}}

View File

@@ -1,11 +1,10 @@
const cds = require("@sap/cds");
const cds = require('@sap/cds');
module.exports = cds.service.impl(function () {
const { A_BusinessPartner } = this.entities;
this.after("UPDATE", A_BusinessPartner, (data, req) =>
this.tx(req).emit("BusinessPartner.Changed", {
BusinessPartner: data.BusinessPartner
})
);
// https://api.sap.com/event/SAPS4HANACloudBusinessEvents_BusinessPartner/resource
this.after('UPDATE', A_BusinessPartner, async data => {
await this.emit("BusinessPartner.Changed", { BusinessPartner: data.BusinessPartner });
});
});

View File

@@ -0,0 +1,2 @@
ID;title;descr;author_ID;stock;price;currency_code;genre_ID;supplier_ID;
299;Mobby Dick;"""Moby-Dick""" or """The Whale""" is an 1851 novel by American writer Herman Melville. The book is the sailor Ishmael's narrative of the obsessive quest of Ahab, captain of the whaling ship Pequod, for revenge on Moby Dick, the giant white sperm whale that on the ship's previous voyage bit off Ahab's leg at the knee. A contribution to the literature of the American Renaissance, Moby-Dick was published to mixed reviews, was a commercial failure, and was out of print at the time of the author's death in 1891.;105;99;15.20;GBP;11;ACME
Can't render this file because it contains an unexpected character in line 2 and column 30.

View File

@@ -0,0 +1,6 @@
ID;name
ACME;A Company Making Everything (local)
B4U;Books for You
S&C;Shakespeare & Co.
WSL;Waterstones
1 ID name
2 ACME A Company Making Everything (local)
3 B4U Books for You
4 S&C Shakespeare & Co.
5 WSL Waterstones

View File

@@ -3,30 +3,59 @@
you actually want to use from there.
*/
using { sap.capire.bookshop as bookshop } from '@capire/bookshop';
using { API_BUSINESS_PARTNER as S4 } from './external/API_BUSINESS_PARTNER.csn';
extend service S4 with {
entity Suppliers as projection on S4.A_BusinessPartner {
@cds.autoexpose // or expose explicitly in Catalog and AdminService
@cds.persistence: {table,skip:false} // add persistency
entity sap.capire.bookshop.Suppliers as projection on S4.A_BusinessPartner {
// TODO: Aliases not supported in Java, yet?
key BusinessPartner as ID,
BusinessPartnerFullName as name,
// REVISIT: following is not supported so far in cds compiler...
// to_BusinessPartnerAddress as city {
// CityCode as code,
// CityName as name
// }
// to_BusinessPartnerAddress
// REVISIT: following is not supported so far in cqn2odata...
// to_BusinessPartnerAddress.CityCode as city,
// to_BusinessPartnerAddress.CityName as city_name,
}
//// REVISIT: Should this be here or in the service, when it is only used for Fiori?
//// Compositions should work as well
//// The version with "virtual" is prefered, as this makes clear that the association is "added" here
// virtual books: Association to Books on book.supplier = $self,
// books2: Association to Books on book.supplier = $self,
//// Add virtual field, that does'nt exisits in the persistence or the underlying service
// virtual saveEnabled: Boolean
} excluding {
OrganizationBPName1, OrganizationBPName2,OrganizationBPName3, OrganizationBPName4, to_BuPaIdentification, to_BuPaIndustry, to_BusinessPartnerAddress, to_BusinessPartnerBank, to_BusinessPartnerContact, to_BusinessPartnerRole, to_BusinessPartnerTax, to_Customer, to_Supplier
}
// REVISIT: Alternative idea to use a specific replication view, but request data from
// a different view and manual map values.
// entity ReplicatedSuppliers as projection on Suppliers {
// ID,
// name,
// to_BusinessPartnerAddress.CityCode as city,
// to_BusinessPartnerAddress.CityName as city_name
// }
/*
You can mashup entities from external services, or projections
thereof, with your project's own entities
*/
using { sap.capire.bookshop.Books, CatalogService } from '@capire/bookshop';
extend Books with {
supplier : Association to S4.Suppliers;
supplier : Association to bookshop.Suppliers;
}
@@ -36,15 +65,9 @@ extend Books with {
addressed to your services into calls to the external service.
*/
extend service AdminService with {
entity Suppliers as projection on S4.Suppliers;
entity Suppliers as projection on bookshop.Suppliers;
}
/*
Optionally add a local persistence to keep replicas of external
entities to have data in fast access locally; much like a cache.
*/
annotate S4.Suppliers with @cds.persistence:{table,skip:false};
/**
Having locally cached replicas also allows us to display supplier
data in lists of books, which otherwise would generate unwanted
@@ -54,11 +77,10 @@ extend projection CatalogService.ListOfBooks with {
supplier.name as supplier
}
// Extend S4 service with modeled event
// Extend S4 service with an event (events are not included in EDMX files)
extend service S4 {
@type: 'sap.s4.beh.businesspartner.v1.BusinessPartner.Changed.v1'
event BusinessPartner.Changed {
BusinessPartner: String(10);
BusinessPartner: S4.A_BusinessPartner:BusinessPartner;
}
}

View File

@@ -12,7 +12,7 @@ module.exports = async()=>{ // called by server.js
const db = await cds.connect.to('db') //> our primary database
// Reflect CDS definition of the Suppliers entity
const { Suppliers } = S4bupa.entities
const Suppliers = db.entities["sap.capire.bookshop.Suppliers"];
admin.prepend (()=>{ //> to ensure our .on handlers below go before the default ones
@@ -23,12 +23,15 @@ module.exports = async()=>{ // called by server.js
})
// Replicate Supplier data when edited Books have suppliers
admin.on (['CREATE','UPDATE'], 'Books', ({data:{supplier}}, next) => {
admin.on (['CREATE','UPDATE'], 'Books', async ({data:{supplier_ID: supplierId}}, next) => {
// Using Promise.all(...) to parallelize local write, i.e. next(), and replication
if (supplier) return Promise.all ([ next(), async()=>{
let replicated = await db.exists (Suppliers, supplier)
if (!replicated) await replicate (supplier, 'initial')
}])
const replicateIfNotExists = async()=>{
let replicated = await db.exists (Suppliers, supplierId);
if (!replicated) await replicate (supplierId, 'initial');
};
if (supplierId) return Promise.all ([ next(), replicateIfNotExists() ])
else return next() //> don't forget to pass down the interceptor stack
})
@@ -36,22 +39,47 @@ module.exports = async()=>{ // called by server.js
// Subscribe to changes in the S4 origin of Suppliers data
S4bupa.on ('BusinessPartner.Changed', async msg => { //> would be great if we had batch events from S/4
let replica = await SELECT.one('ID').from (Suppliers) .where ({ID: msg.data.BusinessPartner})
if (replica) return replicate (replica.ID)
await new Promise( resolve => setTimeout( resolve, 1000 ));
const id = msg.data.BusinessPartner;
let replica = await SELECT.one('ID').from (Suppliers) .where ('ID =', id);
if (replica) await replicate(id);
})
/**
* Helper function to replicate Suppliers data.
* @param {string|string[]} IDs a single ID or an array of IDs
* @param {string} a single ID
* @param {truthy|falsy} _initial indicates whether an insert or an update is required
*/
async function replicate (IDs,_initial) {
if (!Array.isArray(IDs)) IDs = [ IDs ]
let suppliers = await S4bupa.read (Suppliers).where('ID in',IDs)
async function replicate (id,_initial) {
// TODO: Doesn't work when running in same process with mocked API_BUSINESS_PARTNER
// TODO: Doesn't work because fields are not mapped back!
//let supplier = await S4bupa.run(SELECT.one('*').from(Suppliers).where('ID =',id));
let suppliers = await S4bupa.read(Suppliers).where('ID =',id);
if (_initial) return db.insert (suppliers) .into (Suppliers) //> using bulk insert
else return Promise.all(suppliers.map ( //> parallelizing updates
each => db.update (Suppliers,each.ID) .with (each)
))
else return db.update(Suppliers,id) .with (suppliers[0]);
}
// TODO: remove test code
{
// one server: returns AdminService.Suppliers
// two servers: returns API_BUSINESS_PARTNER.A_BusinessPartner
const tx = S4bupa.tx({});
let result = await tx.run(SELECT('*').from ('AdminService.Suppliers') .where ('ID =', 'ACME'));
tx.commit();
console.log(result);
}
// TODO: remove test code
{
// one server: returns AdminService.Suppliers
// two servers: returns AdminService.Suppliers
const tx = db.tx({});
let result = await db.run(SELECT('*').from ('AdminService.Suppliers') .where ('ID =', 'ACME'));
tx.commit();
console.log(result);
}
}