Implement generic expand and navigation feature
This commit is contained in:
252
notes/srv/RemoteHandler.js
Normal file
252
notes/srv/RemoteHandler.js
Normal file
@@ -0,0 +1,252 @@
|
||||
function getEntity(absoluteName) {
|
||||
const [serviceName, entityName] = absoluteName.split(".");
|
||||
return cds.services[serviceName].entities[entityName];
|
||||
}
|
||||
|
||||
function fixColumnName(entity, name) {
|
||||
const fullName = `${entity.name}.${name}`;
|
||||
return RemoteHandler.columnNameFixes[fullName] || name;
|
||||
}
|
||||
|
||||
function associationLink(entity, associationName) {
|
||||
const association = entity.associations[associationName];
|
||||
const cardinalityMax = association.cardinality && association.cardinality.max;
|
||||
if (!association)
|
||||
throw new Error(
|
||||
`Association "${associationName}" does not exists for entity "${entity.name}".`
|
||||
);
|
||||
if (association.keys) {
|
||||
return associationKey(entity, association);
|
||||
}
|
||||
|
||||
if (association.on && association.on.length === 3 && association.on[0].ref[0] === associationName && association.on[1] === "=" ) {
|
||||
const keyFieldName = fixColumnName(entity, association.on[2].ref[0]);
|
||||
const targetKeyFieldName = association.on[0].ref.slice(1).join("_");
|
||||
return [keyFieldName, targetKeyFieldName, association.target, cardinalityMax];
|
||||
}
|
||||
|
||||
if (association.on) {
|
||||
const { reverseAssociationName } = associationOn(entity, association);
|
||||
const targetEntity = getEntity(association.target);
|
||||
const reverseAssociation =
|
||||
targetEntity.associations[reverseAssociationName];
|
||||
const [targetKeyFieldName, keyFieldName] = associationKey(
|
||||
targetEntity,
|
||||
reverseAssociation
|
||||
);
|
||||
return [keyFieldName, targetKeyFieldName, association.target, cardinalityMax];
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Association "${associationName}" of entity "${entity.name}" has no "on" and no "keys".`
|
||||
);
|
||||
}
|
||||
|
||||
function associationOn(entity, association) {
|
||||
if (
|
||||
!association.on.length === 3 ||
|
||||
association.on[1] !== "=" ||
|
||||
association.on[2].ref[0] !== "$self"
|
||||
)
|
||||
throw new Error(
|
||||
`Association "${association.name}" for "${entity.name}" has not the expected form.`
|
||||
);
|
||||
|
||||
const reverseAssociationName = association.on[0].ref[1];
|
||||
const [targetServiceName, targetEntityName] = association.target.split(".");
|
||||
return { targetServiceName, targetEntityName, reverseAssociationName };
|
||||
}
|
||||
|
||||
function associationKey(entity, association) {
|
||||
const key = association.keys && association.keys[0];
|
||||
if (!key)
|
||||
throw new Error(
|
||||
`Association "${association.name}" for entity "${entity.name}" has no keys.`
|
||||
);
|
||||
return [key["$generatedFieldName"], key.ref[0], association.target, association.cardinality.max];
|
||||
}
|
||||
|
||||
class RemoteHandler {
|
||||
constructor(service, remoteEntities) {
|
||||
|
||||
this.service = service;
|
||||
this.remoteEntities = remoteEntities;
|
||||
}
|
||||
|
||||
serviceFor(entityName) {
|
||||
return this.remoteEntities[entityName] || this.service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand "to one" associations with a single key field
|
||||
*
|
||||
* @param {*} req
|
||||
* @param {*} next
|
||||
* @param {*} associationName
|
||||
* @param {*} targetService
|
||||
* @param {*} headers
|
||||
* @returns
|
||||
*/
|
||||
async mixinExpand(req, result, expand) {
|
||||
const associationName = expand.ref[0];
|
||||
|
||||
// Get association target
|
||||
const [keyFieldName, targetKeyFieldName, targetEntityName, cardinalityMax] =
|
||||
associationLink(req.target, associationName);
|
||||
|
||||
// Request all associated entities
|
||||
// REVISIT: Still needed?
|
||||
//const mock = !cds.env.requires.API_BUSINESS_PARTNER.credentials;
|
||||
//const tx = mock ? BupaService.tx(req) : BupaService;
|
||||
let ids = [];
|
||||
if (Array.isArray(result)) {
|
||||
ids = result.map((entry) => entry[keyFieldName]);
|
||||
} else {
|
||||
ids = [result[keyFieldName]];
|
||||
}
|
||||
|
||||
// Take over columns from original query
|
||||
const expandColumns = expand.expand.map((entry) => entry.ref[0]);
|
||||
if (expandColumns.indexOf(targetKeyFieldName) < 0)
|
||||
expandColumns.push(targetKeyFieldName);
|
||||
|
||||
const targetService = this.serviceFor(targetEntityName);
|
||||
|
||||
// Select target
|
||||
const targetQuery = SELECT.from(targetEntityName)
|
||||
.where({ [targetKeyFieldName]: ids })
|
||||
.columns(expandColumns);
|
||||
const targetResult = await targetService.run(targetQuery);
|
||||
|
||||
let targetResultMap;
|
||||
|
||||
switch (cardinalityMax) {
|
||||
case '1':
|
||||
targetResultMap = this.mixinExpand_to_1(targetResult, targetKeyFieldName);
|
||||
break;
|
||||
case '*':
|
||||
targetResultMap = this.mixinExpand_to_many(targetResult, targetKeyFieldName);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Association with cardinality may ${cardinalityMax} is not supported.`);
|
||||
}
|
||||
|
||||
const resultArray = Array.isArray(result) ? result : [ result ];
|
||||
for (const entry of resultArray) {
|
||||
const id = entry[keyFieldName];
|
||||
const targetEntry = targetResultMap[id];
|
||||
if (targetEntry) entry[associationName] = targetEntry;
|
||||
}
|
||||
}
|
||||
|
||||
mixinExpand_to_1(targetResult, targetKeyFieldName) {
|
||||
const targetResultMap = {};
|
||||
for (const targetEntry of targetResult) {
|
||||
const id = targetEntry[targetKeyFieldName];
|
||||
targetResultMap[id] = targetEntry;
|
||||
}
|
||||
|
||||
return targetResultMap;
|
||||
}
|
||||
|
||||
mixinExpand_to_many(targetResult, targetKeyFieldName) {
|
||||
const targetResultMap = {};
|
||||
for (const targetEntry of targetResult) {
|
||||
const id = targetEntry[targetKeyFieldName];
|
||||
if (!targetResultMap[id]) targetResultMap[id] = [];
|
||||
targetResultMap[id].push(targetEntry);
|
||||
}
|
||||
|
||||
return targetResultMap;
|
||||
}
|
||||
|
||||
async resolveNavigation(req, next) {
|
||||
const select = req.query.SELECT;
|
||||
if (select.from.ref.length !== 2) {
|
||||
throw new Error(
|
||||
`Unsupported navigation query with different than 2 entities in FROM clause.`
|
||||
);
|
||||
}
|
||||
|
||||
// Get target
|
||||
const entityName = select.from.ref[0].id;
|
||||
const entity = getEntity(entityName);
|
||||
|
||||
const [keyFieldName, targetKeyFieldName, targetEntityName] =
|
||||
associationLink(entity, select.from.ref[1]);
|
||||
|
||||
const sourceService = this.serviceFor(entityName);
|
||||
const targetService = this.serviceFor(targetEntityName);
|
||||
|
||||
const selectOne = SELECT.one([keyFieldName])
|
||||
.from(entityName)
|
||||
.where(select.from.ref[0].where);
|
||||
const entry = await sourceService.run(selectOne);
|
||||
|
||||
const selectTarget = SELECT(req.query.SELECT.columns)
|
||||
.from(targetEntityName)
|
||||
.where({ [targetKeyFieldName]: entry[keyFieldName] });
|
||||
return await targetService.run(selectTarget);
|
||||
}
|
||||
|
||||
async handle(req, next) {
|
||||
let doRequest;
|
||||
|
||||
if (
|
||||
req.query.SELECT.from.ref.length > 1 &&
|
||||
req.target.name !== req.query.SELECT.from.ref[0]
|
||||
) {
|
||||
doRequest = () => this.resolveNavigation(req, next)
|
||||
} else {
|
||||
const targetService = this.serviceFor(req.target.name);
|
||||
doRequest = targetService === this.service ?
|
||||
next : () => targetService.run(req.query)
|
||||
}
|
||||
|
||||
return this.resolveExpands(req, doRequest);
|
||||
}
|
||||
|
||||
async resolveExpands(req, next) {
|
||||
const select = req.query.SELECT;
|
||||
const expandFilter = (column) => {
|
||||
if (!column.expand) return false;
|
||||
const associationName = column.ref[0];
|
||||
const associationTargetName =
|
||||
req.target.associations[associationName].target;
|
||||
return (
|
||||
this.remoteEntities[associationTargetName] !==
|
||||
this.remoteEntities[req.target.name]
|
||||
);
|
||||
};
|
||||
|
||||
const expands = select.columns.filter(expandFilter);
|
||||
select.columns = select.columns.filter((column) => !expandFilter(column));
|
||||
|
||||
if (expands.length === 0) return next();
|
||||
|
||||
for (const expand of expands) {
|
||||
const associationName = expand.ref[0];
|
||||
const [keyFieldName] = associationLink(req.target, associationName);
|
||||
|
||||
// Make sure id property is contained in select
|
||||
if (
|
||||
!select.columns.find((column) =>
|
||||
column.ref.find((ref) => ref == keyFieldName)
|
||||
)
|
||||
)
|
||||
select.columns.push({ ref: keyFieldName });
|
||||
}
|
||||
|
||||
// Call service implementation
|
||||
const result = await next();
|
||||
|
||||
await Promise.all(
|
||||
expands.map((expand) => this.mixinExpand(req, result, expand))
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
RemoteHandler.columnNameFixes = {};
|
||||
module.exports = RemoteHandler;
|
||||
@@ -1,5 +1,7 @@
|
||||
const cds = require("@sap/cds");
|
||||
|
||||
const RemoteHandler = require('./RemoteHandler');
|
||||
|
||||
const s4apiKey = process.env.S4_APIKEY;
|
||||
if (!s4apiKey && cds.env.profiles.indexOf("sandbox") >= 0) {
|
||||
console.error(
|
||||
@@ -13,11 +15,21 @@ module.exports = cds.service.impl(async function () {
|
||||
|
||||
const bpService = await cds.connect.to("API_BUSINESS_PARTNER");
|
||||
|
||||
// TODO: This seems to be a bug in the compiler
|
||||
RemoteHandler.columnNameFixes = { ["NotesService.Suppliers.BusinessPartner"]: "ID" };
|
||||
|
||||
// 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 } })
|
||||
};
|
||||
|
||||
const remoteHandler = new RemoteHandler(this, { [Suppliers.name]: bpServiceDelegate });
|
||||
|
||||
this.on("READ", Suppliers, (req, next) => remoteHandler.handle(req, next) );
|
||||
this.on("READ", Notes, (req, next) => remoteHandler.handle(req, next) );
|
||||
|
||||
|
||||
/*
|
||||
// Suppliers?$expand=notes
|
||||
this.on("READ", Suppliers, async (req, next) => {
|
||||
const expandIndex = req.query.SELECT.columns.findIndex(
|
||||
@@ -58,4 +70,6 @@ module.exports = cds.service.impl(async function () {
|
||||
return bpServiceDelegate.run(req.query);
|
||||
});
|
||||
|
||||
*/
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user