Skip to content

Commit 70f98c4

Browse files
committed
Add EntityFlattener and use it in the DraftCancelAttachmentsHandler to flatten nested delete requests
1 parent 18e56b4 commit 70f98c4

File tree

3 files changed

+116
-4
lines changed

3 files changed

+116
-4
lines changed

cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/draftservice/DraftCancelAttachmentsHandler.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,13 @@ private boolean hasAttachmentAssociations(CdsEntity entity) {
126126
private List<Attachments> readAttachments(
127127
DraftCancelEventContext context, CdsStructuredType entity, boolean isActiveEntity) {
128128
logger.debug(
129-
"Reading attachments for entity {} (isActiveEntity={})",
130-
entity.getName(),
131-
isActiveEntity);
129+
"Reading attachments for entity {} (isActiveEntity={})", entity.getName(), isActiveEntity);
132130
logger.debug("Original CQN: {}", context.getCqn());
131+
CqnDelete cqnFlattenedEntity = CQL.copy(context.getCqn(), new EntityFlattener());
133132
CqnDelete cqnInactiveEntity =
134133
CQL.copy(
135-
context.getCqn(), new ActiveEntityModifier(isActiveEntity, entity.getQualifiedName()));
134+
cqnFlattenedEntity,
135+
new ActiveEntityModifier(isActiveEntity, entity.getQualifiedName()));
136136
logger.debug("Modified CQN: {}", cqnInactiveEntity);
137137
return attachmentsReader.readAttachments(
138138
context.getModel(), (CdsEntity) entity, cqnInactiveEntity);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* © 2024-2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors.
3+
*/
4+
package com.sap.cds.feature.attachments.handler.draftservice;
5+
6+
import com.sap.cds.ql.CQL;
7+
import com.sap.cds.ql.RefBuilder;
8+
import com.sap.cds.ql.RefBuilder.RefSegment;
9+
import com.sap.cds.ql.StructuredTypeRef;
10+
import com.sap.cds.ql.cqn.CqnPredicate;
11+
import com.sap.cds.ql.cqn.CqnStructuredTypeRef;
12+
import com.sap.cds.ql.cqn.Modifier;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
16+
/**
17+
* Modifier that flattens nested entity references into direct entity references.
18+
*
19+
* <p>This modifier transforms nested references (e.g., Books → covers) into a single entity
20+
* reference (e.g., Books.covers_drafts) while preserving filter conditions from the target segment.
21+
*/
22+
class EntityFlattener implements Modifier {
23+
24+
private static final Logger logger = LoggerFactory.getLogger(EntityFlattener.class);
25+
26+
EntityFlattener() {}
27+
28+
@Override
29+
public CqnStructuredTypeRef ref(CqnStructuredTypeRef original) {
30+
31+
RefBuilder<StructuredTypeRef> ref = CQL.copy(original);
32+
RefSegment rootSegment = ref.rootSegment();
33+
34+
// Flatten the query when it's nested, e.g. Books -> Chapters -> Chapters.images
35+
if (ref.segments().size() > 1) {
36+
logger.debug("Removing nested segments for ref {}", rootSegment);
37+
} else {
38+
// No nested segments, return as is
39+
return original;
40+
}
41+
42+
// Get the filter from the last segment before removing segments
43+
RefSegment lastSegment = ref.segments().get(ref.segments().size() - 1);
44+
CqnPredicate lastFilter = null;
45+
if (lastSegment.filter().isPresent()) {
46+
Modifier modifier = new EntityFlattener();
47+
lastFilter = CQL.copy(lastSegment.filter().get(), modifier);
48+
}
49+
50+
while (ref.segments().size() > 1) {
51+
ref.segments().remove(ref.segments().size() - 1);
52+
}
53+
54+
// Apply the filter to the root segment if there was a filter on the last segment
55+
if (lastFilter != null) {
56+
rootSegment.filter(lastFilter);
57+
}
58+
59+
// Create a direct reference to the target entity with the modified filter
60+
return ref.build();
61+
}
62+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* © 2024-2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors.
3+
*/
4+
package com.sap.cds.feature.attachments.handler.draftservice;
5+
6+
import static com.sap.cds.services.draft.Drafts.IS_ACTIVE_ENTITY;
7+
import static org.assertj.core.api.Assertions.assertThat;
8+
9+
import com.sap.cds.ql.CQL;
10+
import com.sap.cds.ql.Select;
11+
import com.sap.cds.ql.cqn.CqnSelect;
12+
import org.junit.jupiter.api.Test;
13+
14+
public class EntityFlattenerTest {
15+
16+
@Test
17+
void nestedReferenceWithFilterIsFlattened() {
18+
// Create a nested reference with a filter on the last segment: AdminService.Books -> covers
19+
// (with filter)
20+
CqnSelect select =
21+
Select.from(
22+
"AdminService.Books",
23+
c -> c.to("covers").filter(e -> e.get(IS_ACTIVE_ENTITY).eq(false)));
24+
25+
// The target entity name should be the flattened version
26+
var result = CQL.copy(select, new EntityFlattener());
27+
// The filter should still be there
28+
assertThat(result.toString())
29+
.contains("{\"ref\":[\"IsActiveEntity\"]},\"=\",{\"val\":false}]}");
30+
// Should contain root entity
31+
assertThat(result.toString()).contains("AdminService.Books");
32+
// Should not contain the original nested structure
33+
assertThat(result.toString()).doesNotContain("\"to\":");
34+
}
35+
36+
@Test
37+
void nestedReferenceWithoutFilterIsFlattened() {
38+
// Create a nested reference with a filter on the last segment: AdminService.Books -> covers
39+
// (with filter)
40+
CqnSelect select = Select.from("AdminService.Books", c -> c.to("covers").to("anoter_nesting"));
41+
42+
// The target entity name should be the flattened version
43+
var result = CQL.copy(select, new EntityFlattener());
44+
// Should contain root entity
45+
assertThat(result.toString()).contains("\"ref\":[\"AdminService.Books\"]");
46+
System.out.println(result.toString());
47+
// Should not contain the original nested structure
48+
assertThat(result.toString()).doesNotContain("\"to\":");
49+
}
50+
}

0 commit comments

Comments
 (0)