Skip to content
On this page

Fiori Drafts

This section describes which events occur in combination with SAP Fiori Drafts.

Overview

See Advanced > Draft-based Editing for an overview on SAP Fiori Draft support in CAP.

Editing Drafts

When users edit a draft-enabled entity in the frontend, the following requests are sent to the CAP Java backend. As an effect, draft-specific events are triggered, as described in the following table. The draft-specific events are defined by the DraftService interface.

TIP

Draft-enabled entities have an extra key IsActiveEntity by which you can access either the active entity or the draft (inactive entity).

Request sent by Fiori frontendEvent nameDefault implementation
POSTDraftService.EVENT_DRAFT_NEWCreates a new empty draft. Internally triggers DraftService.EVENT_DRAFT_CREATE.
PATCH with key IsActiveEntity=falseDraftService.EVENT_DRAFT_PATCHUpdates an existing draft
DELETE with key IsActiveEntity=falseDraftService.EVENT_DRAFT_CANCELDeletes an existing draft
DELETE with key IsActiveEntity=trueCqnService.EVENT_DELETEDeletes an active entity and the corresponding draft
POST with action draftPrepareDraftService.EVENT_DRAFT_PREPAREEmpty implementation
POST with action draftEditDraftService.EVENT_DRAFT_EDITCreates a new draft from an active entity. Internally triggers DraftService.EVENT_DRAFT_CREATE.
POST with action draftActivateDraftService.EVENT_DRAFT_SAVEActivates a draft and updates the active entity. Triggers an EVENT_CREATE or EVENT_UPDATE event on the affected entity.

You can use these events to add custom logic to the SAP Fiori draft flow, for example to interact with drafts or to validate user data.

The following example registers a @Before handler to fill in default-values into a draft before the user starts editing:

java
@Before(event = DraftService.EVENT_DRAFT_NEW)
public void prefillOrderItems(DraftNewEventContext context, OrderItems orderItem) {
    // Pre-fill fields with default values
}
@Before(event = DraftService.EVENT_DRAFT_NEW)
public void prefillOrderItems(DraftNewEventContext context, OrderItems orderItem) {
    // Pre-fill fields with default values
}

The DraftService.EVENT_DRAFT_CREATE is an internal event that is not triggered by Fiori directly. It can be used to set default or calculated values on new draft instances, regardless if they were created from scratch (DraftService.EVENT_DRAFT_NEW flow) or based on an existing active entity (DraftService.EVENT_DRAFT_EDIT flow).

For more examples, see the Bookshop sample application.

Activating Drafts

When you finish editing drafts by pressing the Save button, a draft gets activated. That means, either a single CREATE or UPDATE event is triggered to create or update the active entity with all of its compositions through a deeply structured document. You can register to these events to validate the activated data.

The following example shows how to validate user input right before an active entity gets created:

java
@Before(event = CqnService.EVENT_CREATE)
public void validateOrderItem(CdsCreateEventContext context, OrderItems orderItem) {
    // Add validation logic
}
@Before(event = CqnService.EVENT_CREATE)
public void validateOrderItem(CdsCreateEventContext context, OrderItems orderItem) {
    // Add validation logic
}

After a draft is activated, it’s deleted from the database.

Working with Draft-Enabled Entities

When deleting active entities that have a draft, the draft is deleted as well. In this case, a DELETE and DRAFT_CANCEL event are triggered.

To read an active entity, send a GET request with key IsActiveEntity=true, for example:

http
GET /v4/myservice/myentity(IsActiveEntity=true,ID=<key>);
GET /v4/myservice/myentity(IsActiveEntity=true,ID=<key>);

Likewise, to read the corresponding draft, call:

http
GET /v4/myservice/myentity(IsActiveEntity=false,ID=<key>);
GET /v4/myservice/myentity(IsActiveEntity=false,ID=<key>);

To get all active entities, you could use a filter as illustrated by the following example:

http
GET /v4/myservice/myentity?$filter=IsActiveEntity eq true
GET /v4/myservice/myentity?$filter=IsActiveEntity eq true

Bypassing the SAP Fiori Draft Flow

It's possible to create and update data directly without creating intermediate drafts. For example, this is useful when prefilling draft-enabled entities with data or in general, when technical components deal with the API exposed by draft-enabled entities. To achieve this, use the following requests. You can register event handlers for the corresponding events to validate incoming data:

HTTP / OData requestEvent nameDefault implementation
POST with IsActiveEntity: true in payloadCqnService.EVENT_CREATECreates the active entity
PUT with key IsActiveEntity=true in URICqnService.EVENT_CREATE
CqnService.EVENT_UPDATE
Creates or updates the active entity (full update)
PATCH with key IsActiveEntity=true in URICqnService.EVENT_UPDATECreates or updates the active entity (sparse update)

These events have the same semantics as described in section Handling CRUD events.

Draft Lock

An entity with a draft is locked from being edited by other users until either the draft is saved or a timeout is hit (15 minutes by default). You can configure this timeout by the following application configuration property:

yaml
cds.drafts.cancellationTimeout=1h
cds.drafts.cancellationTimeout=1h

You can turn off this feature completely by means of the application configuration property:

yaml
cds.security.draftProtection.enabled=false
cds.security.draftProtection.enabled=false

Draft Garbage Collection

Inactive drafts are automatically deleted after a timeout (30 days default). You can configure the timeout with the following application configuration property:

yaml
cds.drafts.deletionTimeout: 8w
cds.drafts.deletionTimeout: 8w

In this example, the draft timeout is set to 8 weeks.

This feature can be also turned off completely by setting the application configuration:

yaml
cds.drafts.gc.enabled: false
cds.drafts.gc.enabled: false

TIP

To get notified when a particular draft-enabled entity is garbage collected, you can register an event handler on the DraftService.EVENT_DRAFT_CANCEL event.

Overriding SAP Fiori's Draft Creation Behaviour

By default SAP Fiori triggers a POST request with an empty body to the entity collection to create a new draft. This behavior can be overridden by implementing a custom action, which SAP Fiori will trigger instead.

  1. Define an action bound to the draft-enabled entity with an explicitly binding parameter typed with many $self.

    This way, the action used to create a new draft is bound to the draft-enabled entity collection.

  2. Annotate the draft-enabled entity with @Common.DraftRoot.NewAction: '<action name>'.

    This indicates to SAP Fiori that this action should be used when creating a new draft.

  3. Implement the action in Java.

    The implementation of the action must trigger the newDraft(CqnInsert) method of the DraftService interface to create the draft. In addition, it must return the created draft entity.

The following code summarizes all of these steps in an example:

cds
service AdminService {
  @odata.draft.enabled
  @Common.DraftRoot.NewAction: 'AdminService.createDraft'
  entity Orders as projection on my.Orders actions {
    action createDraft(in: many $self, orderNo: String) returns Orders;
  };
}
service AdminService {
  @odata.draft.enabled
  @Common.DraftRoot.NewAction: 'AdminService.createDraft'
  entity Orders as projection on my.Orders actions {
    action createDraft(in: many $self, orderNo: String) returns Orders;
  };
}
java
@On(entity = Orders_.CDS_NAME)
public void createDraft(CreateDraftContext context) {
    Orders order = Orders.create();
    order.setOrderNo(context.getOrderNo());
    context.setResult(adminService.newDraft(Insert.into(Orders_.class).entry(order)).single(Orders.class));
}
@On(entity = Orders_.CDS_NAME)
public void createDraft(CreateDraftContext context) {
    Orders order = Orders.create();
    order.setOrderNo(context.getOrderNo());
    context.setResult(adminService.newDraft(Insert.into(Orders_.class).entry(order)).single(Orders.class));
}

Consuming Draft Services

If an Application Service is created based on a service definition, that contains a draft-enabled entity, it also implements the DraftService interface. This interface provides an API layer around the draft-specific events, and allows to create new draft entities, patch, cancel or save them, and put active entities back into edit mode.

The Draft-Service-specific APIs only operate on entities in draft-mode. The CQN Query APIs (run methods) provided by any Application Service, operate on active entities only. However, there’s one exception from this behavior, which is the READ event: When reading from a Draft Service, active and inactive entities are both queried and the results are combined.

WARNING

Persistence Services aren't draft-aware. Use the respective Draft Service or Application Service, when running draft-aware queries.

The following example, shows the usage of the Draft-Service-specific APIs:

java
import static bookshop.Bookshop_.ORDERS;

DraftService adminService = …;
// create draft
Orders order = adminService.newDraft(Insert.into(ORDERS)).single(Orders.class);
// set values
order.setOrderNo("DE-123456");
// patch draft
adminService.patchDraft(Update.entity(ORDERS).data(order)
    .where(o -> o.ID().eq(order.getId()).and(o.IsActiveEntity().eq(false))));
// save draft
CqnSelect orderDraft = Select.from(ORDERS)
    .where(o -> o.ID().eq(order.getId()).and(o.IsActiveEntity().eq(false)));
adminService.saveDraft(orderDraft);
// read draft
Orders draftOrder = adminService.run(orderDraft).single().as(Order.class);
// put draft back to edit mode
CqnSelect orderActive = Select.from(ORDERS)
    .where(o -> o.ID().eq(order.getId()).and(o.IsActiveEntity().eq(true)));
adminService.editDraft(orderActive, true);
// read entities in draft mode and activated entities
adminService.run(Select.from(ORDERS).where(o -> o.ID().eq(order.getId())));
import static bookshop.Bookshop_.ORDERS;

DraftService adminService = …;
// create draft
Orders order = adminService.newDraft(Insert.into(ORDERS)).single(Orders.class);
// set values
order.setOrderNo("DE-123456");
// patch draft
adminService.patchDraft(Update.entity(ORDERS).data(order)
    .where(o -> o.ID().eq(order.getId()).and(o.IsActiveEntity().eq(false))));
// save draft
CqnSelect orderDraft = Select.from(ORDERS)
    .where(o -> o.ID().eq(order.getId()).and(o.IsActiveEntity().eq(false)));
adminService.saveDraft(orderDraft);
// read draft
Orders draftOrder = adminService.run(orderDraft).single().as(Order.class);
// put draft back to edit mode
CqnSelect orderActive = Select.from(ORDERS)
    .where(o -> o.ID().eq(order.getId()).and(o.IsActiveEntity().eq(true)));
adminService.editDraft(orderActive, true);
// read entities in draft mode and activated entities
adminService.run(Select.from(ORDERS).where(o -> o.ID().eq(order.getId())));