From eb4bc703dd29f19e384c8b991d4edfc909e8b59a Mon Sep 17 00:00:00 2001 From: Uwe Klinger Date: Mon, 3 May 2021 12:52:11 +0200 Subject: [PATCH] Improvements for Supplier replication --- suppliers/monkey-patch.js | 38 +++++++++++++++++++ suppliers/requests.http | 27 +++++++++++++ suppliers/server.js | 3 ++ .../srv/external/API_BUSINESS_PARTNER.js | 10 +++++ suppliers/srv/mashup.cds | 12 ++++++ suppliers/srv/mashup.js | 21 +++++++--- 6 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 suppliers/monkey-patch.js create mode 100644 suppliers/requests.http create mode 100644 suppliers/srv/external/API_BUSINESS_PARTNER.js diff --git a/suppliers/monkey-patch.js b/suppliers/monkey-patch.js new file mode 100644 index 00000000..5b24936a --- /dev/null +++ b/suppliers/monkey-patch.js @@ -0,0 +1,38 @@ +const deploy = require("@sap/cds/lib/deploy"); + +const DEBUG = (...args) => console.log(...args); + +deploy.exclude_external_entities_in = function (csn, _bound) { + // NOSONAR + for (let [each, { service = each, model, credentials }] of Object.entries( + cds.requires + )) { + if (!model) continue; //> not for internal services like cds.requires.odata + if (_bound && !credentials) continue; + DEBUG && DEBUG("excluding external entities for", service, "..."); + const prefix = service + "."; + for (let each in csn.definitions) { + const def = csn.definitions[each]; + if (def["@cds.persistence.table"] === true) continue; + if (each.startsWith(prefix)) { + DEBUG && DEBUG("excluding external entity", each); + _exclude(each); + } + } + } + return csn; + + function _exclude(each) { + const def = csn.definitions[each]; + if (def.kind !== "entity") return; + def["@cds.persistence.skip"] = true; + // propagate to all views... + for (let other in csn.definitions) { + const d = csn.definitions[other]; + // do not exclude replica table + if (d["@cds.persistence.table"] === true) continue; + const p = (d.query && d.query.SELECT) || d.projection; + if (p && p.from.ref && p.from.ref[0] === each) _exclude(other); + } + } +}; diff --git a/suppliers/requests.http b/suppliers/requests.http new file mode 100644 index 00000000..2572f89d --- /dev/null +++ b/suppliers/requests.http @@ -0,0 +1,27 @@ + +@server = http://localhost:4004 +@authAlice = Authorization: Basic alice: + +PUT {{server}}/api-business-partner/A_BusinessPartner('ACME') +Content-Type: application/json + +{ + "BusinessPartnerFullName": "Alice Changed" +} + +### + +PATCH {{server}}/admin/Books(201) +{{authAlice}} +Content-Type: application/json + +{ + "supplier_ID": "PNG" +} + + + +### + +GET {{server}}/admin/Books?$top=11&$expand=supplier +{{authAlice}} diff --git a/suppliers/server.js b/suppliers/server.js index 2e8cbb64..69bcb13c 100644 --- a/suppliers/server.js +++ b/suppliers/server.js @@ -1,3 +1,6 @@ const cds = require ('@sap/cds') + +require('./monkey-patch'); + cds.once('served', require('./srv/mashup')) module.exports = cds.server diff --git a/suppliers/srv/external/API_BUSINESS_PARTNER.js b/suppliers/srv/external/API_BUSINESS_PARTNER.js new file mode 100644 index 00000000..d3f696d4 --- /dev/null +++ b/suppliers/srv/external/API_BUSINESS_PARTNER.js @@ -0,0 +1,10 @@ +const cds = require('@sap/cds'); + +module.exports = cds.service.impl(function (srv) { + const { A_BusinessPartner } = this.entities; + + srv.after('UPDATE', A_BusinessPartner, data => { + console.log(`>>> BusinessPartner updated ${data.BusinessPartner}`); + srv.emit("BusinessPartners/Changed", { businessPartners: [ data.BusinessPartner ] }); + }); +}); diff --git a/suppliers/srv/mashup.cds b/suppliers/srv/mashup.cds index 3d80f0f6..6a445aba 100644 --- a/suppliers/srv/mashup.cds +++ b/suppliers/srv/mashup.cds @@ -16,10 +16,22 @@ extend service S4 with { // 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: 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 + // } } diff --git a/suppliers/srv/mashup.js b/suppliers/srv/mashup.js index a77b238c..98ac7773 100644 --- a/suppliers/srv/mashup.js +++ b/suppliers/srv/mashup.js @@ -23,13 +23,21 @@ 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') + + /* + // ERROR: Reference integrity is violated for association "supplier" + if (supplierId) return Promise.all ([ next(), async()=>{ + let replicated = await db.exists (Suppliers, supplierId) + if (!replicated) await replicate (supplierId, 'initial') }]) else return next() //> don't forget to pass down the interceptor stack + */ + + let replicated = await db.exists (Suppliers, supplierId); + if (!replicated) await replicate (supplierId, 'initial'); + return next(); }) }) @@ -47,7 +55,10 @@ module.exports = async()=>{ // called by server.js */ async function replicate (IDs,_initial) { if (!Array.isArray(IDs)) IDs = [ IDs ] - let suppliers = await S4bupa.read (Suppliers).where('ID in',IDs) + // TODO: Doesn't work when running in same process with mocked API_BUSINESS_PARTNER + + let suppliers = await S4bupa.read (Suppliers).where(...([[]].concat(IDs).reduce( (where, id, index ) => { where.push(`${index>1 ? "OR ":""}ID = `, id); return where }))); + //let suppliers = await S4bupa.read (Suppliers).where('ID in',IDs) 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)