Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

create Global switch for preserveDelete #96

Merged
merged 7 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/change-log.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,8 @@ 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.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
Loading