SuperAPI AI Directory

fiori-draft

Data & analytics

View repository

Updated 10-16 02:37

About this skill

Fiori Draft — CAP Best Practices

Primary reference: https://cap.cloud.sap/docs/node.js/fiori
Fiori Draft overview: https://cap.cloud.sap/docs/guides/fiori#draft-support

Enabling draft

service OrderService {
  @odata.draft.enabled
  entity Orders as projection on db.Orders;
}

One annotation — CAP auto-generates the _drafts shadow table, all OData draft actions (draftEdit, draftActivate, draftDiscard), and wires up the Fiori Elements UI automatically.

Draft lifecycle events

module.exports = class OrderService extends cds.ApplicationService {
  async init() {
    const { Orders } = this.entities

    // NEW — user starts creating a new record
    this.before('NEW', Orders.drafts, req => {
      req.data.status = 'Draft'
    })

    // EDIT — user opens an existing active record for editing
    this.on('EDIT', Orders, async (req) => {
      const { ID } = req.params[0]
      const order = await SELECT.one(Orders).where({ ID })
      if (order?.status === 'Closed') req.reject(409, 'CANNOT_EDIT_CLOSED')
      return next()
    })

    // SAVE (draft activation) — user clicks Save
    this.before('CREATE', Orders, this.validateOnSave)  // fires on draft activate
    this.before('UPDATE', Orders, this.validateOnSave)

    // CANCEL — user discards changes, goes back to active state
    this.on('CANCEL', Orders.drafts, req => {
      // no-op usually, but you can add side effects
    })

    // DISCARD — user explicitly deletes the draft
    this.on('DISCARD', Orders.drafts, req => { /* cleanup if needed */ })

    return super.init()
  }

  async validateOnSave(req) {
    const { title, amount } = req.data
    if (!title) req.error({ code: 400, message: 'Title required', target: 'title' })
    if (amount <= 0) req.error({ code: 422, message: 'Amount must be positive', target: 'amount' })
    req.reject()  // throws all accumulated errors if any
  }
}

Draft Messages (GA since CAP August 2025)

Persistent Fiori draft messages provide fast inline feedback while the user is editing (on each PATCH, not just on Save):

// package.json — enabled by default since Aug 2025
{
  "cds": {
    "fiori": {
      "draft_messages": true
    }
  }
}

To run validations on every PATCH (not just on Save):

// Triggers on every field change in the Fiori UI
this.before('PATCH', Orders.drafts, req => {
  if (req.data.amount !== undefined && req.data.amount < 0) {
    req.error({ code: 422, message: 'Amount must be positive', target: 'amount' })
  }
  req.reject()
})

Note: Draft Messages require a database schema update. Run cds deploy after enabling. Not supported on OData V2 UIs or UI5 < 1.135.0.

Querying draft entities

// Read active entities only
const orders = await SELECT.from(Orders)

// Read draft entities
const drafts = await SELECT.from(Orders.drafts)

// Read both (Fiori "All" filter — CAP handles this automatically)
// Don't try to merge manually; let CAP's generic handlers do it

// Check if a draft exists for an active record
const hasDraft = await SELECT.one(Orders.drafts).where({ ID: id })

Programmatic draft operations (Node.js)

// Create a new draft
await srv.new(Orders.drafts, { title: 'New Order', amount: 0 })

// Start editing an active record (creates draft from active)
await srv.edit(Orders, { ID: id })

// Activate (save) a draft
await srv.save(Orders.drafts, { ID: id })

// Discard a draft
await srv.discard(Orders.drafts, { ID: id })

Calculated elements in drafts (GA since CAP Node.js 9.x)

entity Orders : cuid, managed {
  amount     : Decimal(9,2);
  taxRate    : Decimal(4,2);
  grossAmount = amount * (1 + taxRate) : Decimal(9,2);  // calculated element
}

Calculated elements are now properly evaluated when reading from the _drafts table — no need for virtual elements + custom code.

Lean Draft (simplified mode)

For simpler use cases without full draft lifecycle:

service OrderService {
  @odata.draft.enabled: { mode: 'exclusive' }
  entity Orders as projection on db.Orders;
}

Bypass Drafts by Default (CDS 10+)

Since CDS 10, direct access to active entities bypasses draft choreography by default. Non-Fiori clients (e.g. AI agents, REST clients) can now work with active data directly:

// CDS 10: illustrative OData V4 request shapes, not literal calls to run
// These access the ACTIVE entity directly (no draft processing)
GET    /odata/v4/Orders(:id)      // ✓ reads active directly
PATCH  /odata/v4/Orders(:id)      // ✓ updates active directly
DELETE /odata/v4/Orders(:id)      // ✓ deletes active directly

// These still go through draft choreography:
GET    /odata/v4/Orders(:id)/DraftAdministrativeData
PATCH  /odata/v4/Orders(:id)/DraftAdministrativeData/InProcessByUserDescription

Impact on handlers: Validation must now account for both paths:

// These are all valid CDS 10 entry points for a draft-enabled entity:
this.before('PATCH', Orders, ...)     // Called for both draft AND active PATCH
this.before('DELETE', Orders, ...)    // Called for both draft AND active DELETE
this.on('draft', Orders, ...)          // Still: draft-specific lifecycle

Opt-out (if you need pre-CDS 10 behavior):

// srv/cat-service.js
cds.fiori.bypass_draft = false

draft_new_action (optional): move draft creation to a named action so that plain POST creates active entities:

{
  "cds": { "fiori": { "draft_new_action": true } }
}

With this, POST /Books { ... } creates an active entity; use POST /Books/draftNew to create a draft.


Common mistakes to avoid

  • ❌ Registering EDIT handler on Orders.drafts — EDIT must be on the active entity Orders
  • ❌ Registering SAVE validation in before('SAVE', ...) — it doesn't exist; use before('CREATE', Orders, ...) and before('UPDATE', Orders, ...) (these fire on draft activation)
  • ❌ Manually merging active + draft results for "All" filter — let CAP do this
  • ❌ Forgetting req.reject() after accumulating req.error() calls — errors won't be thrown
  • ❌ Using LargeBinary fields with draft + hdb driver (causes deadlock) — switch to hana-client
  • ❌ Not running schema migration after enabling draft_messages
  • ❌ Using virtual elements for calculated values in drafts — use CDS calculated elements instead (CAP 9+)