diff --git a/.vscode/settings.json b/.vscode/settings.json index 7431ebe8..1ca4aeaf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,5 +13,5 @@ "**/cds/lib/req/cls.js", "**/odata-v4/okra/**" ] - }, + } } diff --git a/README.md b/README.md index 16ee56f1..ba71c072 100644 --- a/README.md +++ b/README.md @@ -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 \ No newline at end of file diff --git a/fiori/package.json b/fiori/package.json index 57985f96..76f51da1 100644 --- a/fiori/package.json +++ b/fiori/package.json @@ -7,7 +7,7 @@ "@capire/orders": "*", "@capire/reviews": "*", "@capire/suppliers": "*", - "@sap/cds": "^5", + "@sap/cds": ">=5", "express": "^4.17.1", "passport": "^0.4.1" }, diff --git a/suppliers/package.json b/suppliers/package.json index 10ce7031..9be35291 100644 --- a/suppliers/package.json +++ b/suppliers/package.json @@ -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": "" + } + } + }, + "[development]": { + "messaging": { + "kind": "file-based-messaging" + } } } } diff --git a/suppliers/requests.http b/suppliers/requests.http index eb4ff156..48ca0c1f 100644 --- a/suppliers/requests.http +++ b/suppliers/requests.http @@ -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}} diff --git a/suppliers/srv/external/API_BUSINESS_PARTNER.js b/suppliers/srv/external/API_BUSINESS_PARTNER.js index d092f316..0121884d 100644 --- a/suppliers/srv/external/API_BUSINESS_PARTNER.js +++ b/suppliers/srv/external/API_BUSINESS_PARTNER.js @@ -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 }); + }); }); diff --git a/suppliers/srv/external/data/sap.capire.bookshop-Books.csv b/suppliers/srv/external/data/sap.capire.bookshop-Books.csv new file mode 100644 index 00000000..d4e1fc64 --- /dev/null +++ b/suppliers/srv/external/data/sap.capire.bookshop-Books.csv @@ -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 \ No newline at end of file diff --git a/suppliers/srv/external/data/sap.capire.bookshop-Suppliers.csv b/suppliers/srv/external/data/sap.capire.bookshop-Suppliers.csv new file mode 100644 index 00000000..1096eb4a --- /dev/null +++ b/suppliers/srv/external/data/sap.capire.bookshop-Suppliers.csv @@ -0,0 +1,6 @@ +ID;name +ACME;A Company Making Everything (local) +B4U;Books for You +S&C;Shakespeare & Co. +WSL;Waterstones + diff --git a/suppliers/srv/mashup.cds b/suppliers/srv/mashup.cds index cb5751b5..3d2e9799 100644 --- a/suppliers/srv/mashup.cds +++ b/suppliers/srv/mashup.cds @@ -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; } } - diff --git a/suppliers/srv/mashup.js b/suppliers/srv/mashup.js index f2079922..3de8f285 100644 --- a/suppliers/srv/mashup.js +++ b/suppliers/srv/mashup.js @@ -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); } }