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 = [