Sitemap
Ockert van Schalkwyk

Ockert van Schalkwyk

Web developer from South-Africa

Building a CAP Plug-in with Custom Aspects

4 min readMar 10, 2026

--

Press enter or click to view image in full size

In CAP, aspects are the mechanism for Aspect-Oriented Modelling. Where annotations add metadata, aspects allows you to add reusable structures and behaviours to your entities without modifying the original definition.

Create a CAP Project

cds init aspect-demo
cds add nodejs
cd aspect-demo
npm install

Edit db/schema.cds: and add a simple aspect and entity that uses it (later we will remove this and create a plugin with an aspect):

namespace demo;
using { cuid } from '@sap/cds/common';

aspect MyAspect {
b: String;
c: String;
}

entity AspectTest : MyAspect {
key ID : UUID;
a: String;
}

Here you can see the custom aspect MyAspect used in @AspectTest. This adds two extra fields to the entity.

Next, configure the service atsrv/user-service.cds:

using { demo } from '../db/schema';
service UserService @(path: '/users') {
entity Users as projection on demo.Users;
}

Edit package.json to use Sqlite as follows:

{
...
"cds": {
"requires": {
"db": {
"kind": "sqlite",
"credentials": {
"database": "db.sqlite"
}
}
}
}
}

Also add some test data by creating db/data/demo-AspectTest.csv:

ID,a,b,c
aaaaaaaa-0000-0000-0000-000000000001,a,b,c
aaaaaaaa-0000-0000-0000-000000000002,d,e,f

You can now deploy the database using cds deployand run the project using npm run start

You can read the entity using the following:

curl -s http://localhost:4004/users/AspectTest

Note the output contains the two extra fields from the aspect:

{
"@odata.context": "$metadata#AspectTest",
"value": [
{
"b": "b",
"c": "c",
"ID": "aaaaaaaa-0000-0000-0000-000000000001",
"a": "a"
},
{
"b": "e",
"c": "f",
"ID": "aaaaaaaa-0000-0000-0000-000000000002",
"a": "d"
}
]
}

Write the Plugin

Let’s implement a plugin for an aspect. Create a directory in the project root named history-plugin .

Get Ockert van Schalkwyk’s stories in your inbox

Join Medium for free to get updates from this writer.

Next, create history-plugin/package.jsonas follows:

{
"name": "history-plugin",
"version": "1.0.0",
"cds": {
"plugin": true,
"model": "index.cds"
},
"peerDependencies": {
"@sap/cds": ">=7"
}
}

Create history-plugin/index.cds as follows:

namespace my.custom.plugin;

aspect History {
customField : String;
createdAt : Timestamp @cds.on.insert: $now;
changedAt : Timestamp;
}

Create history-plugin/cds-plugin.js as follows:

const cds = require('@sap/cds')
cds.on('served', (services) => {
for (const srv of services) {
if (!(srv instanceof cds.ApplicationService)) continue;
for (const entity of srv.entities) {
if (entity.elements.changedAt) {
console.log(`[History-Plugin]: Registered update handler for ${entity.name}`)
srv.before(['UPDATE', 'PUT'], entity, async (req) => {
console.log(`[History-Plugin]: Updating 'changedAt' for ${entity.name}`)
req.data.changedAt = new Date().toISOString()
if (entity.elements.changedBy) {
req.data.changedBy = req.user.id
}
})
}
}
}
})

Here when we update the entity being annotated with the aspect, the changedAt attribute that was added by the aspect will have its value set to the current date.

Using the Plugin

Edit the main project package.json ensuring the plugin is specified:

{
...
"dependencies": {
"history-plugin": "file:history-plugin"
...
}

Edit the schema db/schema.cds to create an entity that uses the aspect we defined:

namespace demo;
using { cuid } from '@sap/cds/common';
using { my.custom.plugin.History } from '../history-plugin';
entity Product : History {
key ID : Integer;
name : String;
}

Create a service to expose this entity:

using { demo } from '../db/schema';

service StoreService @(path: '/store') {
entity Product as projection on demo.Product;
}

Finally, add some test data by creating db/data/demo-Product.csv as follows:

ID,name,customField,createdAt
1,Windmill Blade,Initial Stock,2026-03-09T16:14:00Z
2,Galvanized Pipe,Bulk Order,2026-03-09T16:15:00Z
3,Deep Well Cylinder,Replacement Part,2026-03-09T16:20:00Z

At this point you can deploy using cds deploy and run the server using npm run start .

Verify that the plugin was registered by looking out for the following log line:

[History-Plugin]: Registered update handler for StoreService.Product

To verify the functionality, use the following script:

#!/bin/bash
JSON_DATA=$(curl -s http://localhost:4004/store/Product)
echo "$JSON_DATA" | jq -c '.value[]' | while read line; do
id=$(echo "$line" | jq -r '.ID')
name=$(echo "$line" | jq -r '.name')
echo "Updating ID: $id (Current Name: $name)..."
curl -s -X PATCH "http://localhost:4004/store/Product($id)" \
-H "Content-Type: application/json" \
-d "{\"name\": \"$name (Updated)\"}" \
| jq .
echo "-----------------------------------"
done

This runs an OData PATCH on each of the items in the database. You should see the following in the server logs:

[History-Plugin]: Updating 'changedAt' for StoreService.Product

Furthermore, you should see the changedAt field that was added from the History aspect automatically update to the current date after each PATCH operation.

Press enter or click to view image in full size

--

--