Skip to content

TEST: feat: add @Validation.MinItems and @Validation.MaxItems for attachment compositions#752

Open
lisajulia wants to merge 1 commit intomainfrom
feat/validation-min-max-items
Open

TEST: feat: add @Validation.MinItems and @Validation.MaxItems for attachment compositions#752
lisajulia wants to merge 1 commit intomainfrom
feat/validation-min-max-items

Conversation

@lisajulia
Copy link
Contributor

Summary

  • Adds @Validation.MinItems / @Validation.MaxItems annotation support on attachment compositions
  • In draft mode (entity not yet activated) violations produce warnings; on active entities and during DRAFT_SAVE they produce errors
  • Annotation values can be integers (and the framework is ready for property references / dynamic expressions as a follow-up)
  • i18n override: apps provide attachment_minItems_{EntityName}_{compositionName} in their own bundle to customise the message per property

New files

File Purpose
ItemCountValidationHelper Shared utility — iterates compositions, reads annotations, issues warn/error messages targeting the table element
ItemsCountValidationHandler @Before LATE on ApplicationService CREATE + UPDATE; warns on draft (IsActiveEntity=false), errors on active
DraftSaveItemsCountValidationHandler @Before LATE on DraftService DRAFT_SAVE; always errors

Test plan

  • ItemsCountValidationHandlerTest — 13 unit tests (active errors, draft warnings, no-violation, absent composition)
  • DraftSaveItemsCountValidationHandlerTest — 7 unit tests (always errors, never warns)
  • RegistrationTest — updated handler count 8 → 10
  • Full build: mvn test -pl cds-feature-attachments passes (345 tests, 0 failures)

@hyperspace-insights
Copy link
Contributor

Summary

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


Add @Validation.MinItems and @Validation.MaxItems Support for Attachment Compositions

New Feature

✨ Introduces @Validation.MinItems and @Validation.MaxItems annotation support on attachment compositions. In draft mode (entity not yet activated), violations produce warnings; on active entities and during DRAFT_SAVE, they produce errors. Apps can override i18n messages per property using keys like attachment_minItems_{EntityName}_{compositionName}.

Changes

  • ItemCountValidationHelper: New shared utility class that iterates compositions, reads @Validation.MinItems / @Validation.MaxItems annotations, and issues warning or error messages targeting the composition table element depending on draft state.
  • ItemsCountValidationHandler: New @Before LATE handler on ApplicationService for CREATE and UPDATE events. Issues warnings for draft data (IsActiveEntity=false) and errors for active entities.
  • DraftSaveItemsCountValidationHandler: New @Before LATE handler on DraftService for DRAFT_SAVE events. Always raises errors since draft activation transitions to an active entity.
  • Registration.java: Registers both new handlers — ItemsCountValidationHandler for application services and DraftSaveItemsCountValidationHandler for draft services.
  • i18n.properties: Added default messages for min/max item count violations: attachment_minItems and attachment_maxItems.
  • db-model.cds (test resource): Added @Validation.MinItems and @Validation.MaxItems annotations to EventItems.sizeLimitedAttachments and Roots.attachments for use in tests.
  • RegistrationTest.java: Updated expected handler count from 8 to 10 and added assertions for the two new handler classes.
  • ItemsCountValidationHandlerTest.java: New test class with 13 unit tests covering active errors, draft warnings, no-violation, and absent composition scenarios.
  • DraftSaveItemsCountValidationHandlerTest.java: New test class with 7 unit tests verifying that DRAFT_SAVE always produces errors regardless of draft flags.

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

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

  • LLM: anthropic--claude-4.6-sonnet
  • Output Template: Default Template
  • Summary Prompt: Default Prompt
  • Event Trigger: pull_request.opened
  • Correlation ID: a9eab0b0-1eda-11f1-8aa6-fcb090456ed9

💌 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.

Copy link
Contributor

@hyperspace-insights hyperspace-insights bot left a comment

Choose a reason for hiding this comment

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

The PR introduces a well-structured feature for @Validation.MinItems/@Validation.MaxItems support with clear separation between draft and active-entity handling. However, there are several substantive correctness issues: the count aggregation in ItemCountValidationHelper sums items across all records in the batch rather than validating each record independently, the isDraft detection in ItemsCountValidationHandler uses anyMatch which causes active records in a mixed batch to be validated as draft (warnings instead of errors), and the raw (Number) cast can throw ClassCastException for non-numeric annotation values. These should be addressed before merging.

PR Bot Information

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

  • Correlation ID: a9eab0b0-1eda-11f1-8aa6-fcb090456ed9
  • LLM: anthropic--claude-4.6-sonnet
  • Event Trigger: pull_request.opened

Comment on lines +22 to +33
boolean presentInPayload = data.stream().anyMatch(d -> d.containsKey(compositionName));
if (!presentInPayload) {
return;
}

long count = data.stream()
.filter(d -> d.containsKey(compositionName))
.mapToLong(d -> {
Object val = d.get(compositionName);
return val instanceof List<?> list ? list.size() : 0L;
})
.sum();
Copy link
Contributor

Choose a reason for hiding this comment

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

Logic Error: Count aggregation sums items across all records, not per-record, making per-entity validation incorrect

count is computed as the sum of items from all records in the data list. For example, if there are 3 records in the batch each with 4 attachments, count will be 12 even though each individual entity only has 4. The validation should check the constraint per data row, not across the entire payload.

Consider iterating over each item in data individually and validating its composition list count independently:

data.stream()
    .filter(d -> d.containsKey(compositionName))
    .forEach(d -> {
      Object val = d.get(compositionName);
      long count = val instanceof List<?> list ? list.size() : 0L;

      composition.<Object>findAnnotation("Validation.MinItems").ifPresent(annotation -> {
        Object value = annotation.getValue();
        if (value instanceof Boolean) return;
        int limit = ((Number) value).intValue();
        if (count < limit) {
          String msgKey = MIN_ITEMS_KEY + "_" + entity.getName() + "_" + compositionName;
          issueMessage(isDraft, messages, msgKey, limit, entity.getQualifiedName(), compositionName);
        }
      });

      composition.<Object>findAnnotation("Validation.MaxItems").ifPresent(annotation -> {
        Object value = annotation.getValue();
        if (value instanceof Boolean) return;
        int limit = ((Number) value).intValue();
        if (count > limit) {
          String msgKey = MAX_ITEMS_KEY + "_" + entity.getName() + "_" + compositionName;
          issueMessage(isDraft, messages, msgKey, limit, entity.getQualifiedName(), compositionName);
        }
      });
    });

Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

composition.<Object>findAnnotation("Validation.MinItems").ifPresent(annotation -> {
Object value = annotation.getValue();
if (value instanceof Boolean) return; // bare annotation without value → skip
int limit = ((Number) value).intValue();
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: ClassCastException possible when annotation value is not a Number

The cast ((Number) value).intValue() assumes the annotation value is always a Number. However, CDS annotations could carry a String (e.g. a property path for a dynamic reference). This will throw a ClassCastException at runtime for any non-numeric value that isn't Boolean.

Consider adding a type guard before the cast:

if (!(value instanceof Number)) return; // non-numeric value (e.g. path reference) → skip
int limit = ((Number) value).intValue();

Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

Comment on lines +42 to +43
private boolean isDraftData(List<CdsData> data) {
return data.stream().anyMatch(d -> Boolean.FALSE.equals(d.get("IsActiveEntity")));
Copy link
Contributor

Choose a reason for hiding this comment

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

Logic Error: isDraft detection uses anyMatch, so a mixed batch of draft and active records is treated as all-draft

If a data list contains both draft records (IsActiveEntity=false) and active records (IsActiveEntity=true or absent), anyMatch returns true and all records in the batch are validated with isDraft=true (warnings only). Active records in the same batch would silently miss the required error.

The draft/active distinction should be evaluated per record, not once for the whole list. This is closely related to the per-record aggregation fix in ItemCountValidationHelper; passing isDraft per row into the helper (or moving the isDraft check inside the helper loop) would resolve both issues.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

int limit = ((Number) value).intValue();
if (count < limit) {
String msgKey = MIN_ITEMS_KEY + "_" + entity.getName() + "_" + compositionName;
issueMessage(isDraft, messages, msgKey, limit, entity.getQualifiedName(), compositionName);
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: The same violation message key is issued once per data entry that contains the composition, but the deduplication relies solely on the message key string. If a batch contains multiple root records that each violate the constraint, only one error/warning may be surfaced (depending on framework message de-duplication). The target lambda e -> e.to(compositionName) does not include the record's key, so consumers cannot identify which record triggered the violation.

Consider including a record identifier in the target expression so each message is uniquely targeted to its offending row.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

…ttachment compositions

Introduces item-count validation for attachment compositions annotated with
@Validation.MinItems and @Validation.MaxItems. In draft mode violations
produce warnings (draft allows an invalid state); on active entities and
during draft activation (DRAFT_SAVE) they produce errors.

- ItemCountValidationHelper: shared utility that reads annotations, counts
  items in the payload, and issues warn/error messages targeting the
  composition table so Fiori elements can highlight it. Supports integer
  values, and the i18n override pattern
  (attachment_minItems_{Entity}_{property}) for per-property message
  customisation.
- ItemsCountValidationHandler: @before LATE on ApplicationService CREATE
  and UPDATE; detects draft state via IsActiveEntity=false in payload.
- DraftSaveItemsCountValidationHandler: @before LATE on DraftService
  DRAFT_SAVE; always errors since activation must enforce constraints.
- Registration: registers both new handlers alongside the existing ones.
- i18n: adds attachment_minItems and attachment_maxItems message keys.
@lisajulia lisajulia force-pushed the feat/validation-min-max-items branch from 99c05b6 to 9680ab6 Compare March 13, 2026 12:48
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.

1 participant