Skip to content

Commit

Permalink
fix: Support capturing the Object ID and the value when there is a ne…
Browse files Browse the repository at this point in the history
…sted duplicated association name. (#58)

1. fix the issue that no value is returned for 'parent.parent.x' when
building hierachical entities with CAP compositions.
2. when the app creates both parent and child nodes, the parent node
does not exist in the data table, so the corresponding data needs to be
obtained from the draft table.
3. when updating a parent node and deleting a child node, the
corresponding node will disappear from the draft table. Therefore, it is
necessary to get the data from the data table.

This PR includes fixes for all three of these situations.

---------

Co-authored-by: I560824 <[email protected]>
  • Loading branch information
Sv7enNowitzki and Sv7enNowitzki authored Jan 24, 2024
1 parent a9d5671 commit 5b9b458
Show file tree
Hide file tree
Showing 22 changed files with 773 additions and 41 deletions.
12 changes: 6 additions & 6 deletions lib/change-log.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ const _getEntityIDs = function (txParams) {
* ...
* }
*/
const _formatAssociationContext = async function (changes) {
const _formatAssociationContext = async function (changes, reqData) {
for (const change of changes) {
const a = cds.model.definitions[change.serviceEntity].elements[change.attribute]
if (a?.type !== "cds.Association") continue
Expand All @@ -111,10 +111,10 @@ const _formatAssociationContext = async function (changes) {
SELECT.one.from(a.target).where({ [ID]: change.valueChangedTo })
])

const fromObjId = await getObjectId(a.target, semkeys, { curObjFromDbQuery: from || undefined }) // Note: ... || undefined is important for subsequent object destructuring with defaults
const fromObjId = await getObjectId(reqData, a.target, semkeys, { curObjFromDbQuery: from || undefined }) // Note: ... || undefined is important for subsequent object destructuring with defaults
if (fromObjId) change.valueChangedFrom = fromObjId

const toObjId = await getObjectId(a.target, semkeys, { curObjFromDbQuery: to || undefined }) // Note: ... || undefined is important for subsequent object destructuring with defaults
const toObjId = await getObjectId(reqData, a.target, semkeys, { curObjFromDbQuery: to || undefined }) // Note: ... || undefined is important for subsequent object destructuring with defaults
if (toObjId) change.valueChangedTo = toObjId

const isVLvA = a["@Common.ValueList.viaAssociation"]
Expand Down Expand Up @@ -219,7 +219,7 @@ const _getObjectIdByPath = async function (
const entityUUID = getUUIDFromPathVal(nodePathVal)
const obj = await getCurObjFromDbQuery(entityName, entityUUID)
const curObj = { curObjFromReqData, curObjFromDbQuery: obj }
return getObjectId(entityName, objIdElementNames, curObj)
return getObjectId(reqData, entityName, objIdElementNames, curObj)
}

const _formatObjectID = async function (changes, reqData) {
Expand Down Expand Up @@ -267,7 +267,7 @@ const _isCompositionContextPath = function (aPath) {

const _formatChangeLog = async function (changes, req) {
await _formatObjectID(changes, req.data)
await _formatAssociationContext(changes)
await _formatAssociationContext(changes, req.data)
await _formatCompositionContext(changes, req.data)
}

Expand Down Expand Up @@ -349,7 +349,7 @@ async function track_changes (req) {
let isComposition = _isCompositionContextPath(req.context.path)
let entityKey = diff.ID

if (req.event === "DELETE") {
if (cds.transaction(req).context.event === "DELETE") {
if (isDraftEnabled || !isComposition) {
return await DELETE.from(`sap.changelog.ChangeLog`).where({ entityKey })
}
Expand Down
60 changes: 55 additions & 5 deletions lib/entity-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const getCurObjFromReqData = function (reqData, nodePathVal, pathVal) {
}


async function getObjectId (entityName, fields, curObj) {
async function getObjectId (reqData, entityName, fields, curObj) {
let all = [], { curObjFromReqData: req_data={}, curObjFromDbQuery: db_data={} } = curObj
let entity = cds.model.definitions[entityName]
if (!fields?.length) fields = entity["@changelog"]?.map?.(k => k['='] || k) || []
Expand All @@ -81,13 +81,30 @@ async function getObjectId (entityName, fields, curObj) {
while (path.length > 1) {
let assoc = current.elements[path[0]]; if (!assoc?.isAssociation) break
let foreignKey = assoc.keys?.[0]?.$generatedFieldName
let IDval = req_data[foreignKey] || _db_data[foreignKey]
let IDval =
req_data[foreignKey] && current.name === entityName
? req_data[foreignKey]
: _db_data[foreignKey]
if (IDval) try {
// REVISIT: This always reads all elements -> should read required ones only!
let ID = assoc.keys?.[0]?.ref[0] || 'ID'
// When parent/child nodes are created simultaneously, data is taken from draft table
_db_data = await SELECT.one.from(assoc._target).where({[ID]: IDval}) ||
await SELECT.one.from(`${assoc._target}.drafts`).where({[ID]: IDval}) || {}
const isComposition = hasComposition(assoc._target, current)
// Peer association and composition are distinguished by the value of isComposition.
if (isComposition) {
// This function can recursively retrieve the desired information from reqData without having to read it from db.
_db_data = _getCompositionObjFromReq(reqData, IDval)
// When multiple layers of child nodes are deleted at the same time, the deep layer of child nodes will lose the information of the upper nodes, so data needs to be extracted from the db.
if (!_db_data || JSON.stringify(_db_data) === '{}') {
_db_data =
(await SELECT.one
.from(assoc._target)
.where({ [ID]: IDval })) || {}
}
} else {
_db_data =
(await SELECT.one.from(assoc._target).where({ [ID]: IDval })) ||
{}
}
} catch (e) {
LOG.error("Failed to generate object Id for an association entity.", e)
throw new Error("Failed to generate object Id for an association entity.", e)
Expand Down Expand Up @@ -134,6 +151,39 @@ const getValueEntityType = function (entityName, fields) {
return types.join(', ')
}

const hasComposition = function (parentEntity, subEntity) {
if (!parentEntity.compositions) {
return false
}

const compositions = Object.values(parentEntity.compositions);

for (const composition of compositions) {
if (composition.target === subEntity.name) {
return true;
}
}

return false
}

const _getCompositionObjFromReq = function (obj, targetID) {
if (obj.ID === targetID) {
return obj;
}

for (const key in obj) {
if (typeof obj[key] === "object" && obj[key] !== null) {
const result = _getCompositionObjFromReq(obj[key], targetID);
if (result) {
return result;
}
}
}

return null;
};

module.exports = {
getCurObjFromReqData,
getCurObjFromDbQuery,
Expand Down
18 changes: 17 additions & 1 deletion tests/bookshop/db/_i18n/i18n.properties
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,20 @@ serviceAuthors.name=Author Name
#XTIT
bookStoreRegistry.objectTitle=Book Store Registry
bookStoreRegistry.code=Code
bookStoreRegistry.validOn=Valid On
bookStoreRegistry.validOn=Valid On

## RootEntity
#XTIT
RootEntity.objectTitle= Root Entity

## Level1Entity
#XTIT
Level1Entity.objectTitle=Level1 Entity

## Level2Entity
#XTIT
Level2Entity.objectTitle=Level2 Entity

## Level3Entity
#XTIT
Level3Entity.objectTitle=Level3 Entity
4 changes: 4 additions & 0 deletions tests/bookshop/db/data/sap.capire.bookshop-AssocOne.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ID;name;info_ID
bc21e0d9-a313-4f52-8336-c1be5f88c346;Mission1;bc21e0d9-a313-4f52-8336-c3da5f66d537
bc21e0d9-a313-4f52-8336-c1be5f55d137;Mission2;bc21e0d9-a313-4f52-8336-d4ad6c55d563
bc21e0d9-a313-4f52-8336-c1be5f44f435;Mission3;bc21e0d9-a313-4f52-8336-b5fa4d22a123
4 changes: 4 additions & 0 deletions tests/bookshop/db/data/sap.capire.bookshop-AssocThree.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ID;name
bc21e0d9-a313-4f52-8336-a4eb6d55c137;Super Mario1
bc21e0d9-a313-4f52-8336-a2dcec6d33f541;Super Mario2
bc21e0d9-a313-4f52-8336-d3da5a66c153;Super Mario3
4 changes: 4 additions & 0 deletions tests/bookshop/db/data/sap.capire.bookshop-AssocTwo.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ID;name;info_ID
bc21e0d9-a313-4f52-8336-c3da5f66d537;Track1;bc21e0d9-a313-4f52-8336-a4eb6d55c137
c21e0d9-a313-4f52-8336-d4ad6c55d563;Track2;bc21e0d9-a313-4f52-8336-a2dcec6d33f541
bc21e0d9-a313-4f52-8336-b5fa4d22a123;Track3;bc21e0d9-a313-4f52-8336-d3da5a66c153
6 changes: 6 additions & 0 deletions tests/bookshop/db/data/sap.capire.bookshop-Level1Entity.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
ID;title;parent_ID
9d703c23-54a8-4eff-81c1-cdce6b8376b1;Level1 Wuthering Heights;64625905-c234-4d0d-9bc1-283ee8940812
676059d4-8851-47f1-b558-3bdc461bf7d5;Level1 Jane Eyre;5ab2a87b-3a56-4d97-a697-7af72334b123
42bc7997-f6ce-4ae9-8a64-ee5e02ef1087;Level1 The Raven;5ab2a87b-3a56-4d97-a697-7af72334b213
9297e4ea-396e-47a4-8815-cd4622dea8b1;Level1 Eleonora;8aaed432-8336-4b0d-be7e-3ef1ce7f14dc
574c8add-0ee3-4175-ab62-ca09a92c723c;Level1 Catweazle;8aaed432-8336-4b0d-be7e-3ef1ce7f14dc
3 changes: 3 additions & 0 deletions tests/bookshop/db/data/sap.capire.bookshop-Level1Object.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ID;title;parent_ID
9a61178f-bfb3-4c17-8d17-c6b4a63e0802;Level1Object title1;0a41a187-a2ff-4df6-bd12-fae8996e7e28
ae0d8b10-84cf-4777-a489-a198d1717c75;Level1Object title2;6ac4afbf-deda-45ae-88e6-2883157cd576
2 changes: 2 additions & 0 deletions tests/bookshop/db/data/sap.capire.bookshop-Level2Entity.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ID;title;parent_ID
dd1fdd7d-da2a-4600-940b-0baf2946c4ff;Level2 The title;9d703c23-54a8-4eff-81c1-cdce6b8376b1
3 changes: 3 additions & 0 deletions tests/bookshop/db/data/sap.capire.bookshop-Level2Object.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ID;title;parent_ID
a40a9fd8-573d-4f41-1111-fa8ea0d8b1bc;Level2Object title1;9a61178f-bfb3-4c17-8d17-c6b4a63e0802
55bb60e4-ed86-46e6-9378-346153eba8d4;Level2Object title2;ae0d8b10-84cf-4777-a489-a198d1717c75
2 changes: 2 additions & 0 deletions tests/bookshop/db/data/sap.capire.bookshop-Level3Entity.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ID;title;parent_ID
dd1fdd7d-da2a-4600-940b-1cdd2946c4ff;Level3 The Hope;dd1fdd7d-da2a-4600-940b-0baf2946c4ff
2 changes: 2 additions & 0 deletions tests/bookshop/db/data/sap.capire.bookshop-Level3Object.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ID;title;parent_ID
a40a9fd8-573d-4f41-1111-fb8ea0d8c5cc;Level3Object title;a40a9fd8-573d-4f41-1111-fa8ea0d8b1bc
10 changes: 5 additions & 5 deletions tests/bookshop/db/data/sap.capire.bookshop-OrderHeader.csv
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
ID;status;
6b75449a-d44f-11ed-afa1-0242ac120001;Ordered;
8567d0de-d44f-11ed-afa1-0242ac120001;Shipped;
6b75449a-d44f-11ed-afa1-0242ac120002;Ordered;
8567d0de-d44f-11ed-afa1-0242ac120002;Shipped;
ID;status
6b75449a-d44f-11ed-afa1-0242ac120001;Ordered
8567d0de-d44f-11ed-afa1-0242ac120001;Shipped
6b75449a-d44f-11ed-afa1-0242ac120002;Ordered
8567d0de-d44f-11ed-afa1-0242ac120002;Shipped
4 changes: 2 additions & 2 deletions tests/bookshop/db/data/sap.capire.bookshop-OrderItem.csv
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
ID;quantity;price;order_ID;customer_ID;
ID;quantity;price;order_ID;customer_ID
9a61178f-bfb3-4c17-8d17-c6b4a63e0097;10.0;5.0;0a41a187-a2ff-4df6-bd12-fae8996e6e31;47f97f40-4f41-488a-b10b-a5725e762d57
ae0d8b10-84cf-4777-a489-a198d1716b61;11.0;6.0;0a41a187-a2ff-4df6-bd12-fae8996e6e31;47f97f40-4f41-488a-b10b-a5725e762d57
ae0d8b10-84cf-4777-a489-a198d1716b61;11.0;6.0;0a41a187-a2ff-4df6-bd12-fae8996e6e31;47f97f40-4f41-488a-b10b-a5725e762d57
4 changes: 4 additions & 0 deletions tests/bookshop/db/data/sap.capire.bookshop-RootEntity.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ID;name;lifecycleStatus_code;info_ID
64625905-c234-4d0d-9bc1-283ee8940812;Wuthering Heights;IP;bc21e0d9-a313-4f52-8336-c1be5f88c346
5ab2a87b-3a56-4d97-a697-7af72334b123;Jane Eyre;CL;bc21e0d9-a313-4f52-8336-c1be5f55d137
8aaed432-8336-4b0d-be7e-3ef1ce7f14dc;The Raven;AC;bc21e0d9-a313-4f52-8336-c1be5f44f435
3 changes: 3 additions & 0 deletions tests/bookshop/db/data/sap.capire.bookshop-RootObject.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ID;title
0a41a187-a2ff-4df6-bd12-fae8996e7e28;RootObject title1
6ac4afbf-deda-45ae-88e6-2883157cd576;RootObject title2
71 changes: 71 additions & 0 deletions tests/bookshop/db/schema.cds
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,77 @@ using {sap.capire.bookshop.PaymentAgreementStatusCodes as PaymentAgreementStatus

namespace sap.capire.bookshop;

@fiori.draft.enabled
@title: '{i18n>RootEntity.objectTitle}'
entity RootEntity @(cds.autoexpose) : managed, cuid {
name : String;
lifecycleStatus : LifecycleStatusCode;
child : Composition of many Level1Entity
on child.parent = $self;
info : Association to one AssocOne;
}

@title: '{i18n>Level1Entity.objectTitle}'
entity Level1Entity : managed, cuid {
title : String;
parent : Association to one RootEntity;
child : Composition of many Level2Entity
on child.parent = $self;
}

@title: '{i18n>Level2Entity.objectTitle}'
entity Level2Entity : managed, cuid {
title : String;
parent : Association to one Level1Entity;
child : Composition of many Level3Entity
on child.parent = $self;
}

@title: '{i18n>Level3Entity.objectTitle}'
entity Level3Entity : managed, cuid {
title : String;
parent : Association to one Level2Entity;
}

entity AssocOne : cuid {
name : String;
info : Association to one AssocTwo;
}

entity AssocTwo : cuid {
name : String;
info : Association to one AssocThree;
}

entity AssocThree : cuid {
name : String;
}

entity RootObject : cuid {
child : Composition of many Level1Object
on child.parent = $self;
title : String;
}

entity Level1Object : cuid {
parent : Association to one RootObject;
child : Composition of many Level2Object
on child.parent = $self;
title : String;
}

entity Level2Object : cuid {
title : String;
parent : Association to one Level1Object;
child : Composition of many Level3Object
on child.parent = $self;
}

entity Level3Object : cuid {
parent : Association to one Level2Object;
title : String;
}

@fiori.draft.enabled
@title : '{i18n>bookStore.objectTitle}'
entity BookStores @(cds.autoexpose) : managed, cuid {
Expand Down
77 changes: 72 additions & 5 deletions tests/bookshop/srv/admin-service.cds
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,25 @@ service AdminService {
@odata.draft.enabled
entity BookStores @(cds.autoexpose) as projection on my.BookStores;

entity Authors as projection on my.Authors;
entity Report as projection on my.Report;
entity Order as projection on my.Order;
entity OrderItem as projection on my.OrderItem;
@odata.draft.enabled
entity RootEntity @(cds.autoexpose) as projection on my.RootEntity;

entity RootObject as projection on my.RootObject;
entity Level1Object as projection on my.Level1Object;
entity Level2Object as projection on my.Level2Object;
entity Level3Object as projection on my.Level3Object;
entity Level1Entity as projection on my.Level1Entity;
entity Level2Entity as projection on my.Level2Entity;
entity Level3Entity as projection on my.Level3Entity;
entity AssocOne as projection on my.AssocOne;
entity AssocTwo as projection on my.AssocTwo;
entity AssocThree as projection on my.AssocThree;
entity Authors as projection on my.Authors;
entity Report as projection on my.Report;
entity Order as projection on my.Order;
entity OrderItem as projection on my.OrderItem;

entity OrderItemNote as projection on my.OrderItemNote actions {
entity OrderItemNote as projection on my.OrderItemNote actions {
@cds.odata.bindingparameter.name: 'self'
@Common.SideEffects : {TargetEntities: [self]}
action activate();
Expand All @@ -25,6 +38,60 @@ service AdminService {
entity Customers as projection on my.Customers;
}

annotate AdminService.RootEntity with @changelog: [name] {
name @changelog;
child @changelog : [child.child.child.title];
lifecycleStatus @changelog : [lifecycleStatus.name];
info @changelog : [info.info.info.name];
};

annotate AdminService.Level1Entity with @changelog: [parent.lifecycleStatus.name] {
title @changelog;
child @changelog : [child.title];
};

annotate AdminService.Level2Entity with @changelog: [parent.parent.lifecycleStatus.name] {
title @changelog;
child @changelog : [child.title];
};

annotate AdminService.Level3Entity with @changelog: [parent.parent.parent.lifecycleStatus.name] {
title @changelog;
}

annotate AdminService.AssocOne with {
name @changelog;
info @changelog: [info.info.name]
};

annotate AdminService.AssocTwo with {
name @changelog;
info @changelog: [info.name]
};

annotate AdminService.AssocThree with {
name @changelog;
};

annotate AdminService.RootObject with {
title @changelog;
}

annotate AdminService.Level1Object with {
title @changelog;
child @changelog: [child.title];
}

annotate AdminService.Level2Object with {
title @changelog;
child @changelog: [child.title];
};

annotate AdminService.Level3Object with {
title @changelog;
parent @changelog: [parent.parent.parent.title]
};

annotate AdminService.Authors with {
name @(Common.Label : '{i18n>serviceAuthors.name}');
};
Expand Down
Loading

0 comments on commit 5b9b458

Please sign in to comment.