All skills
secondsky avatar

/sap-cap-capire

@620a19a
by Eddiesecondsky/sap-skills456 stars
120

SAP Cloud Application Programming Model (CAP) development skill using Capire documentation. Use when: building CAP applications, defining CDS models, implementing services, working with SAP HANA/SQLite/PostgreSQL databases, deploying to SAP BTP Cloud Foundry or Kyma, implementing Fiori UIs, handling authorization, multitenancy, or messaging. Covers CDL/CQL/CSN syntax, Node.js and Java runtimes, event handlers, OData services, and CAP plugins.

Use this Skill: https://skilld.dev/gh/secondsky/sap-skills/sap-cap-capire

This session only. Nothing lands on disk.

referencesevent-handlers-nodejs.md

≈2.5k tokens on demand. Your agent reads this file only when SKILL.md points to it.

CAP Event Handlers for Node.js Complete Reference

Source: https://cap.cloud.sap/docs/node.js/events

Table of Contents

Handler Registration

Class-Based Style

const cds = require('@sap/cds');

module.exports = class CatalogService extends cds.ApplicationService {
  async init() {
    const { Books, Authors } = this.entities;

    // Register handlers
    this.before('CREATE', Books, this.validateBook);
    this.on('READ', Books, this.onReadBooks);
    this.after('READ', Books, this.enrichBooks);
    this.on('submitOrder', this.onSubmitOrder);

    return super.init();
  }

  validateBook(req) { /* ... */ }
  async onReadBooks(req) { /* ... */ }
  enrichBooks(books, req) { /* ... */ }
  async onSubmitOrder(req) { /* ... */ }
}

Functional Style

module.exports = function() {
  const { Books } = this.entities;

  this.before('CREATE', Books, validateBook);
  this.on('READ', Books, onReadBooks);
  this.after('READ', Books, enrichBooks);
  this.on('submitOrder', onSubmitOrder);

  function validateBook(req) { /* ... */ }
  async function onReadBooks(req) { /* ... */ }
  function enrichBooks(books, req) { /* ... */ }
  async function onSubmitOrder(req) { /* ... */ }
}

Handler Phases

before Handlers

Run before the main handler. Use for validation, enrichment, authorization.

// Validation
this.before('CREATE', 'Books', (req) => {
  const { title, price } = req.data;
  if (!title) req.error(400, 'Title is required');
  if (price < 0) req.error(400, 'Price must be positive');
});

// Enrichment
this.before('CREATE', 'Orders', (req) => {
  req.data.createdAt = new Date();
  req.data.status = 'draft';
});

// Authorization check
this.before('UPDATE', 'Books', async (req) => {
  const book = await SELECT.one.from('Books', req.data.ID);
  if (book.createdBy !== req.user.id && !req.user.is('admin')) {
    req.reject(403, 'Not authorized');
  }
});

on Handlers

Replace or intercept the default implementation.

// Custom READ implementation
this.on('READ', 'Books', async (req) => {
  const books = await cds.db.run(req.query);
  return books;
});

// Action handler
this.on('submitOrder', async (req) => {
  const { book, quantity } = req.data;
  // ... business logic
  return { success: true };
});

// Call next handler in chain
this.on('READ', 'Books', async (req, next) => {
  const result = await next();  // Call default/next handler
  return result.filter(b => b.stock > 0);
});

after Handlers

Run after the main handler. Use for post-processing results.

// Enrich results
this.after('READ', 'Books', (books, req) => {
  for (const book of books) {
    book.discount = book.stock > 100 ? '10%' : null;
    book.available = book.stock > 0;
  }
});

// Async after handler
this.after('CREATE', 'Orders', async (order, req) => {
  await this.emit('OrderCreated', { orderID: order.ID });
});

Event Names

CRUD Events

this.on('CREATE', 'Books', handler);
this.on('READ', 'Books', handler);
this.on('UPDATE', 'Books', handler);
this.on('DELETE', 'Books', handler);

Wildcard Events

// All CRUD events on Books
this.before('*', 'Books', handler);

// All entities
this.before('CREATE', '*', handler);

// All events on all entities
this.before('*', handler);

Custom Actions/Functions

// Unbound action
this.on('submitOrder', handler);

// Bound action (entity-level)
this.on('confirm', 'Orders', handler);

// Function
this.on('getTotal', 'Orders', handler);

Draft Events

this.on('NEW', 'Books', handler);      // Create draft
this.on('EDIT', 'Books', handler);     // Edit existing
this.on('PATCH', 'Books', handler);    // Update draft
this.on('SAVE', 'Books', handler);     // Activate draft
this.on('CANCEL', 'Books', handler);   // Discard draft

Request Object (req)

Properties

this.on('CREATE', 'Books', (req) => {
  req.event;      // 'CREATE', 'READ', 'UPDATE', 'DELETE', or action name
  req.target;     // Entity definition from CSN
  req.entity;     // Entity name string
  req.path;       // Full path '/Books(123)'
  req.query;      // CQN query object
  req.data;       // Request payload
  req.params;     // URL parameters [{ID: '123'}]
  req.headers;    // HTTP headers
  req.user;       // Authenticated user
  req.tenant;     // Tenant ID (multitenant apps)
  req.locale;     // User locale
  req.timestamp;  // Request timestamp
  req.id;         // Correlation ID
});

User Object

req.user.id;              // User ID
req.user.is('admin');     // Check role
req.user.attr.country;    // User attribute
req.user.roles;           // Assigned roles array

Response Methods

// Success responses
req.reply(data);                    // Return data
return data;                        // Same as reply

// Errors
req.reject(400, 'Bad request');     // Throw immediately
req.reject(404);                    // HTTP status only
req.error(400, 'Error 1');          // Collect error
req.error(400, 'Error 2');          // Multiple errors
// Errors collected, rejected at end of phase

// Warnings/Info (collected in response headers)
req.warn(200, 'Warning message');
req.info(200, 'Info message');
req.notify(200, 'Notification');

Query Manipulation

this.before('READ', 'Books', (req) => {
  // Add where clause
  req.query.where({ stock: { '>': 0 } });

  // Limit results
  req.query.limit(100);

  // Add columns
  req.query.columns('ID', 'title', 'author.name as authorName');
});

Error Handling

Throwing Errors

// Immediate rejection
if (!valid) req.reject(400, 'Invalid data');

// With error code
req.reject(400, 'INVALID_INPUT', 'Invalid input data');

// With target field
req.reject(400, { message: 'Invalid', target: 'title' });

// Multiple errors (collected)
if (!data.title) req.error(400, 'Title required', 'title');
if (!data.price) req.error(400, 'Price required', 'price');
// Framework rejects if req.errors is not empty

Error Codes

const { errors } = require('@sap/cds');

// Standard CAP errors
throw new errors.NotFound('Book not found');
throw new errors.Unauthorized('Login required');
throw new errors.Forbidden('Access denied');

Database Operations

Using cds.db

const { Books } = cds.entities;

// SELECT
const books = await SELECT.from(Books).where({ stock: { '>': 0 } });
const book = await SELECT.one.from(Books, bookId);

// INSERT
await INSERT.into(Books).entries({ title: 'New Book', stock: 10 });

// UPDATE
await UPDATE(Books, bookId).set({ stock: 50 });
await UPDATE(Books).set({ stock: { '+=': 10 } }).where({ genre: 'fiction' });

// DELETE
await DELETE.from(Books, bookId);
await DELETE.from(Books).where({ stock: 0 });

// UPSERT
await UPSERT.into(Books).entries({ ID: bookId, title: 'Updated' });

Using req.query

this.on('READ', 'Books', async (req) => {
  // Run the incoming query
  return await cds.db.run(req.query);
});

Transactions

this.on('transferStock', async (req) => {
  const { from, to, amount } = req.data;

  // Automatic transaction within handler
  await UPDATE(Books, from).set({ stock: { '-=': amount } });
  await UPDATE(Books, to).set({ stock: { '+=': amount } });

  // Or explicit transaction
  return cds.tx(async (tx) => {
    await tx.run(UPDATE(Books, from).set({ stock: { '-=': amount } }));
    await tx.run(UPDATE(Books, to).set({ stock: { '+=': amount } }));
    return { success: true };
  });
});

Connecting to Services

Database Service

const db = await cds.connect.to('db');
await db.run(SELECT.from('Books'));

Other CAP Services

// Connect to another service
const adminSrv = await cds.connect.to('AdminService');
const books = await adminSrv.read('Books');

// Send actions
await adminSrv.send('updateStock', { book: bookId, delta: 10 });

Remote Services

// Configured in package.json cds.requires
const externalApi = await cds.connect.to('ExternalAPI');
const result = await externalApi.run(SELECT.from('Products'));

Event Emission

Emit Events

// Emit to own service
this.emit('OrderCreated', { orderID: order.ID });

// Emit to messaging service
const messaging = await cds.connect.to('messaging');
await messaging.emit('OrderCreated', { orderID: order.ID });

Subscribe to Events

module.exports = async function() {
  const messaging = await cds.connect.to('messaging');

  messaging.on('OrderCreated', async (msg) => {
    const { orderID } = msg.data;
    console.log('Order created:', orderID);
  });
}

Lifecycle Events

// Before commit
req.before('commit', async () => {
  // Run before transaction commits
});

// After success
req.on('succeeded', async () => {
  // Run after successful commit
  // Note: Outside transaction!
});

// After failure
req.on('failed', async () => {
  // Run after rollback
});

// Always (success or failure)
req.on('done', async () => {
  // Cleanup logic
});

Context Access

cds.context

// Available anywhere in async call chain
const { user, tenant, locale } = cds.context;

Setting Context

// For background jobs
cds.context = { user: new cds.User('system'), tenant: 't1' };
await doSomething();

TypeScript

import cds from '@sap/cds';
import { Request } from '@sap/cds';

export default class CatalogService extends cds.ApplicationService {
  async init() {
    this.on('READ', 'Books', this.readBooks);
    return super.init();
  }

  private async readBooks(req: Request) {
    return await cds.db.run(req.query);
  }
}

Source: SKILL.md on GitHub

1 alert16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The analyzed skill is a reference and template package providing guidance for SAP Cloud Application Programming Model (CAP) development, configuration, data privacy, and testing. It contains no executable scripts, hardcoded credentials, or malicious behavior.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer6mo

    12/34 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 620a19a. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 2 months ago
Other metadata
metadata
{
  "maintainer": "Eduard Jiglau",
  "maintainer_email": "hello@sap-ai-skills.com",
  "website": "https://sap-ai-skills.com",
  "version": "2.4.1",
  "last_verified": "2026-02-22",
  "cap_version": "@sap/cds 9.7.x",
  "mcp_version": "@cap-js/mcp-server 0.0.5",
  "lsp_version": "@sap/cds-lsp 9.7.x"
}

README badge

README badge for secondsky/sap-skills/sap-cap-capire