Skip to content

Commit 45a73b0

Browse files
committed
Add EntityFlattener and use it in the DraftCancelAttachmentsHandler to flatten nested delete requests
1 parent 4dc2d1f commit 45a73b0

File tree

3 files changed

+121
-4
lines changed

3 files changed

+121
-4
lines changed

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,16 @@ private boolean hasAttachmentAssociations(CdsEntity entity) {
125125
private List<Attachments> readAttachments(
126126
DraftCancelEventContext context, CdsStructuredType entity, boolean isActiveEntity) {
127127
logger.debug(
128-
"Reading attachments for entity {} (isActiveEntity={})",
129-
entity.getName(),
130-
isActiveEntity);
128+
"Reading attachments for entity {} (isActiveEntity={})", entity.getName(), isActiveEntity);
131129
logger.debug("Original CQN: {}", context.getCqn());
130+
CqnDelete cqnFlattenedEntity =
131+
CQL.copy(
132+
context.getCqn(),
133+
new EntityFlattener());
132134
CqnDelete cqnInactiveEntity =
133135
CQL.copy(
134-
context.getCqn(), new ActiveEntityModifier(isActiveEntity, entity.getQualifiedName()));
136+
cqnFlattenedEntity,
137+
new ActiveEntityModifier(isActiveEntity, entity.getQualifiedName()));
135138
logger.debug("Modified CQN: {}", cqnInactiveEntity);
136139
return attachmentsReader.readAttachments(
137140
context.getModel(), (CdsEntity) entity, cqnInactiveEntity);
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
* 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
21+
* segment. This is particularly useful when dealing with composition relationships that need
22+
* to be flattened for direct entity access.
23+
*/
24+
class EntityFlattener implements Modifier {
25+
26+
private static final Logger logger = LoggerFactory.getLogger(EntityFlattener.class);
27+
28+
EntityFlattener() {}
29+
30+
@Override
31+
public CqnStructuredTypeRef ref(CqnStructuredTypeRef original) {
32+
33+
RefBuilder<StructuredTypeRef> ref = CQL.copy(original);
34+
RefSegment rootSegment = ref.rootSegment();
35+
36+
// Flatten the query when it's nested, e.g. Books -> Chapters -> Chapters.images
37+
if (ref.segments().size() > 1) {
38+
logger.debug("Removing nested segments for ref {}", rootSegment);
39+
} else {
40+
// No nested segments, return as is
41+
return original;
42+
}
43+
44+
// Get the filter from the last segment before removing segments
45+
RefSegment lastSegment = ref.segments().get(ref.segments().size() - 1);
46+
CqnPredicate lastFilter = null;
47+
if (lastSegment.filter().isPresent()) {
48+
Modifier modifier = new EntityFlattener();
49+
lastFilter = CQL.copy(lastSegment.filter().get(), modifier);
50+
}
51+
52+
while (ref.segments().size() > 1) {
53+
ref.segments().remove(ref.segments().size() - 1);
54+
}
55+
56+
// Apply the filter to the root segment if there was a filter on the last segment
57+
if (lastFilter != null) {
58+
rootSegment.filter(lastFilter);
59+
}
60+
61+
// Create a direct reference to the target entity with the modified filter
62+
return ref.build();
63+
}
64+
}
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 org.assertj.core.api.Assertions.assertThat;
7+
import org.junit.jupiter.api.Test;
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 static com.sap.cds.services.draft.Drafts.IS_ACTIVE_ENTITY;
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()).contains("{\"ref\":[\"IsActiveEntity\"]},\"=\",{\"val\":false}]}");
29+
// Should contain root entity
30+
assertThat(result.toString()).contains("AdminService.Books");
31+
// Should not contain the original nested structure
32+
assertThat(result.toString()).doesNotContain("\"to\":");
33+
}
34+
35+
@Test
36+
void nestedReferenceWithoutFilterIsFlattened() {
37+
// Create a nested reference with a filter on the last segment: AdminService.Books -> covers
38+
// (with filter)
39+
CqnSelect select = Select.from("AdminService.Books", c -> c.to("covers").to("anoter_nesting"));
40+
41+
// The target entity name should be the flattened version
42+
var result = CQL.copy(select, new EntityFlattener());
43+
// Should contain root entity
44+
assertThat(result.toString()).contains("\"ref\":[\"AdminService.Books\"]");
45+
System.out.println(result.toString());
46+
// Should not contain the original nested structure
47+
assertThat(result.toString()).doesNotContain("\"to\":");
48+
}
49+
50+
}

0 commit comments

Comments
 (0)