Implement customer-exit hooks, extend entities at runtime, use cds-plugin.js, and @Core.SideEffects.
extend entity@sap.fiori.draft.enabled, @Core.SideEffects)SAP-style platforms must be extensible — customers need to add fields, override business rules, and integrate without forking the base. These patterns are how enterprise CAP applications support that model cleanly.
// Base service — exposes hook points
module.exports = class BaseOrderService extends cds.ApplicationService {
async init () {
// Register hook slots other plugins can fill
this.before('CREATE', 'Orders', this._beforeCreate.bind(this))
this.on('CREATE', 'Orders', this._onCreate.bind(this))
await super.init()
}
// Override these in subclass or plugin
async _beforeCreate (req) { /* default: no-op */ }
async _onCreate (req) {
// ... default create logic
await INSERT.into('Orders').entries(req.data)
return SELECT.one.from('Orders').where({ ID: req.data.ID })
}
}
// Extended service (customer or partner)
const BaseOrderService = require('@company/base-orders')
module.exports = class CustomOrderService extends BaseOrderService {
async _beforeCreate (req) {
// Inject custom validation without touching base code
if (!req.data.costCenter) return req.reject(400, 'Cost center required')
}
}
// Base service emits an extensibility event
this.before('CREATE', 'Orders', async (req) => {
// Allow external plugins to veto or enrich
await this.emit('BEFORE_CREATE_ORDERS', req.data)
})
// Customer plugin subscribes
const baseService = await cds.connect.to('OrderService')
baseService.before('BEFORE_CREATE_ORDERS', (req) => {
req.data.customField = computeCustomValue(req.data)
})
// Base model (not to be modified)
// db/base/schema.cds
namespace base;
entity Orders {
key ID : UUID;
status : String;
total : Decimal;
}
// Extension (in customer namespace)
// db/extensions/customer.cds
using base from '../../base/schema';
extend base.Orders with {
costCenter : String(10);
projectCode : String(20);
approvedBy : String;
}
// Also extend the service projection
using OrderService as svc from '../../srv/order-service';
extend projection svc.Orders with {
costCenter,
projectCode,
approvedBy
}
@Core.SideEffects)When changing one field triggers recalculation of another in Fiori:
annotate svc.OrderItems with @(
Core.SideEffects #quantityChanges: {
SourceProperties: [quantity, price],
TargetProperties: ['_it/total', '_it/taxAmount']
}
);
This tells Fiori: when quantity or price changes, re-fetch total and taxAmount from the server.
my-cap-plugin/
├── package.json
├── index.js ← plugin entry point
└── cds-plugin.js ← auto-loaded by CAP on install
// package.json
{
"name": "@mycompany/cap-audit",
"version": "1.0.0",
"cds": {
"plugin": true
}
}
// cds-plugin.js — runs at CDS bootstrap
const cds = require('@sap/cds')
// Hook into every service globally
cds.on('serving', (service) => {
service.after(['CREATE','UPDATE','DELETE'], '*', (data, req) => {
// Global audit log — applied to every entity in every service
logAudit({ entity: req.target.name, op: req.event, user: req.user.id })
})
})
Install in any CAP project with npm install @mycompany/cap-audit — the plugin registers automatically.
const featureFlags = await cds.connect.to('feature-flags')
this.before('CREATE', 'Orders', async (req) => {
const enabled = await featureFlags.evaluate('newValidationRule', req.tenant)
if (enabled) {
// Run new validation only for tenants with the flag enabled
await runNewValidation(req.data)
}
})
| Item | Pattern |
|---|---|
| Add new fields | extend entity in customer namespace |
| Override business rule | Override _beforeCreate in subclass |
| Add cross-cutting logic | cds-plugin.js with cds.on('serving', ...) |
| Customer-specific workflow | Subscribe to domain events |
| Feature rollout | Feature flag evaluated per tenant |
| UI extensions | Separate annotation cds file via extend projection |
_validate(req) method_validate with stricter rulescds-plugin.js that logs every CREATE across all servicespriority on Orders via extend entityextend entity adds fields in a customer namespace (non-destructive)cds-plugin.js with cds.on('serving', ...) applies cross-service logic globally@Core.SideEffects tells Fiori which fields to re-fetch after a change