25 · OData Actions & Functions

Define and implement bound/unbound OData actions (POST) and functions (GET). Pass parameters and return complex types.

Episode 25 — Actions & Functions

What you'll learn

Why this matters

CRUD covers entity lifecycle; actions and functions cover business operations — approve an order, calculate a quote, trigger a workflow. They're the OData equivalent of RPC calls and are first-class citizens in CAP.

CDS definitions

Unbound action and function

// srv/orders-service.cds
service OrdersService {
  entity Orders as projection on my.Orders;

  // Action: modifies state, uses POST
  action  submitOrder (orderId : UUID)                   returns Order;

  // Function: read-only, uses GET
  function getOrderTotal (orderId : UUID)                returns Decimal;
  function topProducts   (limit   : Integer default 10) returns array of Product;
}

Bound action and function

service OrdersService {
  entity Orders as projection on my.Orders {
    // bound to Orders entity
    action  cancel ()                  returns Orders;
    action  clone  (newTitle : String) returns Orders;
    function daysUntilDelivery ()      returns Integer;
  }
}

Implementing actions & functions

Unbound action

// srv/orders-service.js
this.on('submitOrder', async (req) => {
  const { orderId } = req.data           // named parameters from CDS

  const order = await SELECT.one.from('Orders').where({ ID: orderId })
  if (!order) return req.reject(404, `Order ${orderId} not found`)
  if (order.status !== 'DRAFT') return req.reject(409, 'Order already submitted')

  await UPDATE('Orders').set({ status: 'SUBMITTED', submittedAt: new Date() })
                        .where({ ID: orderId })

  return SELECT.one.from('Orders').where({ ID: orderId })  // return updated order
})

Bound action

// Bound actions receive the bound entity key in req.params
this.on('cancel', 'Orders', async (req) => {
  const [{ ID }] = req.params    // key of the bound entity

  await UPDATE('Orders').set({ status: 'CANCELLED' }).where({ ID })
  return SELECT.one.from('Orders').where({ ID })
})

Function (read-only)

this.on('getOrderTotal', async (req) => {
  const { orderId } = req.data
  const items = await SELECT.from('OrderItems').where({ order_ID: orderId })
  return items.reduce((sum, i) => sum + i.price * i.quantity, 0)
})

HTTP calls

### Unbound action (POST, JSON body with named params)
POST http://localhost:4004/orders/submitOrder
Content-Type: application/json

{ "orderId": "c0f0b8a0-..." }

### Bound action (POST to entity instance)
POST http://localhost:4004/orders/Orders(c0f0b8a0-...)/cancel

### Unbound function (GET with query parameters)
GET http://localhost:4004/orders/getOrderTotal(orderId='c0f0b8a0-...')

### Function returning collection
GET http://localhost:4004/orders/topProducts(limit=5)

Returning complex types

type OrderSummary {
  ID         : UUID;
  total      : Decimal;
  itemCount  : Integer;
  canCancel  : Boolean;
}

service OrdersService {
  function getOrderSummary (orderId : UUID) returns OrderSummary;
}
this.on('getOrderSummary', async (req) => {
  const { orderId } = req.data
  const order = await SELECT.one.from('Orders').where({ ID: orderId })
  const items = await SELECT.from('OrderItems').where({ order_ID: orderId })

  return {
    ID:        order.ID,
    total:     items.reduce((s, i) => s + i.price * i.qty, 0),
    itemCount: items.length,
    canCancel: order.status === 'SUBMITTED'
  }
})

Fiori UI5 call

// In a Fiori controller
const oModel = this.getView().getModel()
oModel.callFunction('/submitOrder', {
  method: 'POST',
  urlParameters: { orderId: sOrderId },
  success: (data) => MessageToast.show('Order submitted!'),
  error:   (err)  => MessageBox.error(err.message)
})

Common mistakes

Mistake Symptom Fix
Using GET for an action 405 Method Not Allowed Actions always POST
Returning plain value from function that declares complex type Serialization error Return an object matching the CDS type
Forgetting req.params for bound actions ID is undefined Use const [{ ID }] = req.params
No returns clause in CDS CAP infers void return Add returns <type> for typed responses

Hands-on exercise

  1. Add action addToCart (bookId: UUID, quantity: Integer) returns Cart; to your service
  2. Implement it: find the cart for req.user.id, upsert the item, return the updated cart
  3. Add function cartTotal () returns Decimal; and implement it
  4. Test both with REST client: POST for action, GET for function

Checkpoint ✓

Sign in for progress tracking & Q&A →