Skip to content

Commit

Permalink
fix: When deleting the root entity, incorrect ObjectID will be captur…
Browse files Browse the repository at this point in the history
…ed in the generated changelog.
  • Loading branch information
Sv7enNowitzki committed Jun 25, 2024
1 parent 6399a9f commit bee2a5c
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 18 deletions.
13 changes: 4 additions & 9 deletions lib/change-log.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,15 +348,10 @@ async function track_changes (req) {
let isDraftEnabled = !!target.drafts
let isComposition = _isCompositionContextPath(req.context.path)
let entityKey = diff.ID

if (cds.transaction(req).context.event === "DELETE") {
if (cds.env.requires["change-tracking"]?.preserveDeletes) {
//toDo
}
else {
if (isDraftEnabled || !isComposition) {
return await DELETE.from(`sap.changelog.ChangeLog`).where({ entityKey })
}

if (cds.transaction(req).context.event === "DELETE" && !cds.env.requires["change-tracking"]?.preserveDeletes) {
if (isDraftEnabled || !isComposition) {
return await DELETE.from(`sap.changelog.ChangeLog`).where({ entityKey })
}
}

Expand Down
16 changes: 7 additions & 9 deletions lib/entity-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ async function getObjectId (reqData, entityName, fields, curObj) {
req_data[foreignKey] && current.name === entityName
? req_data[foreignKey]
: _db_data[foreignKey]
if (IDval) try {
if (!IDval) {
_db_data = {};
} else try {
// REVISIT: This always reads all elements -> should read required ones only!
let ID = assoc.keys?.[0]?.ref[0] || 'ID'
const isComposition = hasComposition(assoc._target, current)
Expand All @@ -94,16 +96,12 @@ async function getObjectId (reqData, entityName, fields, curObj) {
// 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 })) || {}
const entityKeys = Object.keys(reqData).filter(item => !Object.keys(assoc._target.keys).some(ele => item === ele));
if (!_db_data || JSON.stringify(_db_data) === '{}' || entityKeys.length === 0) {
_db_data = await getCurObjFromDbQuery(assoc._target, IDval, ID);
}
} else {
_db_data =
(await SELECT.one.from(assoc._target).where({ [ID]: IDval })) ||
{}
_db_data = await getCurObjFromDbQuery(assoc._target, IDval, ID);
}
} catch (e) {
LOG.error("Failed to generate object Id for an association entity.", e)
Expand Down
77 changes: 77 additions & 0 deletions tests/integration/fiori-draft-disabled.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,67 @@ describe("change log draft disabled test", () => {
expect(afterChanges.length).to.equal(0);
});

it("1.4 When the global switch is on, all changelogs should be retained after the root entity is deleted, and a changelog for the deletion operation should be generated", async () => {
cds.env.requires["change-tracking"].preserveDeletes = true;

cds.services.AdminService.entities.RootObject["@changelog"] = [
{ "=": "title" }
];
cds.services.AdminService.entities.Level1Object["@changelog"] = [
{ "=": "parent.title" }
];
cds.services.AdminService.entities.Level2Object["@changelog"] = [
{ "=": "parent.parent.title" }
];
const RootObject = await POST(
`/odata/v4/admin/RootObject`,
{
ID: "a670e8e1-ee06-4cad-9cbd-a2354dc37c9d",
title: "new RootObject title",
child: [
{
ID: "48268451-8552-42a6-a3d7-67564be97733",
title: "new Level1Object title",
child: [
{
ID: "12ed5dd8-d45b-11ed-afa1-1942bd228115",
title: "new Level2Object title",
}
]
}
]
},
);

const beforeChanges = await adminService.run(SELECT.from(ChangeView));
expect(beforeChanges.length > 0).to.be.true;

// Test when the root and child entity deletion occur simultaneously
await DELETE(`/odata/v4/admin/RootObject(ID=${RootObject.data.ID})`);

const afterChanges = await adminService.run(SELECT.from(ChangeView));
expect(afterChanges.length).to.equal(8);

const changelogCreated = afterChanges.filter(ele=> ele.modification === "Create");
const changelogDeleted = afterChanges.filter(ele=> ele.modification === "Delete");

const compareAttributes = ['keys', 'attribute', 'entity', 'serviceEntity', 'parentKey', 'serviceEntityPath', 'valueDataType', 'objectID', 'parentObjectID', 'entityKey'];

let commonItems = changelogCreated.filter(beforeItem => {
return changelogDeleted.some(afterItem => {
return compareAttributes.every(attr => beforeItem[attr] === afterItem[attr])
&& beforeItem['valueChangedFrom'] === afterItem['valueChangedTo']
&& beforeItem['valueChangedTo'] === afterItem['valueChangedFrom'];
});
});

expect(commonItems.length > 0).to.be.true;

delete cds.services.AdminService.entities.RootObject["@changelog"];
delete cds.services.AdminService.entities.Level1Object["@changelog"];
delete cds.services.AdminService.entities.Level2Object["@changelog"];
});

it("3.1 Composition creatition by odata request on draft disabled entity - should log changes for root entity (ERP4SMEPREPWORKAPPPLAT-670)", async () => {
await POST(
`/admin/Order(ID=0a41a187-a2ff-4df6-bd12-fae8996e6e31)/orderItems(ID=9a61178f-bfb3-4c17-8d17-c6b4a63e0097)/notes`,
Expand Down Expand Up @@ -450,6 +511,22 @@ describe("change log draft disabled test", () => {
expect(createOrderChanges.length).to.equal(1);
const createOrderChange = createOrderChanges[0];
expect(createOrderChange.objectID).to.equal("test Order title");

await PATCH(`/odata/v4/admin/Order(ID=0a41a187-a2ff-4df6-bd12-fae8996e7c44)`, {
title: "Order title changed"
});

const updateOrderChanges = await adminService.run(
SELECT.from(ChangeView).where({
entity: "sap.capire.bookshop.Order",
attribute: "title",
modification: "update",
}),
);
expect(updateOrderChanges.length).to.equal(1);
const updateOrderChange = updateOrderChanges[0];
expect(updateOrderChange.objectID).to.equal("Order title changed");

delete cds.db.entities.Order["@changelog"];
});

Expand Down
79 changes: 79 additions & 0 deletions tests/integration/fiori-draft-enabled.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,64 @@ describe("change log integration test", () => {
await data.reset();
});


it("1.5 When the global switch is on, all changelogs should be retained after the root entity is deleted, and a changelog for the deletion operation should be generated", async () => {
cds.env.requires["change-tracking"].preserveDeletes = true;

// Root and child nodes are created at the same time
const createAction = POST.bind({}, `/odata/v4/admin/RootEntity`, {
ID: "01234567-89ab-cdef-0123-987654fedcba",
name: "New name for RootEntity",
child: [
{
ID: "12ed5dd8-d45b-11ed-afa1-0242ac120003",
title: "New name for Level1Entity",
child: [
{
ID: "12ed5dd8-d45b-11ed-afa1-0242ac124446",
title: "New name for Level2Entity",
child: [
{
ID: "12ed5dd8-d45b-11ed-afa1-0242ac123335",
title: "New name for Level3Entity",
},
],
},
],
},
],
});
await utils.apiAction(
"admin",
"RootEntity",
"01234567-89ab-cdef-0123-987654fedcba",
"AdminService",
createAction,
true,
);
const beforeChanges = await adminService.run(SELECT.from(ChangeView));
expect(beforeChanges.length > 0).to.be.true;

await DELETE(`/admin/RootEntity(ID=01234567-89ab-cdef-0123-987654fedcba,IsActiveEntity=true)`);

const afterChanges = await adminService.run(SELECT.from(ChangeView));

const changelogCreated = afterChanges.filter(ele=> ele.modification === "Create");
const changelogDeleted = afterChanges.filter(ele=> ele.modification === "Delete");

const compareAttributes = ['keys', 'attribute', 'entity', 'serviceEntity', 'parentKey', 'serviceEntityPath', 'valueDataType', 'objectID', 'parentObjectID', 'entityKey'];

let commonItems = changelogCreated.filter(beforeItem => {
return changelogDeleted.some(afterItem => {
return compareAttributes.every(attr => beforeItem[attr] === afterItem[attr])
&& beforeItem['valueChangedFrom'] === afterItem['valueChangedTo']
&& beforeItem['valueChangedTo'] === afterItem['valueChangedFrom'];
});
});
expect(commonItems.length > 0).to.be.true;
expect(afterChanges.length).to.equal(14);
});

it("2.1 Child entity creation - should log basic data type changes (ERP4SMEPREPWORKAPPPLAT-32 ERP4SMEPREPWORKAPPPLAT-613)", async () => {
const action = POST.bind(
{},
Expand Down Expand Up @@ -814,6 +872,27 @@ describe("change log integration test", () => {
const BookStoresChange = BookStoresChanges[0];
expect(BookStoresChange.objectID).to.equal("new name");

const updateBookStoresAction = PATCH.bind({}, `/admin/BookStores(ID=9d703c23-54a8-4eff-81c1-cdce6b6587c4,IsActiveEntity=false)`, {
name: "name update",
});
await utils.apiAction(
"admin",
"BookStores",
"9d703c23-54a8-4eff-81c1-cdce6b6587c4",
"AdminService",
updateBookStoresAction,
);
const updateBookStoresChanges = await adminService.run(
SELECT.from(ChangeView).where({
entity: "sap.capire.bookshop.BookStores",
attribute: "name",
modification: "update",
}),
);
expect(updateBookStoresChanges.length).to.equal(1);
const updateBookStoresChange = updateBookStoresChanges[0];
expect(updateBookStoresChange.objectID).to.equal("name update");

delete cds.services.AdminService.entities.BookStores["@changelog"];

cds.services.AdminService.entities.Books["@changelog"] = [
Expand Down
36 changes: 36 additions & 0 deletions tests/integration/service-api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@ describe("change log integration test", () => {
await data.reset();
});

it("1.6 When the global switch is on, all changelogs should be retained after the root entity is deleted, and a changelog for the deletion operation should be generated", async () => {
cds.env.requires["change-tracking"].preserveDeletes = true;
const level3EntityData = [
{
ID: "12ed5dd8-d45b-11ed-afa1-0242ac654321",
title: "Service api Level3 title",
parent_ID: "dd1fdd7d-da2a-4600-940b-0baf2946c4ff",
},
];
await adminService.run(INSERT.into(adminService.entities.Level3Entity).entries(level3EntityData));
let beforeChanges = await SELECT.from(ChangeView);
expect(beforeChanges.length > 0).to.be.true;

await adminService.run(DELETE.from(adminService.entities.RootEntity).where({ ID: "64625905-c234-4d0d-9bc1-283ee8940812" }));
let afterChanges = await SELECT.from(ChangeView);
expect(afterChanges.length).to.equal(11);
});

it("2.5 Root entity deep creation by service API - should log changes on root entity (ERP4SMEPREPWORKAPPPLAT-32 ERP4SMEPREPWORKAPPPLAT-613)", async () => {
const bookStoreData = {
ID: "843b3681-8b32-4d30-82dc-937cdbc68b3a",
Expand Down Expand Up @@ -86,6 +104,24 @@ describe("change log integration test", () => {
const createBookStoresChange = createBookStoresChanges[0];
expect(createBookStoresChange.objectID).to.equal("new name");

await UPDATE(adminService.entities.BookStores)
.where({
ID: "9d703c23-54a8-4eff-81c1-cdce6b6587c4"
})
.with({
name: "BookStores name changed"
});
const updateBookStoresChanges = await adminService.run(
SELECT.from(ChangeView).where({
entity: "sap.capire.bookshop.BookStores",
attribute: "name",
modification: "update",
}),
);
expect(updateBookStoresChanges.length).to.equal(1);
const updateBookStoresChange = updateBookStoresChanges[0];
expect(updateBookStoresChange.objectID).to.equal("BookStores name changed");

cds.services.AdminService.entities.BookStores["@changelog"].pop();

const level3EntityData = [
Expand Down

0 comments on commit bee2a5c

Please sign in to comment.