Skip to content

10. Advanced topics

Nikica Josipovic edited this page Jul 8, 2025 · 3 revisions

Code completion and developer support

While we do plan to introduce out-of-the-box for code completion in CDS-Oyster, there are still ways to utilize what is already available with CAP today. Since the code sandbox has very strict limitations, you cannot require or import types like in normal CAP event handlers. You can utilize JSDoc instructions, though. Let's try an example: You need to install CDS Typer first, using

cds add typer

to the extension project. Please make sure that a new folder @cds-models appears in you project (if it doesn't, try making a dummy change to your CDS extension file). We can now modify the before-CREATE.js file to look like this

/**
 *
 * @typedef {import ('#cds-models/ProcessorService').Incident} Incident
 */

module.exports = async function createIncident (
  /** @type {import ('@sap/cds').Request<Incident>} */
  req
) {
  const { Customers } =
    /** @type {{Customers:import ('#cds-models/ProcessorService').Customers}}} */ (
      this.entities
    )

  const res = await SELECT.one
    .from(Customers)
    .columns('status')
    .where({ ID: req.data.customer_ID })

  if (res?.status === 'Gold') {
    req.data.urgency_code = 'H'
  }
}

What are we doing here?

The first block @typeded Incidents introduces the Incident Type to the editor via JSDoc instructions. The second comment @type {import ('sap/cds)} assigns this type definition to the req parameter of our function, while additionally making the editor aware of the cds.requestobject. If you now hover over req.data in the editor, you will get code completion for the Incidents entity - including the extensions you made to it. The third comment @type {Customers: import} will allow you to get autocompletion in the query to customers. If you go to the columns section, and enter a comma, you should get a list of preformatted strings of all attributes of the Customers type.

Note

This is just a rough tour of already existing CAP capabilities from the CDS Types and CDS Typer plugins, applied to the limited CDS-Oyster sandbox. We do plan to provide a much better, built-in support of these tools in the versions to follow

BADI style extensibility

In the SAP universe, BADIs are well known. They resemble predefined, and well defined extension points, which are used in an exit-fashion, meaning they are explicitly triggered by the application developer, and only executed when implemented. Let's try to create such an exit in our extension project. We are going to utilize an event for our exit. Please augment your ext.cds extension with the following definition

extend service ProcessorService with {
  event customerPromoted {Customer_ID: String}
};

Note

While you can emit any kind of event within CAP at any point in time, you can only register sandbioxed event handlers to events pre-defined in CDS. Creating a custom handler to an undefined event will result with a no-op

Create a file on-customerPromoted.js directly in the srv/ProcessorService folder. Events are not bound to an entity, but to a service, so their definition needs to be located top level. We are going to do the bare minimum here:

module.exports = async function (req) {
  console.log(`Customer with ID: ${req.data.customer_ID} has been promoted to Gold status.`);  
}

Our handler for on-promoteCustomer.js should look like this

module.exports = async function (req) {

    await this.update('CustomersProjection').with({ status: 'Gold' }).where({ ID: req.data.Customer_ID })

    await this.update('Incidents').with({ urgency_code: 'H' }).where({ 
        customer_ID: req.data.Customer_ID,
        and : { not: { status_code: 'C' } }
    })
  await this.emit('customerPromoted', {
    customer_ID: req.data.Customer_ID
  })
}

Voila, we have emulated a BADI style extension point. In the real world, the event would have been defined by the application developer, and emited somewhere in the application code, and only the handler to this exit point would have been provided by the extension developer. But this approach here demonstrates a couple of key concepts

  • You can register custom business logic on practically any event in CAP, as long as they are part of the CDS model - CRUD, custom events, bound and unbound actions
  • You can emit events in CDS-Oyster code, and also await their implementation
  • An event triggered in CDS-Oyster can trigger the execution of anther sandboxed event handler, effectively creaing a chain of sandboxed logic

Now, let's make this example even more BADI-like. We do not want to emit an event, but call an action.

Please first extend the model with the action definition:

extend service ProcessorService with {
  action validateCustomer(Customer_ID : String) returns String
};

And provide in implementation in srv/ProcessorService/on-validateCustomer.js

module.exports = async function (req) {
    return 'BADI called with Customer_ID: ' + req.data.Customer_ID;
}

We want to call this exit point at Incident Creation srv/ProcessorService/Incidents/before-CREATE.js like this:

module.exports = async function createIncident (req) {
  const { Customers } = this.entities

  const res = await SELECT.one
    .from(Customers)
    .columns('status')
    .where({ ID: req.data.customer_ID })

  if (res?.status === 'Gold') {
    req.data.urgency_code = 'H'
  }
  const badi = await this.validateCustomer(req.data.customer_ID)
  console.log(badi)
}

This is the typical use case for pre-defined exits. Standard application code calls an action defined in the model, but with no implementation. Customers provide the implementation, usually some validation logic, and return a result, which then gets evaluated somehow in the standard application logic. Please observe, how we were able to call the action using this.actionName in the sandboxed code. You can call all unbound action like this, while for bound actions you would need to use this.send({ event: 'actionName', data: { req data }, entity:'Bound Target', params: [{ entity keys }] })

Configuration and security

CAP has built-in support for block lists and allow lists. CDS-Oyster can integrate in this concept to allow fine-grained control, where business logic extension are possible or not. The format is like this:

"cds.xt.ExtensibilityService": {
        "namespace-blocklist": ["com.sap.", "sap."],
        "extension-allowlist": [
          {
            "for": ["ProcessorService"],
            "kind": "entity",
            "new-fields": 10,
            "code" : ["READ", "UPDATE", "DELETE", "function"]
          },
          {
            "for": ["ProcessorService"],
            "kind": "service",
            "new-entities": 5,
            "code" : ["action"]
          },
          {
            "for": ["AnotherService"],
            "kind": "service",
            "new-entities": 1,
            "code" : ["INSERT", "function"]
          }
        ]
      }

You can play around with these settings and see what happens when you push an extension. This is also a good time to play around with code validation. You could try deploying code using asynchronous calls, or require statements, or something that tries to work with prototypes or constructurs.

Proceed to the Summary, to get a small recap and outlook

Clone this wiki locally