Skip to content

[Version: 3.13.1] Attachments no longer deleted when omitted on non-draft PATCH operations #505

Description

@gab-john

Describe the bug

Our team is currently upgrading from 3.6.0, but we noticed that attachments are no longer being marked for deletion on S3 when they are omitted from arrays in PATCH requests.

We have a structure similar to this:

Order
  N OrderItems
    N OrderItemAttachments

When the user sends a replacement array for attachments, these attachments no longer appear in req.attachmentsToDelete, as reported by our tests.

Failing scenarios:

  • (Scenario 1) PATCH OrderItem with attachments: []: All attachments should be deleted, but req.attachmentsToDelete is undefined.
  • (Scenario 2) PATCH Order with an existing OrderItem, but attachments: []: Same as above.

Edit: Maybe also relevant for testing, PATCH Order with a missing OrderItem (e.g., items: []) should also delete that item's attachments.

Working scenarios (kept for contrast):

  • (Scenarios 3, 4) PATCH keeping attachments: nothing to delete. Currently passes only
    because non-draft PATCH is not being handled at all, but will become a real test case once a fix is developed.
  • (Scenarios 5, 6) DELETE on OrderItem or Order: Cascade deletion works correctly at both nesting levels, and req.attachmentsToDelete is populated.

Note: Our scenario involves entities that are not draft-enabled, with two levels of nesting, as originally reported by @viniciuslora.

To Reproduce

I've made a minimal example that covers both nested and double-nested PATCH scenarios.

cat-service.js (used to inspect req.attachmentsToDelete by logging it to the console):

const cds = require('@sap/cds');

module.exports = cds.service.impl(function () {
  this.after('*', '*', (_, req) => {
    console.log('attachmentsToDelete:', req.attachmentsToDelete);
  });
});

cat-service.cds:

using { Attachments } from '@cap-js/attachments';

entity dbOrders {
  key ID    : UUID;
  items     : Composition of many dbOrderItems on items.order = $self;
}

entity dbOrderItems {
  key ID      : UUID;
  order       : Association to dbOrders;
  attachments : Composition of many dbOrderItemAttachments on attachments.item = $self;
}

entity dbOrderItemAttachments : Attachments {
  item : Association to dbOrderItems;
}

service CatalogService {
  entity Orders             as projection on dbOrders;
  entity OrderItems         as projection on dbOrderItems;
  entity OrderItemAttachments as projection on dbOrderItemAttachments;
}

Here's the REST Client file with all scenarios above, with comments explaining the expected behavior.

Note: Please run the "Test setup" part before each of the scenario.

scenarios.rest:

# ----------------------------------------
# Test setup
# ----------------------------------------

### DELETE Order (to reset state between tests, will return 404 at first)
DELETE http://localhost:4004/odata/v4/catalog/Orders(00000000-0000-0000-0000-000000000001)

### Create Order → Item → Attachment
POST http://localhost:4004/odata/v4/catalog/Orders
Content-Type: application/json

{
  "ID": "00000000-0000-0000-0000-000000000001",
  "items": [
    {
      "ID": "00000000-0000-0000-0000-000000000002",
      "attachments": [
        {
          "ID": "00000000-0000-0000-0000-000000000003",
          "filename": "test.txt",
          "mimeType": "text/plain",
          "url": "http://example.com"
        }
      ]
    }
  ]
}

### Verify state
GET http://localhost:4004/odata/v4/catalog/Orders(00000000-0000-0000-0000-000000000001)?$expand=items($expand=attachments)

# ----------------------------------------
# Failing PATCH scenarios
# ----------------------------------------

### (Scenario 1) PATCH OrderItem omitting certain attachments (should delete them)
PATCH http://localhost:4004/odata/v4/catalog/OrderItems(00000000-0000-0000-0000-000000000002)
Content-Type: application/json

{
    "attachments": []
}

### (Scenario 2) PATCH Order with existing OrderItem, but omitting attachments (should delete them)
PATCH http://localhost:4004/odata/v4/catalog/Orders(00000000-0000-0000-0000-000000000001)
Content-Type: application/json

{
  "items": [
    {
      "ID": "00000000-0000-0000-0000-000000000002",
      "attachments": []
    }
  ]
}

# ----------------------------------------
# Working PATCH scenarios
# ----------------------------------------

### (Scenario 3) PATCH OrderItem, keeping its attachments (nothing to delete)
PATCH http://localhost:4004/odata/v4/catalog/OrderItems(00000000-0000-0000-0000-000000000002)
Content-Type: application/json

{
  "attachments": [
    {
      "ID": "00000000-0000-0000-0000-000000000003"
    }
  ]
}

### (Scenario 4) PATCH Order with existing OrderItem, keeping its attachments (nothing to delete)
PATCH http://localhost:4004/odata/v4/catalog/Orders(00000000-0000-0000-0000-000000000001)
Content-Type: application/json

{
  "items": [
    {
      "ID": "00000000-0000-0000-0000-000000000002",
      "attachments": [
        {
          "ID": "00000000-0000-0000-0000-000000000003"
        }
      ]
    }
  ]
}

# ----------------------------------------
# Working DELETE scenarios
# ----------------------------------------

### (Scenario 5) DELETE OrderItem
DELETE http://localhost:4004/odata/v4/catalog/OrderItems(00000000-0000-0000-0000-000000000002)

### (Scenario 6) DELETE Order
DELETE http://localhost:4004/odata/v4/catalog/Orders(00000000-0000-0000-0000-000000000001)

Expected behavior
Expected req.attachmentsToDelete to be populated, so that these attachments can be properly deleted from S3.

[X] is it a regression issue?

Yes, it worked in 3.6.0. Regression was likely introduced in 3.8.0 which mentions that dependence on req.diff was removed.

Customer Info
Company: SAP.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions