From e321cb164f51afd4eb328231ab7cc7eb57f103e1 Mon Sep 17 00:00:00 2001 From: nkaputnik Date: Tue, 18 Jun 2024 10:11:13 +0200 Subject: [PATCH 1/6] create Global switch for preserveDelete --- lib/change-log.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/change-log.js b/lib/change-log.js index 0ad2f68..99588f9 100644 --- a/lib/change-log.js +++ b/lib/change-log.js @@ -350,8 +350,13 @@ async function track_changes (req) { let entityKey = diff.ID if (cds.transaction(req).context.event === "DELETE") { - if (isDraftEnabled || !isComposition) { - return await DELETE.from(`sap.changelog.ChangeLog`).where({ entityKey }) + if (cds.env.requires.change-tracking?.preserveDeletes) { + //toDo + } + else { + if (isDraftEnabled || !isComposition) { + return await DELETE.from(`sap.changelog.ChangeLog`).where({ entityKey }) + } } } From 931af17106c865ee7cf0e2055ac07fc8af1baea2 Mon Sep 17 00:00:00 2001 From: nkaputnik Date: Tue, 18 Jun 2024 10:20:59 +0200 Subject: [PATCH 2/6] fixed dash bug --- lib/change-log.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/change-log.js b/lib/change-log.js index 99588f9..cf54825 100644 --- a/lib/change-log.js +++ b/lib/change-log.js @@ -350,7 +350,7 @@ async function track_changes (req) { let entityKey = diff.ID if (cds.transaction(req).context.event === "DELETE") { - if (cds.env.requires.change-tracking?.preserveDeletes) { + if (cds.env.requires.changeTracking?.preserveDeletes) { //toDo } else { From a21f92103f85d1cd437c21386cb25378d07f89c8 Mon Sep 17 00:00:00 2001 From: Nick Josipovic Date: Wed, 19 Jun 2024 10:07:04 +0200 Subject: [PATCH 3/6] Update change-log.js Corrected this to the change-tracking flag... --- lib/change-log.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/change-log.js b/lib/change-log.js index cf54825..cd30fc0 100644 --- a/lib/change-log.js +++ b/lib/change-log.js @@ -350,7 +350,7 @@ async function track_changes (req) { let entityKey = diff.ID if (cds.transaction(req).context.event === "DELETE") { - if (cds.env.requires.changeTracking?.preserveDeletes) { + if (cds.env.requires.['change-tracking']?.preserveDeletes) { //toDo } else { From 7ca0ccc14ef9a7853df7e6a9d5e4cd61d3cda07a Mon Sep 17 00:00:00 2001 From: Nick Josipovic Date: Wed, 19 Jun 2024 10:16:41 +0200 Subject: [PATCH 4/6] Update change-log.js --- lib/change-log.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/change-log.js b/lib/change-log.js index cd30fc0..c518cde 100644 --- a/lib/change-log.js +++ b/lib/change-log.js @@ -350,7 +350,7 @@ async function track_changes (req) { let entityKey = diff.ID if (cds.transaction(req).context.event === "DELETE") { - if (cds.env.requires.['change-tracking']?.preserveDeletes) { + if (cds.env.requires.["change-tracking"]?.preserveDeletes) { //toDo } else { From 6399a9f7a454b448035a6a9dbe7d45346170b788 Mon Sep 17 00:00:00 2001 From: Nick Josipovic Date: Wed, 19 Jun 2024 10:18:26 +0200 Subject: [PATCH 5/6] Update change-log.js --- lib/change-log.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/change-log.js b/lib/change-log.js index c518cde..6c30e71 100644 --- a/lib/change-log.js +++ b/lib/change-log.js @@ -350,7 +350,7 @@ async function track_changes (req) { let entityKey = diff.ID if (cds.transaction(req).context.event === "DELETE") { - if (cds.env.requires.["change-tracking"]?.preserveDeletes) { + if (cds.env.requires["change-tracking"]?.preserveDeletes) { //toDo } else { From bee2a5c04494ef78e866a87fad895b1a40cd38db Mon Sep 17 00:00:00 2001 From: I560824 Date: Tue, 25 Jun 2024 17:30:05 +0800 Subject: [PATCH 6/6] fix: When deleting the root entity, incorrect ObjectID will be captured in the generated changelog. --- lib/change-log.js | 13 +-- lib/entity-helper.js | 16 ++-- .../integration/fiori-draft-disabled.test.js | 77 ++++++++++++++++++ tests/integration/fiori-draft-enabled.test.js | 79 +++++++++++++++++++ tests/integration/service-api.test.js | 36 +++++++++ 5 files changed, 203 insertions(+), 18 deletions(-) diff --git a/lib/change-log.js b/lib/change-log.js index 6c30e71..ccaebde 100644 --- a/lib/change-log.js +++ b/lib/change-log.js @@ -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 }) } } diff --git a/lib/entity-helper.js b/lib/entity-helper.js index f45f7d7..fc0b050 100644 --- a/lib/entity-helper.js +++ b/lib/entity-helper.js @@ -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) @@ -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) diff --git a/tests/integration/fiori-draft-disabled.test.js b/tests/integration/fiori-draft-disabled.test.js index ba44d59..8fbc3ad 100644 --- a/tests/integration/fiori-draft-disabled.test.js +++ b/tests/integration/fiori-draft-disabled.test.js @@ -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`, @@ -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"]; }); diff --git a/tests/integration/fiori-draft-enabled.test.js b/tests/integration/fiori-draft-enabled.test.js index ca000ca..b3c52ba 100644 --- a/tests/integration/fiori-draft-enabled.test.js +++ b/tests/integration/fiori-draft-enabled.test.js @@ -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( {}, @@ -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"] = [ diff --git a/tests/integration/service-api.test.js b/tests/integration/service-api.test.js index 0ed616b..aec614d 100644 --- a/tests/integration/service-api.test.js +++ b/tests/integration/service-api.test.js @@ -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", @@ -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 = [