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", "**/cds/lib/req/cls.js",
"**/odata-v4/okra/**" "**/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 ## 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. 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/orders": "*",
"@capire/reviews": "*", "@capire/reviews": "*",
"@capire/suppliers": "*", "@capire/suppliers": "*",
"@sap/cds": "^5", "@sap/cds": ">=5",
"express": "^4.17.1", "express": "^4.17.1",
"passport": "^0.4.1" "passport": "^0.4.1"
}, },

View File

@@ -15,12 +15,19 @@
}, },
"cds": { "cds": {
"requires": { "requires": {
"db": {
"kind": "sql"
},
"API_BUSINESS_PARTNER": { "API_BUSINESS_PARTNER": {
"kind": "odata", "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 @server = http://localhost:4006
@bpServer = http://localhost:5001
################################################# @authAlice = Authorization: Basic alice:
#
# Suppliers Service (-> S/4)
#
PATCH {{S4bupa}}/A_BusinessPartner('ACME')
Content-Type: application/json
{ "FirstName":"ACME" }
### ###
### 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 () { module.exports = cds.service.impl(function () {
const { A_BusinessPartner } = this.entities; const { A_BusinessPartner } = this.entities;
this.after("UPDATE", A_BusinessPartner, (data, req) => // https://api.sap.com/event/SAPS4HANACloudBusinessEvents_BusinessPartner/resource
this.tx(req).emit("BusinessPartner.Changed", { this.after('UPDATE', A_BusinessPartner, async data => {
BusinessPartner: data.BusinessPartner 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. 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'; 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, key BusinessPartner as ID,
BusinessPartnerFullName as name, BusinessPartnerFullName as name,
// REVISIT: following is not supported so far in cds compiler... // REVISIT: following is not supported so far in cds compiler...
// to_BusinessPartnerAddress as city { // to_BusinessPartnerAddress as city {
// CityCode as code, // CityCode as code,
// CityName as name // CityName as name
// } // }
// to_BusinessPartnerAddress
// REVISIT: following is not supported so far in cqn2odata... // REVISIT: following is not supported so far in cqn2odata...
// to_BusinessPartnerAddress.CityCode as city, // to_BusinessPartnerAddress.CityCode as city,
// to_BusinessPartnerAddress.CityName as city_name, // 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 You can mashup entities from external services, or projections
thereof, with your project's own entities thereof, with your project's own entities
*/ */
using { sap.capire.bookshop.Books, CatalogService } from '@capire/bookshop'; using { sap.capire.bookshop.Books, CatalogService } from '@capire/bookshop';
extend Books with { 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. addressed to your services into calls to the external service.
*/ */
extend service AdminService with { 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 Having locally cached replicas also allows us to display supplier
data in lists of books, which otherwise would generate unwanted data in lists of books, which otherwise would generate unwanted
@@ -54,11 +77,10 @@ extend projection CatalogService.ListOfBooks with {
supplier.name as supplier 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 { extend service S4 {
@type: 'sap.s4.beh.businesspartner.v1.BusinessPartner.Changed.v1' @type: 'sap.s4.beh.businesspartner.v1.BusinessPartner.Changed.v1'
event BusinessPartner.Changed { 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 const db = await cds.connect.to('db') //> our primary database
// Reflect CDS definition of the Suppliers entity // 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 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 // 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 // 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) const replicateIfNotExists = async()=>{
if (!replicated) await replicate (supplier, 'initial') 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 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 // 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 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}) await new Promise( resolve => setTimeout( resolve, 1000 ));
if (replica) return replicate (replica.ID) 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. * 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 * @param {truthy|falsy} _initial indicates whether an insert or an update is required
*/ */
async function replicate (IDs,_initial) { async function replicate (id,_initial) {
if (!Array.isArray(IDs)) IDs = [ IDs ] // TODO: Doesn't work when running in same process with mocked API_BUSINESS_PARTNER
let suppliers = await S4bupa.read (Suppliers).where('ID in',IDs)
// 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 if (_initial) return db.insert (suppliers) .into (Suppliers) //> using bulk insert
else return Promise.all(suppliers.map ( //> parallelizing updates else return db.update(Suppliers,id) .with (suppliers[0]);
each => db.update (Suppliers,each.ID) .with (each) }
))
// 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);
} }
} }