Skip to content

Commit 588490f

Browse files
authored
Merge pull request #460 from cap-java/createAttachmentInActiveEntity
Create attachment in active entity
2 parents 92597d9 + 3fe7198 commit 588490f

7 files changed

Lines changed: 1448 additions & 40 deletions

File tree

.github/workflows/singleTenant_deploy_and_Integration_test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,4 +235,4 @@ jobs:
235235
236236

237237

238-
238+

README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ This plugin can be consumed by the CAP application deployed on BTP to store thei
2525
- Attachment changelog: Provides the capability to view complete audit trail of attachments.
2626
- Localization of error messages and UI fields: Provides the capability to have the UI fields and error messages translated to the local language of the leading application.
2727
- Attachment Upload Status: Upload Status is the new field which displays the upload status of attachment when being uploaded.
28+
- Active entity attachment creation: Provides the capability to create attachments directly on active (non-draft) entities.
2829

2930
## Table of Contents
3031

@@ -45,6 +46,7 @@ This plugin can be consumed by the CAP application deployed on BTP to store thei
4546
- [Support for Edit of Link type attachments](#support-for-edit-of-link-type-attachments)
4647
- [Support for Localization](#support-for-localization)
4748
- [Support for Attachment Upload Status](#support-for-attachment-upload-status)
49+
- [Support for Attachment creation in Active Entities](#support-for-attachment-creation-in-active-entities)
4850
- [Known Restrictions](#known-restrictions)
4951
- [Support, Feedback, Contributing](#support-feedback-contributing)
5052
- [Code of Conduct](#code-of-conduct)
@@ -1332,6 +1334,64 @@ Success;Success;3
13321334
Failed;Scan Failed;2
13331335
```
13341336

1337+
### Support for Attachment Creation in Active Entities
1338+
1339+
By default, the SDM CAP plugin handles attachment creation through the **draft flow** — attachments are first created on a draft entity and later activated. This feature adds support for creating attachments **directly on active entities**, which is useful in scenarios where the parent entity bypasses the draft lifecycle (e.g., programmatic entity creation, background jobs, or APIs that operate on active records).
1340+
1341+
### How It Works
1342+
1343+
When an attachment is created, the plugin automatically determines whether the parent entity is in a **draft** or **active** context:
1344+
1345+
1. **Draft detection:** The plugin queries the parent entity's draft table to check if the parent record exists there. If it does, the standard draft flow is used.
1346+
2. **Active entity flow:** If the parent record is **not** found in the draft table, the plugin treats it as an active entity context. In this case:
1347+
- The attachment content is uploaded to the SAP Document Management repository.
1348+
- The SDM metadata (`objectId`, `folderId`, `repositoryId`, etc.) is temporarily stored in-memory.
1349+
- After the framework completes the database INSERT, an `@After` handler updates the active entity record with the SDM metadata.
1350+
3. **Backwards compatibility:** If the context cannot be determined (e.g., the model has no draft table), the plugin defaults to the draft flow to ensure existing applications continue to work without changes.
1351+
1352+
### Key Behavior
1353+
1354+
- **Automatic detection:** No configuration is required. The plugin automatically detects whether to use the draft or active entity flow based on the parent entity's presence in the draft table.
1355+
- **Duplicate handling:** If an attachment with the same filename already exists on the active entity, the plugin gracefully handles the duplicate by reusing the existing attachment record.
1356+
1357+
### Usage in Leading Applications
1358+
1359+
To create attachments on active entities, the leading application needs to trigger an `INSERT` on the attachment entity through the `ApplicationService` (or `DraftService`, which extends it). The plugin intercepts the content automatically and routes it through the active entity flow.
1360+
1361+
#### Steps
1362+
1363+
1. **Build the attachment data** with the required fields:
1364+
1365+
| Field | Type | Description |
1366+
|------------|---------------|--------------------------------------------------|
1367+
| `ID` | `String` | Unique identifier (e.g., `UUID.randomUUID()`) |
1368+
| `up__ID` | `String` | The parent entity's ID |
1369+
| `fileName` | `String` | The attachment filename |
1370+
| `mimeType` | `String` | The MIME type of the content |
1371+
| `content` | `InputStream` | An `InputStream` containing the file content |
1372+
1373+
2. **Execute the INSERT** using the `ApplicationService`. See this [example](https://github.com/cap-java/sdm/blob/e89c3c4f9fee6a18b20dfec2650b1d05ff244bc3/cap-notebook/demoapp/srv/src/main/java/customer/demoapp/handlers/AdminServiceHandler.java#L142)
1374+
1375+
```java
1376+
import com.sap.cds.ql.Insert;
1377+
import java.io.InputStream;
1378+
import java.util.HashMap;
1379+
import java.util.Map;
1380+
import java.util.UUID;
1381+
1382+
// Build attachment data
1383+
Map<String, Object> attachmentData = new HashMap<>();
1384+
attachmentData.put("ID", UUID.randomUUID().toString());
1385+
attachmentData.put("up__ID", parentEntityId);
1386+
attachmentData.put("fileName", "report.pdf");
1387+
attachmentData.put("mimeType", "application/pdf");
1388+
attachmentData.put("content", inputStream);
1389+
1390+
// Insert into the attachment entity via ApplicationService
1391+
applicationService.run(
1392+
Insert.into("MyService.MyEntity.attachments").entry(attachmentData)
1393+
);
1394+
13351395
## Known Restrictions
13361396

13371397
- UI5 Version 1.135.0: This version causes error in upload of attachments.

sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import com.sap.cds.sdm.model.SDMCredentials;
1515
import com.sap.cds.sdm.persistence.DBQuery;
1616
import com.sap.cds.sdm.service.SDMService;
17+
import com.sap.cds.sdm.service.handler.SDMAttachmentsServiceHandler;
1718
import com.sap.cds.sdm.utilities.SDMUtils;
1819
import com.sap.cds.services.ServiceException;
1920
import com.sap.cds.services.cds.ApplicationService;
@@ -56,6 +57,52 @@ public SDMCreateAttachmentsHandler(
5657
this.dbQuery = dbQuery;
5758
}
5859

60+
/**
61+
* After handler for ApplicationService CREATE to update active entity attachments with SDM
62+
* metadata (objectId, folderId, repositoryId, etc.) after the record has been INSERTed.
63+
*
64+
* <p>During active entity attachment creation, the AttachmentService @On handler uploads to SDM
65+
* and stores metadata in a ThreadLocal. The framework then INSERTs the record with contentId (set
66+
* via finalizeContext). This @After handler runs AFTER the INSERT, so the record exists and can
67+
* be UPDATEd with the remaining SDM metadata.
68+
*/
69+
@After
70+
@HandlerOrder(HandlerOrder.LATE)
71+
public void updateActiveEntitySdmMetadata(CdsCreateEventContext _context) {
72+
handleUpdateActiveEntitySdmMetadata();
73+
}
74+
75+
private void handleUpdateActiveEntitySdmMetadata() {
76+
Map<String, Object> metadata = SDMAttachmentsServiceHandler.SDM_METADATA_THREADLOCAL.get();
77+
if (metadata == null) {
78+
return;
79+
}
80+
try {
81+
SDMAttachmentsServiceHandler.SDM_METADATA_THREADLOCAL.remove();
82+
com.sap.cds.reflect.CdsEntity attachmentEntity =
83+
(com.sap.cds.reflect.CdsEntity) metadata.get("attachmentEntity");
84+
if (attachmentEntity == null) {
85+
logger.warn("No attachmentEntity in ThreadLocal metadata, skipping post-INSERT update");
86+
return;
87+
}
88+
CmisDocument cmisDocument = new CmisDocument();
89+
cmisDocument.setAttachmentId((String) metadata.get("attachmentId"));
90+
cmisDocument.setObjectId((String) metadata.get("objectId"));
91+
cmisDocument.setFolderId((String) metadata.get("folderId"));
92+
cmisDocument.setMimeType((String) metadata.get("mimeType"));
93+
cmisDocument.setUploadStatus((String) metadata.get("uploadStatus"));
94+
logger.info(
95+
"Post-INSERT: Updating active entity attachment {} with objectId {}",
96+
cmisDocument.getAttachmentId(),
97+
cmisDocument.getObjectId());
98+
dbQuery.addAttachmentToDraft(attachmentEntity, persistenceService, cmisDocument);
99+
logger.info("Post-INSERT: Successfully updated active entity attachment with SDM metadata");
100+
} catch (Exception e) {
101+
logger.error(
102+
"Failed to update active entity SDM metadata after INSERT: {}", e.getMessage(), e);
103+
}
104+
}
105+
59106
@Before
60107
@HandlerOrder(HandlerOrder.DEFAULT)
61108
public void processBefore(CdsCreateEventContext context, List<CdsData> data) throws IOException {
@@ -140,6 +187,9 @@ public void processAfter(CdsCreateEventContext context, List<CdsData> data) {
140187
@HandlerOrder(OrderConstants.Before.CHECK_CAPABILITIES - 500)
141188
public void preserveUploadStatus(CdsCreateEventContext context, List<CdsData> data) {
142189
// Preserve uploadStatus before CDS removes readonly fields
190+
logger.debug(
191+
"Preserving readonly fields (uploadStatus) for entity: {} before CDS capability check",
192+
context.getTarget().getQualifiedName());
143193
SDMUtils.preserveReadonlyFields(context.getTarget(), data);
144194
}
145195

0 commit comments

Comments
 (0)