Building a CAP Plugin with Custom Aspects

 

Introduction

In CAP, aspects are the mechanism for Aspect-Oriented Modelling. Where annotations add metadata, aspects allow you to add reusable structures and behaviours to your entities without modifying the original definition. The original article may be read here.

Aspects are sets of fields, associations, or annotations that you can mix in to one or more entities.

We use cds-plugin to include the plugin in the project in a simple way.

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 .

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

The 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 from the aspect automatically update after each PATCH operation

Comments

Popular Posts