Skip to content

Support inline single attachments via Attachment type - #768

Open
busehalis-sap wants to merge 80 commits into
mainfrom
feature/support-single-attachments
Open

Support inline single attachments via Attachment type#768
busehalis-sap wants to merge 80 commits into
mainfrom
feature/support-single-attachments

Conversation

@busehalis-sap

@busehalis-sap busehalis-sap commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Introduce the sap.attachments.Attachment structured type for single-file attachment fields directly on an entity (e.g. profileIcon : Attachment), complementing the existing composition-based Attachments aspect for multi-file scenarios
  • All existing plugin features (malware scanning, status tracking, storage targets, max file size validation, MIME type validation) work transparently for inline attachments
  • Remove Pitest mutation testing from the build pipeline and CI

CDS Model Changes

  • Added type Attachment : MediaData {} for single-file use cases.
  • Moved the managed aspect from MediaData to Attachments so inline fields don't inherit createdBy/modifiedBy on the parent entity.
  • Added Core.ContentDisposition.Filename and Core.ContentDisposition.Type annotations.
  • The Core.MediaType annotation now uses path expression syntax (mimeType).

Application Code Changes

Core detection logic (ApplicationHandlerHelper) — New methods to discover inline attachment fields by scanning for flattened prefix_content elements with _is_media_data annotations. MEDIA_CONTENT_FILTER now matches both composition-based and inline attachment content fields. extractInlineAttachment() strips the prefix from flattened column names and tags results with an _inlinePrefix marker for matching. condenseAttachments() extended to extract inline data from parent entity rows.

Event processing (ModifyAttachmentEvent and implementations) — All event implementations (Create, Update, MarkAsDeleted, DoNothing) gain an Optional<String> inlinePrefix parameter. Create and delete events use resolveField() / prefixed field names to read and write the correct columns. CreateAttachmentEvent manually extracts fileName/mimeType from HTTP headers for inline attachments since CDS flattening breaks path references. MarkAsDeletedAttachmentEvent additionally clears mimeType/fileName for inline types.

Read path (ReadAttachmentsHandler, BeforeReadItemsModifier) — Before-read now discovers inline prefixes and ensures prefix_contentId/prefix_status/prefix_scannedAt columns are selected when prefix_content is explicitly queried. After-read extracts inline attachment data and feeds it through status verification and lazy proxy wrapping. Re-scan transitions use prefixed column names.

Write path (UpdateAttachmentsHandler, ModifyApplicationHandlerHelper) — Early-return optimization now also checks for inline attachment fields in the data. Size limit resolution falls back to prefix_content elements. Existing attachment matching uses the prefix marker instead of entity keys for inline types.

Draft handling (DraftPatchAttachmentsHandler, DraftCancelAttachmentsHandler, ReadonlyDataContextEnhancer) — Draft patch extracts existing inline data from DB using prefixed columns and explicitly persists mimeType/fileName via PersistenceService (the framework's DRAFT_PATCH handler only persists readonly fields). Draft cancel extended to detect and handle prefixed contentId elements. Readonly field backup/restore now supports prefix_DRAFT_READONLY_CONTEXT keys and preserves fileName.

Composition tree traversal (AssociationCascader, AttachmentsReader) — Entities with inline attachments are recorded as media entity paths but traversal continues into their compositions (unlike composition-based attachment entities which are leaf nodes). Attachment reader injects inline columns into both root selects and nested expand clauses.

Malware scanning — All scanner interfaces and implementations (AttachmentMalwareScanner, AsyncMalwareScanExecutor, EndTransactionMalwareScanRunner, DefaultAttachmentMalwareScanner) thread inlinePrefix through to build correct SELECT/UPDATE statements with prefixed column names.

Minor changesMediaTypeService.resolveMimeType() returns null instead of application/octet-stream for unknown types. MediaTypeResolver uses findElement() to avoid NPE on entities without a content element. CreateAttachmentInput record gains an inlinePrefix field.

Unit Tests

  • New tests in ApplicationHandlerHelperTest, ReadAttachmentsHandlerTest, ModifyApplicationHandlerHelperTest, BeforeReadItemsModifierTest, DefaultAttachmentMalwareScannerTest for inline detection, prefix resolution, size limits, read modifier, and scan with prefixed columns
  • Extensive inline-specific tests in CreateAttachmentEventTest (header extraction, prefixed writes, fallbacks) and MarkAsDeletedAttachmentEventTest (prefixed field clearing)
  • ReadonlyDataContextEnhancerTest rewritten to cover both composition and inline preserve/restore
  • DraftCancelAttachmentsHandlerTest extended with inline cancel scenarios
  • CDS test model extended with profilePicture on Roots and a new InlineOnly entity
  • All existing tests updated for the new inlinePrefix parameter

Integration Tests

  • SingleAttachmentDraftTest (new) — Full draft lifecycle: create+activate, create+cancel, update+activate, delete+activate, delete+cancel, draft content reads, no-change re-activation, error rollback, Content-Disposition header persistence, update+cancel cleanup
  • SingleAttachmentNonDraftTest (new) — Full non-draft lifecycle: CRUD on root-level inline attachments, multiple inline fields on same entity, inline on composition children, deep delete/update cascading, concurrent inline and composition-based attachments, header handling, size limit enforcement, error rollback

Pipeline / General Changes

  • Removed Pitest mutation testing from Maven config, CI action, and documentation
  • Bookshop sample extended with profileIcon/coverImage inline attachments and Fiori UI facets; bumped dependency versions
  • README documents the Attachment type, flattened column naming, UI annotations, and combining both patterns

@hyperspace-pr-bot

Copy link
Copy Markdown
Contributor

Summary

The following content is AI-generated and provides a summary of the pull request:


Support Inline Single Attachments via Attachment Type

New Feature: Adds support for inline (single) attachments using the Attachment type directly on an entity field (e.g., profileIcon: Attachment), complementing the existing Composition of many Attachments pattern.

Changes

When an entity declares a field as field: Attachment, CDS flattens the Attachment fields into the parent table with a prefix (e.g., coverImage_content, coverImage_contentId). The handlers now detect this via the @_is_media_data annotation at the element level, extract the prefixed data into normalized Attachments maps, and inject parent entity keys for downstream matching.

CDS Model:

  • attachments.cds: Introduces a new type Attachment @(_is_media_data) and makes MediaData extend it, enabling inline usage.
  • attachments-annotations.cds: Annotates Attachment.content with a static Core.MediaType to expose it as Edm.Stream.

Handler Infrastructure:

  • ApplicationHandlerHelper.java: New utility methods for inline attachment detection: getInlineAttachmentFieldNames, getInlineAttachmentPrefix, extractInlineAttachment, isInlineAttachmentContentField, hasInlineAttachmentElements, isDirectMediaEntity. Updated MEDIA_CONTENT_FILTER and condenseAttachments to support both inline and composition-based patterns.
  • BeforeReadItemsModifier.java: Extended to add prefixed contentId, status, and scannedAt fields to SELECT queries for inline attachments.
  • ReadAttachmentsHandler.java: Detects inline prefix during read/after-read processing and uses it for status transitions and malware scan triggering.
  • UpdateAttachmentsHandler.java: associationsAreUnchanged now also checks inline prefixed fields.
  • ReadonlyDataContextEnhancer.java: Handles preserve/restore of readonly fields using prefixed keys for inline attachments.

Modify Events:

  • CreateAttachmentEvent.java: Detects inline prefix, reads mimeType/fileName from prefixed fields, and writes contentId/status/scannedAt back using prefixed keys.
  • MarkAsDeletedAttachmentEvent.java: Clears all prefixed fields (including mimeType, fileName) on deletion for inline attachments.

Draft Handlers:

  • DraftPatchAttachmentsHandler.java: Extracts inline attachment data from flattened DB results and injects parent keys for event matching.
  • DraftCancelAttachmentsHandler.java: contentIdFilter and deepSearchForAttachments updated to detect inline attachment fields.

Malware Scanning:

  • AsyncMalwareScanExecutor, AttachmentMalwareScanner, EndTransactionMalwareScanProvider, EndTransactionMalwareScanRunner: Added Optional<String> inlinePrefix parameter throughout the malware scan pipeline.
  • DefaultAttachmentMalwareScanner.java: All read/update queries now use resolved (prefixed) column names for inline attachments.
  • DefaultAttachmentsServiceHandler.java: Detects inline prefix post-create and passes it to the malware scan provider.

Other:

  • AssociationCascader.java: Recognizes entities with inline attachment elements and adds them to the traversal path.
  • AttachmentsReader.java: Adds inline attachment columns (prefix_contentId, prefix_status) directly to SELECT queries.
  • MediaTypeResolver.java, AttachmentValidationHelper.java: Use findElement (safe) instead of getElement and isDirectMediaEntity for composition-based checks.
  • Registration.java: Updated EndTransactionMalwareScanRunner instantiation to pass Optional.empty() for inlinePrefix.

Bookshop Sample:

  • attachments.cds, fiori-service.cds: Added profileIcon: Attachment inline attachment to the Books entity with UI facet.
  • pom.xml: Updated cds-feature-attachments version to 1.4.0-SNAPSHOT.

Tests:

  • New and extended unit tests covering all new inline paths: read, create, update, delete, patch, cancel, malware scanning, readonly field preservation, and BeforeReadItemsModifier behavior.
  • New ReadonlyDataContextEnhancerTest.java covering both composition-based and inline preserve/restore scenarios.

  • 🔄 Regenerate and Update Summary
  • ✏️ Insert as PR Description (deletes this comment)
  • 🗑️ Delete comment
PR Bot Information

Version: 1.19.3 | 📖 Documentation | 🚨 Create Incident | 💬 Feedback

  • Output Template: Default Template
  • LLM: anthropic--claude-4.6-sonnet
  • Summary Prompt: Default Prompt
  • Event Trigger: pull_request.opened
  • Correlation ID: bbb28610-2bff-11f1-8fd9-1dd0dd2b50a5

💌 Have ideas or want to contribute? Create an issue and share your thoughts with us!
📑 Check out the documentation for more information.
📬 Subscribe to the Hyperspace PR Bot DL to get the latest announcements and pilot features!

Made with ❤️ by Hyperspace.

@hyperspace-pr-bot hyperspace-pr-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR introduces inline single-attachment support via the Attachment type, which is a meaningful extension of the existing composition-based approach. However, there are several substantive correctness issues that need attention before merging: a recurring "first prefix wins" bug across CreateAttachmentEvent, MarkAsDeletedAttachmentEvent, and DefaultAttachmentsServiceHandler that will produce wrong behavior for entities with more than one inline attachment field; an inverted/dead branch in ModifyApplicationHandlerHelper for contentId resolution; missing root-entity key columns in AttachmentsReader when both inline and expand columns are present; and a conflation in ReadAttachmentsHandler.getAttachmentAssociations that incorrectly treats inline-only root entities as direct media association roots. These should be resolved before the feature is considered production-ready.

PR Bot Information

Version: 1.19.3 | 📖 Documentation | 🚨 Create Incident | 💬 Feedback

  • Event Trigger: pull_request.opened
  • Correlation ID: bbb28610-2bff-11f1-8fd9-1dd0dd2b50a5
  • LLM: anthropic--claude-4.6-sonnet

@lisajulia lisajulia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! 👍
Please update the README file as well 📖 - then I'll approve ;)

@busehalis-sap
busehalis-sap requested a review from a team as a code owner April 13, 2026 12:38
@busehalis-sap
busehalis-sap force-pushed the feature/support-single-attachments branch from 8a97051 to f041e27 Compare April 13, 2026 18:27
busehalis-sap and others added 8 commits April 14, 2026 10:02
…e-attachments

# Conflicts:
#	cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/service/malware/DefaultAttachmentMalwareScanner.java
#	cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/applicationservice/helper/ReadonlyDataContextEnhancerTest.java
…er mock

The feature branch added an inlinePrefix parameter to
EndTransactionMalwareScanProvider.getChangeSetListener. A test added
on main was still using the 2-arg overload.
@Schmarvinius

Copy link
Copy Markdown
Contributor

Blocked until bug resolved with cds-services!

Schmarvinius and others added 19 commits June 19, 2026 15:41
Adds extractFrom(Map<String, Object> values) to AttachmentContext, which
extracts an Attachments view from raw data - either wrapping directly
(composition) or stripping the prefix (inline).

Eliminates the remaining if/else branching in:
- DeleteAttachmentsHandler
- ReadAttachmentsHandler
- DraftCancelAttachmentsHandler

Handlers now simply call attachmentCtx.extractFrom(path.target().values())
instead of branching on isInline() and manually calling extractInlineAttachment.
The parameter holds the attachment entity's own keys (for composition)
or the parent entity's keys (for inline). Calling it 'parentKeys' was
misleading for the composition case. 'keys' is neutral and accurate.
… contentCanBeReadFromDraft to prevent flaky timeout
}

/** Context for composition-based attachments where the attachment is its own entity. */
record Composition() implements AttachmentContext {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Records in java are used to represent value objects. Creating a record which does not hold any data is no idiomatic. A final class should be used instead.

* Context for inline attachments where the structured type fields are flattened into the parent
* entity with a prefix (e.g. {@code "avatar_content"}, {@code "avatar_contentId"}).
*/
record Inline(String prefix) implements AttachmentContext {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Records in java are used to represent value objects. Creating a record which does not hold any data is no idiomatic. A final class should be used instead.

* and write that attachment's fields in the data map, and how to match it against existing
* attachment data.
*/
public sealed interface AttachmentContext {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming: AttachmentContext is not a descriptive name to me.

Everything here is about attachments and "context" is a very generic term. There are also other things named context in this context...

From the subscription, it sounds as if it is a FieldAccessor.

MEDIA_CONTENT_FILTER only matches _content suffix elements, so each
inline prefix triggers the validator exactly once — the seenPrefixes
HashSet never had any effect.
persistInlineAttachmentMetadata was building a WHERE clause without IsActiveEntity, causing the UPDATE to match both draft and active shadow rows in the draft table. Added IsActiveEntity = false to ensure only the draft row is updated.
@busehalis-sap
busehalis-sap force-pushed the feature/support-single-attachments branch from ae9e2aa to 930567a Compare August 12, 2026 11:35
…ent fields

Inline attachment fields (e.g. avatar_content) are flattened into the
parent entity and are not reachable via findElement(content). Add
fetchInlineAcceptableMediaTypes to scan _content-suffixed elements and
register their annotations under entityName#prefix keys. Update
AttachmentDataExtractor to collect inline file names under the same keys
so per-field media type validation works for both composition and inline
attachments.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants