
Sap Cap Capire
- 666 installs
- 399 repo stars
- Updated August 4, 2026
- secondsky/sap-skills
sap-cap-capire is a Claude Code skill that guides SAP Cloud Application Programming Model development with CDS models, OData services, and CAPire patterns for developers who extend SAP BTP cloud applications.
About
sap-cap-capire is a Claude Code skill from secondsky/sap-skills for building production-ready SAP BTP cloud applications with the Cloud Application Programming Model. It covers CAP CDS domain models, OData service exposure, and Node.js or Java service handlers aligned with CAPire reference patterns. Developers reach for sap-cap-capire when extending S/4HANA or SAP BTP solutions that need typed entities, event handlers, and enterprise-grade API contracts instead of ad hoc REST endpoints. The skill targets teams shipping OData-backed microservices and side-by-side extensions on SAP's managed cloud stack.
- CDS domain modeling
- OData service exposure
- Node.js and Java CAP runtimes
- BTP Cloud Foundry deployment
- CAPire reference patterns
Sap Cap Capire by the numbers
- 666 all-time installs (skills.sh)
- +52 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #554 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/secondsky/sap-skills --skill sap-cap-capireAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 666 |
|---|---|
| repo stars | ★ 399 |
| Last updated | August 4, 2026 |
| Repository | secondsky/sap-skills ↗ |
How do you build SAP CAP OData services on BTP?
Build SAP BTP cloud apps with CAP CDS models, OData services, Node/Java handlers, and CAPire patterns for production-ready enterprise extensions.
Who is it for?
Enterprise developers extending SAP S/4HANA or SAP BTP with CAP-based OData services who need CDS modeling and CAPire-aligned handler patterns.
Skip if: Greenfield non-SAP web apps, frontend-only React work, or teams without SAP BTP tenant access and CAP toolchain familiarity.
When should I use this skill?
A developer is scaffolding or extending a SAP CAP project with CDS models, OData endpoints, or CAPire service handlers on SAP BTP.
What you get
CAP CDS models, OData service definitions, and production-oriented Node.js or Java handler implementations for SAP BTP extensions.
- CDS models
- OData service definitions
- CAP handler code
Files
SAP CAP-Capire Development Skill
Related Skills
- sap-fiori-tools: Use for UI layer development, Fiori Elements integration, and frontend application generation
- sapui5: Use for custom UI development, advanced UI patterns, and freestyle application building
- sap-btp-cloud-platform: Use for deployment options, Cloud Foundry/Kyma configuration, and BTP service integration
- sap-hana-cli: Use for database management, schema inspection, and HDI container administration
- sap-abap: Use for ABAP system integration, external service consumption, and SAP extensions
- sap-btp-best-practices: Use for production deployment patterns and architectural guidance
- sap-ai-core: Use when adding AI capabilities to CAP applications or integrating with SAP AI services
- sap-cloud-sdk-ai: Use for SDK-level AI integration (chat completion, streaming, tool calling) in CAP event handlers
- sap-cloud-sdk-ai-python: Use for Python-based AI integration with CAP Java or standalone BTP services
- sap-api-style: Use when documenting CAP OData services or following API documentation standards
- sap-dependency-security: Use for secure dependency, lockfile, supply-chain, and exact MCP server pin controls in CAP service repos
When to Use This Skill
Use this skill when creating CAP projects, modeling CDS entities/services, implementing Node.js or Java event handlers, configuring HANA/SQLite/PostgreSQL persistence, deploying to BTP Cloud Foundry or Kyma, adding Fiori UIs, configuring authorization/multitenancy/messaging, or using CAP MCP/LSP tooling.
Common Issues
| Issue | First check |
|---|---|
cds watch or cds serve fails | Verify @sap/cds-dk, Node.js version, and project package.json scripts. |
| Entity/service not found | Use CAP MCP search_model when available, then inspect db/ and srv/ CDS files. |
| HANA deployment fails | Check HDI service binding, mta.yaml, and the HANA deployment references. |
| Authorization behaves unexpectedly | Review @requires, @restrict, XSUAA/IAS bindings, and user role mappings. |
Table of Contents
Quick Start
Project Initialization
# Use an approved CAP toolchain:
# - project-local devDependencies
# - user-local npm prefix
# - enterprise-managed Node/CAP installation
# Ensure cds and optional cds-lsp commands are on PATH.
cds --version
# Create new project
cds init <project-name>
cds init <project-name> --add sample,hana
# Start development server with live reload
cds watch
# Add capabilities
cds add hana # SAP HANA database
cds add sqlite # SQLite for development
cds add xsuaa # Authentication
cds add mta # Cloud Foundry deployment
cds add multitenancy # SaaS multitenancy
cds add typescript # TypeScript supportBasic Entity Example
using { cuid, managed } from '@sap/cds/common';
namespace my.bookshop;
entity Books : cuid, managed {
title : String(111) not null;
author : Association to Authors;
stock : Integer;
price : Decimal(9,2);
}
entity Authors : cuid, managed {
name : String(111);
books : Association to many Books on books.author = $self;
}Basic Service
using { my.bookshop as my } from '../db/schema';
service CatalogService @(path: '/browse') {
@readonly entity Books as projection on my.Books;
@readonly entity Authors as projection on my.Authors;
@requires: 'authenticated-user'
action submitOrder(book: Books:ID, quantity: Integer) returns String;
}MCP Integration
This skill integrates with the official CAP MCP (Model Context Protocol) server, providing AI agents with live access to your project's compiled CDS model and CAP documentation.
Available MCP Tools:
search_model- Fuzzy search for CDS entities, services, actions, and relationships in your compiled CSN modelsearch_docs- Semantic search through CAP documentation for syntax, patterns, and best practices
Key Benefits:
- Instant Model Discovery: Query your project's entities, associations, and services without reading files
- Context-Aware Documentation: Find relevant CAP documentation based on semantic similarity, not keywords
- Zero Configuration: No credentials or environment variables required
- Offline-Capable: All searches are local (model) or cached (docs)
Setup: See MCP Integration Guide for configuration with Claude Code, opencode, or GitHub Copilot. MCP package pins are governed by sap-dependency-security and validated by npm run validate:mcp-security.
Use Cases: See MCP Use Cases for illustrative local workflow examples and planning assumptions, not repository-verified ROI.
Agent Integration: The specialized agents (cap-cds-modeler, cap-service-developer, cap-project-architect, cap-performance-debugger) automatically use these MCP tools as part of their workflows.
CAP MCP and LSP Routing
Use MCP first for local model and docs questions, then fall back to direct file search when MCP is unavailable. Use rg -n "<entity|service|aspect|annotation|handler|cds compile|deployment>" references/*.md srv db app to locate the narrowest reference before loading long CAP guides.
- Use
references/mcp-integration.mdfor MCP configuration and package pin checks. - Use
references/mcp-use-cases.mdonly for workflow selection and illustrative impact examples. - Use
.lsp.jsonas a Claude-compatible sidecar for CAP editor integration; other harnesses should not assume it is auto-loaded. - For Codex, OpenCode, editors, or other LSP-capable clients, configure the command manually as
node <sap-cap-capire-plugin-root>/lsp/cds-lsp-launcher.mjs --stdio. This still requires@sap/cds-lspto be installed through an approved project-local devDependency, user-local npm prefix, or enterprise-managed toolchain, withcds-lspavailable on PATH. - Without LSP integration, use the Markdown guidance, bundled references,
rg, and CAP CLI checks directly. - Mark live deployment, HANA, XSUAA, and multitenancy verification pending unless the target project or tenant evidence is available.
Project Structure
project/
├── app/ # UI content (Fiori, UI5)
├── srv/ # Service definitions (.cds, .js/.ts)
├── db/ # Data models and schema
│ ├── schema.cds # Entity definitions
│ └── data/ # CSV seed data
├── package.json # Dependencies and CDS config
└── .cdsrc.json # CDS configuration (optional)Core Concepts
CDS Built-in Types
| CDS Type | SQL Mapping | Common Use |
|---|---|---|
UUID | NVARCHAR(36) | Primary keys |
String(n) | NVARCHAR(n) | Text fields |
Integer | INTEGER | Whole numbers |
Decimal(p,s) | DECIMAL(p,s) | Monetary values |
Boolean | BOOLEAN | True/false |
Date | DATE | Calendar dates |
Timestamp | TIMESTAMP | Date/time |
Common Aspects
using { cuid, managed, temporal } from '@sap/cds/common';
// cuid = UUID key
// managed = createdAt, createdBy, modifiedAt, modifiedBy
// temporal = validFrom, validToEvent Handlers (Node.js)
// srv/cat-service.js
module.exports = class CatalogService extends cds.ApplicationService {
init() {
const { Books } = this.entities;
// Before handlers - validation
this.before('CREATE', Books, req => {
if (!req.data.title) req.error(400, 'Title required');
});
// On handlers - custom logic
this.on('submitOrder', async req => {
const { book, quantity } = req.data;
// Custom business logic
return { success: true };
});
return super.init();
}
}Basic CQL Queries
const { Books } = cds.entities;
// SELECT with conditions
const books = await SELECT.from(Books)
.where({ stock: { '>': 0 } })
.orderBy('title');
// INSERT
await INSERT.into(Books)
.entries({ title: 'New Book', stock: 10 });
// UPDATE
await UPDATE(Books, bookId)
.set({ stock: { '-=': 1 } });AI Integration
CAP applications integrate with SAP AI Core via the SAP Cloud SDK for AI. The recommended pattern uses the Orchestration Service through CAP event handlers, with all credential management handled by BTP service bindings.
Service Binding (MTA)
resources:
- name: my-ai-core
type: org.cloudfoundry.managed-service
parameters:
service: aicore
service-plan: extendedLocal Development (Hybrid Mode)
cds bind -2 <AICORE_INSTANCE> && cds-tsx watch --profile hybridEvent Handler Pattern (Node.js/TypeScript)
import { OrchestrationClient } from '@sap-ai-sdk/orchestration';
module.exports = class AnalysisService extends cds.ApplicationService {
async init() {
const { Feedback } = this.entities;
this.on('analyzeFeedback', async (req) => {
const userText = req.data.text;
const client = new OrchestrationClient({
promptTemplating: {
model: { name: 'gpt-4o' },
prompt: [
{ role: 'system', content: 'Categorize feedback as JSON: sentiment, category, urgency.' },
{ role: 'user', content: '{{?userText}}' }
]
}
});
const response = await client.chatCompletion({
placeholderValues: { userText }
});
const aiResult = response.getContent();
await INSERT.into('FeedbackResults').entries({
originalText: userText,
analysisJson: aiResult
});
return aiResult;
});
return super.init();
}
};Asynchronous Processing (Production Pattern)
LLM calls can take 30-60 seconds. Never process them synchronously in production — the BTP load balancer will timeout before the LLM responds.
this.on('analyzeFeedback', async (req) => {
const id = await INSERT.into('FeedbackResults').entries({
originalText: req.data.text,
status: 'processing'
});
cds.spawn(() => processWithLLM(id, req.data.text));
return req.reply(202, { id, status: 'processing' });
});
async function processWithLLM(id, text) {
const response = await client.chatCompletion({
placeholderValues: { userText: text }
});
await UPDATE('FeedbackResults', id).set({
analysisJson: response.getContent(),
status: 'completed'
});
}HANA Vector Type for RAG
entity Documents {
key id : UUID;
content : String(5000);
embedding : Vector(1536);
}Use this with the HANA Cloud Vector Engine and AI Core orchestration grounding to build RAG scenarios directly in your CAP data model.
Prompt Externalization
Do not hardcode prompts in event handlers. Store them in JSON files or a CDS configuration entity so they can be updated without redeployment:
entity PromptTemplates {
key id : UUID;
name : String(100);
systemPrompt : LargeString;
updatedBy : String;
modifiedAt : Timestamp;
}Memory and Deployment
Node.js containers with AI SDK processing large text payloads require at least 512MB memory in the MTA descriptor. The AI SDK and JSON payload handling consume more memory than typical CAP services.
For complete SDK documentation, see sap-cloud-sdk-ai skill. For AI Core platform setup and orchestration configuration, see sap-ai-core skill.
Database Setup
Development (SQLite)
// package.json
{
"cds": {
"requires": {
"db": {
"[development]": {
"kind": "sqlite",
"credentials": { "url": ":memory:" }
},
"[production]": { "kind": "hana" }
}
}
}
}Production (SAP HANA)
cds add hana
cds deploy --to hanaInitial Data (CSV)
- File location:
db/data/my.bookshop-Books.csv - Format:
<namespace>-<EntityName>.csv - Auto-loaded on deployment
Deployment
Cloud Foundry
# Add CF deployment support
cds add hana,xsuaa,mta,approuter
# Build and deploy
npm install --package-lock-only
mbt build
cf deploy mta_archives/<project>_<version>.mtarMultitenancy (SaaS)
cds add multitenancyConfiguration:
{
"cds": {
"requires": {
"multitenancy": true
}
}
}Authorization Examples
// Service-level
@requires: 'authenticated-user'
service CatalogService { ... }
// Entity-level
@restrict: [
{ grant: 'READ' },
{ grant: 'WRITE', to: 'admin' }
]
entity Books { ... }Bundled Resources
Reference Documentation (22 files)
1. references/annotations-reference.md - Complete UI annotations reference (10K lines) 2. references/cdl-syntax.md - Complete CDL syntax reference (503 lines) 3. references/cql-queries.md - CQL query language guide 4. references/csn-cqn-cxn.md - Core Schema Notation and query APIs 5. references/data-privacy-security.md - GDPR and security implementation 6. references/databases.md - Database configuration and deployment 7. references/deployment-cf.md - Cloud Foundry deployment details 8. references/event-handlers-nodejs.md - Node.js event handler patterns 9. references/extensibility-multitenancy.md - SaaS multitenancy implementation 10. references/fiori-integration.md - Fiori Elements and UI integration 11. references/java-runtime.md - Java runtime support 12. references/localization-temporal.md - i18n and temporal data 13. references/nodejs-runtime.md - Node.js runtime reference 14. references/plugins-reference.md - CAP plugins and extensions 15. references/tools-complete.md - Complete CLI tools reference 16. references/consuming-services-deployment.md - Service consumption patterns 17. references/service-definitions.md - Service definition patterns 18. references/event-handlers-patterns.md - Event handling patterns 19. references/cql-patterns.md - CQL usage patterns 20. references/cli-complete.md - Complete CLI reference 21. references/mcp-integration.md - MCP server setup and usage guide (new) 22. references/mcp-use-cases.md - Illustrative MCP workflow scenarios (new)
Templates (8 files)
1. templates/bookshop-schema.cds - Complete data model example 2. templates/catalog-service.cds - Service definition template 3. templates/fiori-annotations.cds - UI annotations example 4. templates/mta.yaml - Multi-target application descriptor 5. templates/package.json - Project configuration template 6. templates/service-handler.js - Node.js handler template 7. templates/service-handler.ts - TypeScript handler template 8. templates/xs-security.json - XSUAA security configuration
Quick References
- CAP Documentation: https://cap.cloud.sap/docs/
- CDS Language: https://cap.cloud.sap/docs/cds/
- Node.js Runtime: https://cap.cloud.sap/docs/node.js/
- Java Runtime: https://cap.cloud.sap/docs/java/
- Best Practices: https://cap.cloud.sap/docs/about/best-practices
- GitHub Repository: https://github.com/cap-js/docs
Common CLI Commands
cds init [name] # Create project
cds add <feature> # Add capability
cds watch # Dev server with live reload
cds serve # Start server
cds compile <model> # Compile CDS to CSN/SQL/EDMX
cds deploy --to hana # Deploy to HANA
cds build # Build for deployment
cds env # Show configuration
cds repl # Interactive REPL
cds version # Show version infoBest Practices
DO ✓
- Use
cuidandmanagedaspects from@sap/cds/common - Keep domain models in
db/, services insrv/, UI inapp/ - Use managed associations (let CAP handle foreign keys)
- Design single-purpose services per use case
- Start with SQLite, switch to HANA for production
DON'T ✗
- Don't use SELECT * - be explicit about projections
- Don't bypass CAP's query API with raw SQL
- Don't create microservices prematurely
- Don't hardcode credentials in config files
- Don't write custom OData providers
Version Information
- CAP Version: @sap/cds 9.7.x
- MCP Version: @cap-js/mcp-server 0.0.5
- LSP Version: @sap/cds-lsp 9.7.x
- License: GPL-3.0
SAP CAP-Capire Skill
A comprehensive AI coding assistant skill for SAP Cloud Application Programming Model (CAP) development.
Capability Index
| Capability | Status |
|---|---|
| Commands | 5: /cap-cds-reference, /cap-deployment-checklist, /cap-mcp-tools, /cap-setup-wizard, /cap-troubleshooter |
| Agents | 4: cap-cds-modeler, cap-performance-debugger, cap-project-architect, cap-service-developer |
| Hooks | Yes: hooks/hooks.json |
| MCP | Yes: .mcp.json |
| LSP | Yes: .lsp.json |
| Source Freshness | last_verified: 2026-02-22; CAP MCP/LSP package freshness noted separately in audit report. |
| Verification | npm run validate; live CAP tenant/deployment checks pending unless project evidence exists. |
LSP Usage By Harness
CAP LSP support requires a Node runtime and @sap/cds-lsp installed through an approved project-local devDependency, user-local npm prefix, or enterprise-managed toolchain, with cds-lsp available on PATH. The bundled launcher fixes Windows npm .cmd shim resolution without bundling or installing the SAP package.
- Claude Code: the installed plugin uses
.lsp.jsonas its optional LSP activation file. - Codex, OpenCode, editors, and other LSP-capable clients: configure the LSP command manually as
node <sap-cap-capire-plugin-root>/lsp/cds-lsp-launcher.mjs --stdio. - No LSP integration: use this Markdown skill, bundled references,
rg, and CAP CLI checks directly.
Overview
This skill provides guidance for building enterprise-grade applications using SAP CAP framework. It covers the complete development lifecycle from project initialization to production deployment.
When to Use This Skill
Use this skill when:
- Creating CAP projects:
cds init, project structure, configuration - Defining data models: CDS entity definitions, types, associations, compositions
- Implementing services: Service definitions, projections, actions, functions
- Writing event handlers: Before/on/after handlers, validation, business logic
- Working with databases: SQLite, SAP HANA, PostgreSQL configuration
- Building Fiori UIs: UI annotations, drafts, value helps, field control
- Deploying applications: Cloud Foundry, Kyma, MTA builds
- Implementing security: Authorization, authentication, XSUAA
- Enabling multitenancy: SaaS applications, MTX, tenant isolation
- Using messaging: Event emission, subscriptions, messaging services
- Integrating plugins: Attachments, audit logging, change tracking
Keywords
Core Technologies
- SAP CAP
- Cloud Application Programming Model
- CDS
- CDL (Conceptual Definition Language)
- CQL (Conceptual Query Language)
- CSN (Core Schema Notation)
- Capire
Development
- cds init
- cds watch
- cds add
- cds deploy
- cds serve
- cds build
- cds-dk
- @sap/cds
- @sap/cds-dk
- Node.js CAP
- Java CAP
- TypeScript CAP
- cds-typer
Data Modeling
- CDS entities
- CDS aspects
- CDS types
- managed associations
- compositions
- cuid
- managed aspect
- temporal data
- localized data
- @sap/cds/common
Services
- CDS services
- service projections
- OData services
- REST services
- GraphQL CAP
- actions
- functions
- event handlers
- before handlers
- after handlers
Databases
- SAP HANA
- SQLite
- PostgreSQL
- H2
- HDI container
- cds deploy
- database configuration
- @cap-js/sqlite
- @cap-js/hana
- @cap-js/postgres
Fiori Integration
- Fiori Elements
- UI annotations
- @UI.LineItem
- @UI.HeaderInfo
- @UI.Facets
- @UI.FieldGroup
- draft handling
- @odata.draft.enabled
- value help
- field control
Security
- @requires
- @restrict
- authorization
- authentication
- XSUAA
- xs-security.json
- roles
- grants
Deployment
- Cloud Foundry
- CF deployment
- Kyma
- MTA
- mta.yaml
- cds up
- mbt build
- cf deploy
- approuter
Multitenancy
- SaaS
- multitenancy
- MTX
- @sap/cds-mtxs
- tenant isolation
- subscription
Messaging
- CAP messaging
- event emission
- srv.emit
- messaging service
- enterprise-messaging
- file-based-messaging
- Event Mesh
Plugins
- @cap-js/attachments
- @cap-js/audit-logging
- @cap-js/change-tracking
- @cap-js/telemetry
- @cap-js/graphql
- OData V2 adapter
MCP Integration
- MCP
- Model Context Protocol
- @cap-js/mcp-server
- search_model
- search_docs
- CSN model search
- semantic documentation search
- AI-assisted CAP development
- model discovery
- CAP documentation query
Common Errors
- CDS compilation error
- OData error
- HANA deployment error
- Authorization error
- Draft error
- Validation error
- Service binding error
Directory Structure
sap-cap-capire/
├── SKILL.md # Main skill file (342 lines)
├── README.md # This file
├── references/ # 22 comprehensive reference files
│ ├── annotations-reference.md # UI annotations reference (10K lines)
│ ├── cdl-syntax.md # Complete CDL syntax reference (503 lines)
│ ├── cql-queries.md # CQL query language guide
│ ├── csn-cqn-cxn.md # Core Schema Notation and query APIs
│ ├── data-privacy-security.md # GDPR and security implementation
│ ├── databases.md # Database configuration and deployment
│ ├── deployment-cf.md # Cloud Foundry deployment details
│ ├── event-handlers-nodejs.md # Node.js event handler patterns
│ ├── extensibility-multitenancy.md # SaaS multitenancy implementation
│ ├── fiori-integration.md # Fiori Elements and UI integration
│ ├── java-runtime.md # Java runtime support
│ ├── localization-temporal.md # i18n and temporal data
│ ├── nodejs-runtime.md # Node.js runtime reference
│ ├── plugins-reference.md # CAP plugins and extensions
│ ├── tools-complete.md # Complete CLI tools reference
│ ├── consuming-services-deployment.md # Service consumption patterns
│ ├── service-definitions.md # Service definition patterns
│ ├── event-handlers-patterns.md # Event handling patterns
│ ├── cql-patterns.md # CQL usage patterns
│ ├── cli-complete.md # Complete CLI reference
│ ├── mcp-integration.md # MCP server setup and usage guide (new)
│ └── mcp-use-cases.md # Illustrative MCP workflow scenarios (new)
└── templates/ # 8 practical template files
├── bookshop-schema.cds # Complete data model example
├── catalog-service.cds # Service definition template
├── fiori-annotations.cds # UI annotations example
├── mta.yaml # Multi-target application descriptor
├── package.json # Project configuration template
├── service-handler.js # Node.js handler template
├── service-handler.ts # TypeScript handler template
└── xs-security.json # XSUAA security configurationDocumentation Links
- CAP Documentation: https://cap.cloud.sap/docs/
- CAP GitHub: https://github.com/cap-js/docs
- SAP Samples: https://github.com/SAP-samples/cloud-cap-samples
- CDS Language: https://cap.cloud.sap/docs/cds/
- Node.js Runtime: https://cap.cloud.sap/docs/node.js/
- Java Runtime: https://cap.cloud.sap/docs/java/
- Plugins: https://cap.cloud.sap/docs/plugins/
Version Information
- Skill Version: 2.1.0
- CAP Version: @sap/cds 9.4.x
- MCP Version: @cap-js/mcp-server 0.0.5
- Last Verified: 2026-02-22
License
GPL-3.0
CDS Annotations Complete Reference
Source: https://cap.cloud.sap/docs/cds/annotations
Table of Contents
- Core Annotations
- Access Control Annotations
- Draft Annotations
- Object Model (OM) Annotations
- OData Annotations
- UI Annotations
- Analysis Annotations
- Personalized Recommendations
- Communication Scenario
- Field Control
- Value Help
- Media Annotations
- Hierarchy Annotations
- Temporal Annotations
- Custom Annotations
Core Annotations
General Purpose
| Annotation | Purpose | Example |
|---|---|---|
@title | Display label (maps to Common.Label) | @title: 'Book Title' |
@description | Description text (maps to Core.Description) | @description: 'The book title' |
Access Control
| Annotation | Purpose | Example |
|---|---|---|
@readonly | Prevent modifications | @readonly entity Logs { } |
@insertonly | Allow only CREATE | @insertonly entity Orders { } |
@requires | Role-based access | @requires: 'admin' |
@restrict | Fine-grained authorization | See Authorization section |
@readonly semantics:
- Entity-level: Blocks CREATE, UPDATE, DELETE operations. Only READ allowed.
- Field-level: Field cannot be modified in UPDATE/CREATE payloads.
- Example:
@readonly createdAt : Timestamp- auto-set by system, user cannot modify.
Input Validation
| Annotation | Purpose | Example |
|---|---|---|
@mandatory | Require non-null value | title : String @mandatory |
@assert.format | Regex validation | @assert.format: '[a-z]+' |
@assert.range | Value range | @assert.range: [0, 100] |
@assert.target | FK reference exists | author : Association to Authors @assert.target |
Services & APIs
| Annotation | Purpose | Example |
|---|---|---|
@path | Custom service endpoint | @path: '/api/v1' |
@impl | Custom implementation | @impl: './my-service.js' |
@protocol | Expose protocols | @protocol: ['odata', 'rest'] |
@cds.autoexpose | Auto-expose in services | @cds.autoexpose entity Codes { } |
@cds.api.ignore | Hide from API | field @cds.api.ignore |
Persistence
| Annotation | Purpose | Example |
|---|---|---|
@cds.persistence.exists | Table exists externally | @cds.persistence.exists |
@cds.persistence.skip | Don't create table | @cds.persistence.skip |
@cds.persistence.table | Force table (not view) | @cds.persistence.table |
@cds.on.insert | Value on insert | @cds.on.insert: $now |
@cds.on.update | Value on update | @cds.on.update: $user |
Query Behavior
| Annotation | Purpose | Example |
|---|---|---|
@cds.query.limit | Default/max results | @cds.query.limit: { default: 20, max: 100 } |
@cds.search | Searchable fields | @cds.search: { title, author.name } |
Localization
| Annotation | Purpose | Example |
|---|---|---|
@cds.localized | Enable translations | @cds.localized: false to disable |
Temporal Data
| Annotation | Purpose | Example |
|---|---|---|
@cds.valid.from | Valid from timestamp | validFrom @cds.valid.from |
@cds.valid.to | Valid to timestamp | validTo @cds.valid.to |
OData Annotations
Type Mapping
| Annotation | Purpose | Example |
|---|---|---|
@odata.Type | Override EDM type | @odata.Type: 'Edm.String' |
@odata.MaxLength | Max string length | @odata.MaxLength: 1000 |
@odata.Precision | Decimal precision | @odata.Precision: 10 |
@odata.Scale | Decimal scale | @odata.Scale: 2 |
Entity Behavior
| Annotation | Purpose | Example |
|---|---|---|
@odata.draft.enabled | Enable drafts | @odata.draft.enabled |
@odata.singleton | Single-instance entity | @odata.singleton |
@odata.etag | ETag field | modifiedAt @odata.etag |
Core Vocabulary (OData)
Computed & Immutable
entity Books {
@Core.Computed // Read-only, server-computed
createdAt : Timestamp;
@Core.Immutable // Set once, then read-only
key ID : UUID;
}Media Types
entity Documents {
@Core.MediaType: 'application/pdf'
content : LargeBinary;
@Core.MediaType: mediaType
@Core.ContentDisposition.Filename: fileName
attachment : LargeBinary;
@Core.IsMediaType
mediaType : String;
fileName : String;
}UI Annotations (Fiori)
Header Info
annotate Books with @UI.HeaderInfo: {
TypeName : 'Book',
TypeNamePlural : 'Books',
Title : { Value: title },
Description : { Value: author.name },
ImageUrl : coverImage
};Selection Fields (Filter Bar)
annotate Books with @UI.SelectionFields: [
title,
author_ID,
genre_code,
price
];Line Item (List Columns)
annotate Books with @UI.LineItem: [
{ Value: title, Label: 'Title' },
{ Value: author.name, Label: 'Author' },
{ Value: stock },
{ Value: price },
{ $Type: 'UI.DataFieldForAction', Action: 'CatalogService.order', Label: 'Order' }
];Facets (Object Page Sections)
annotate Books with @UI.Facets: [
{
$Type : 'UI.ReferenceFacet',
Target : '@UI.FieldGroup#General',
Label : 'General Information'
},
{
$Type : 'UI.ReferenceFacet',
Target : 'reviews/@UI.LineItem',
Label : 'Reviews'
},
{
$Type : 'UI.CollectionFacet',
Label : 'Details',
Facets : [
{ $Type: 'UI.ReferenceFacet', Target: '@UI.FieldGroup#Details' }
]
}
];Field Groups
annotate Books with @UI.FieldGroup#General: {
Data: [
{ Value: title },
{ Value: descr },
{ Value: author_ID, Label: 'Author' },
{ Value: genre_code }
]
};
annotate Books with @UI.FieldGroup#Pricing: {
Data: [
{ Value: price },
{ Value: currency_code },
{ Value: stock }
]
};Identification (Header Actions)
annotate Books with @UI.Identification: [
{ $Type: 'UI.DataFieldForAction', Action: 'CatalogService.order', Label: 'Order Now' }
];Hidden Fields
annotate Books with {
createdBy @UI.Hidden;
modifiedBy @UI.Hidden;
};Criticality (Status Colors)
annotate Orders with @UI.LineItem: [
{
Value: status,
Criticality: criticality,
CriticalityRepresentation: #WithIcon
}
];Criticality Values and Visual Representation:
| Value | Constant | Color | Icon | Use Case |
|---|---|---|---|---|
| 0 | Neutral | Grey | None | Default/informational |
| 1 | Negative | Red | ❌ | Error, failed, rejected |
| 2 | Critical | Yellow/Orange | ⚠️ | Warning, needs attention |
| 3 | Positive | Green | ✓ | Success, completed, approved |
Computed Field Example:
entity Orders {
status : String;
criticality : Integer = case
when status = 'cancelled' then 1
when status = 'pending' then 2
when status = 'delivered' then 3
else 0
end stored;
}Common Annotations
Labels & Text
annotate Books with {
@Common.Label: 'Book Title'
title;
@Common.Text: author.name
@Common.TextArrangement: #TextOnly
author_ID;
};Value Lists (Dropdowns)
annotate Books with {
@Common.ValueList: {
Label: 'Authors',
CollectionPath: 'Authors',
Parameters: [
{ $Type: 'Common.ValueListParameterInOut', LocalDataProperty: author_ID, ValueListProperty: 'ID' },
{ $Type: 'Common.ValueListParameterDisplayOnly', ValueListProperty: 'name' }
]
}
author_ID;
};
// Shorthand for simple value lists
@cds.odata.valuelist
entity Genres { ... }Field Control
annotate Orders with {
@Common.FieldControl: #ReadOnly
totalAmount;
// Dynamic: #Mandatory when status is 'draft'
@Common.FieldControl: { $edmJson: {
$If: [
{ $Eq: [{ $Path: 'status' }, 'draft'] },
1, // Mandatory
3 // Optional
]
}}
customerNote;
};
// FieldControl values:
// 0 = Inapplicable (hidden)
// 1 = Mandatory
// 3 = Optional
// 7 = ReadOnlySemantic Key
annotate Books with @Common.SemanticKey: [isbn];Capabilities Annotations
annotate CatalogService.Books with @Capabilities: {
InsertRestrictions: { Insertable: false },
UpdateRestrictions: { Updatable: true },
DeleteRestrictions: { Deletable: false },
FilterRestrictions: {
Filterable: true,
RequiredProperties: [title],
NonFilterableProperties: [descr]
},
SortRestrictions: {
NonSortableProperties: [descr, content]
}
};Authorization Annotations
@requires
@requires: 'authenticated-user'
service CatalogService { ... }
@requires: ['admin', 'manager'] // OR logic
entity SensitiveData { ... }@restrict
@restrict: [
{ grant: 'READ' }, // Anyone can read
{ grant: 'CREATE', to: 'creator' },
{ grant: 'UPDATE', to: 'editor', where: 'createdBy = $user' },
{ grant: 'DELETE', to: 'admin' },
{ grant: '*', to: 'superadmin' }
]
entity Documents { ... }Grant Types
READ- GET requestsCREATE- POST requestsUPDATE- PUT/PATCH requestsDELETE- DELETE requestsWRITE- CREATE + UPDATE + DELETE*- All operations- Action/function names
Where Conditions
@restrict: [
// User attribute matching
{ grant: 'READ', where: '$user.country = country_code' },
// Owner matching
{ grant: 'UPDATE', where: 'createdBy = $user' },
// Exists subquery
{ grant: 'READ', where: 'exists members[user_ID = $user.id]' },
// Complex conditions
{ grant: 'READ', where: 'status = #published or createdBy = $user' }
]SQL Annotations
Native SQL
@sql.append: 'WITH ASSOCIATIONS'
entity Books { ... }
@sql.prepend: 'CREATE COLUMN TABLE'
entity ColumnTable { ... }
entity Books {
@sql.append: 'FUZZY SEARCH INDEX ON'
title : String(200);
}Data Privacy Annotations
entity Customers {
@PersonalData.IsPotentiallyPersonal
name : String;
@PersonalData.IsPotentiallySensitive
healthInfo : String;
@PersonalData.FieldSemantics: #EmailAddress
email : String;
}
@PersonalData.EntitySemantics: #DataSubject
entity Customers { ... }Analytics Annotations
@Aggregation.ApplySupported: {
Transformations: ['aggregate', 'groupby', 'filter'],
GroupableProperties: [category, region],
AggregatableProperties: [{
Property: amount,
SupportedAggregationMethods: ['sum', 'avg', 'min', 'max']
}]
}
entity Sales { ... }CDL (Conceptual Definition Language) Complete Syntax Reference
Source: https://cap.cloud.sap/docs/cds/cdl
Table of Contents
- Keywords & Identifiers
- Literals & Values
- Comments
- Model Organization
- Entity Definitions
- Views & Projections
- Associations
- Compositions
- Annotations
- Aspects
- Services
Keywords & Identifiers
- Keywords are case-insensitive (typically lowercase)
- Identifiers are case-sensitive; must match
/^[$A-Za-z_]\w*$/ - Delimited identifiers use
![identifier name]syntax (avoid when possible)
Literals & Values
// Booleans
true, false, null
// Numbers
11, 2.4, 1e3, 1.23e-11
// Strings
'single quotes'
`backtick for
multiline strings`
// Records & Arrays
{ key: 'value', nested: { prop: 1 } }
[1, 'two', { three: 4 }]
// Date/Time literals
date'2024-11-22'
time'16:30:00'
timestamp'2024-11-22T16:30:00Z'Comments
// Line-end comment
/* Block comment
spanning multiple lines */
/**
* Doc comment - stored in CSN 'doc' property
* Supports markdown formatting
*/
entity Books { ... }Model Organization
Namespaces
namespace my.bookshop;
entity Books { ... } // Full name: my.bookshop.BooksContexts
context admin {
entity Users { ... } // Full name: my.bookshop.admin.Users
}Scoped Names
entity Orders { ... }
entity Orders.Items { ... } // Scoped under OrdersImports
using my.bookshop.Books from './db/schema';
using { Authors, Genres } from './db/schema';
using { Books as MyBooks } from './db/schema';
using * from './db/schema'; // Import all definitionsEntity Definitions
Basic Entity
entity Books {
key ID : UUID;
title : String(255) not null;
descr : String(5000);
stock : Integer default 0;
price : Decimal(10,2);
available : Boolean default true;
}Structured Elements
type Address {
street : String;
city : String;
zipCode : String(10);
country : Country;
}
entity Customers {
homeAddress : Address;
workAddress : Address;
}Arrayed Types
entity Contacts {
emails : many String; // Array of strings
phones : array of String(20); // Same as above
tags : many { name: String }; // Array of structs
}Virtual Elements
entity Employees {
// Not persisted, computed at runtime
virtual fullName : String @Core.Computed;
}Calculated Elements
On-read (computed during queries):
entity Employees {
firstName : String;
lastName : String;
fullName : String = firstName || ' ' || lastName;
upperName : String = upper(fullName);
}Stored (persisted in database):
entity Employees {
fullName : String = (firstName || ' ' || lastName) stored;
}Default Values
entity Orders {
status : String default 'draft';
priority : Integer default 5;
createdAt : Timestamp default $now;
createdBy : String default $user;
}Type References
entity Books {
title : String(200);
subtitle : type of title; // Same type as title
}
entity Reviews {
bookTitle : Books:title; // Reference Books.title type
}Constraints
entity Books {
title : String(200) not null;
isbn : String(13) not null unique;
}
// Unique constraints
@assert.unique: {
isbn: [isbn],
titleAuthor: [title, author_ID]
}
entity Books { ... }Enumerations
// String enum
type Status : String enum {
draft;
submitted;
approved;
rejected = 'REJ'; // Custom value
}
// Integer enum
type Priority : Integer enum {
low = 1;
medium = 2;
high = 3;
}
entity Orders {
status : Status default #draft;
priority : Priority default #medium;
}Views & Projections
As Select From
entity BooksList as select from Books {
ID,
title,
author.name as authorName,
stock * price as value : Decimal
} where stock > 0
order by title;As Projection On
entity PublicBooks as projection on Books {
ID, title, descr, author
} excluding { createdBy, modifiedBy };View with Parameters
entity FilteredBooks(minStock: Integer, maxPrice: Decimal)
as select from Books
where stock >= :minStock
and price <= :maxPrice;Associations
Managed To-One
entity Books {
author : Association to Authors;
// CAP auto-generates foreign key column: author_ID
// The FK type matches the target's primary key (UUID if Authors uses cuid)
// No explicit definition needed - CAP handles persistence automatically
}Managed To-One with Default
entity Orders {
customer : Association to Customers default 'DEFAULT_CUSTOMER_ID';
}Unmanaged Association
entity Books {
author : Association to Authors on author.ID = author_ID;
author_ID : UUID; // Explicit foreign key - REQUIRED for unmanaged associations
// Unlike managed associations, you must define the FK field yourself
// and specify the ON condition to join the entities
}To-Many Association
entity Authors {
books : Association to many Books on books.author = $self;
}Many-to-Many (via Link Entity)
entity Books {
categories : Association to many Book2Category on categories.book = $self;
}
entity Categories {
books : Association to many Book2Category on books.category = $self;
}
entity Book2Category {
key book : Association to Books;
key category : Association to Categories;
}Infix Filters on Associations
entity Authors {
// Filter: only books with stock > 0
availableBooks : Association to many Books on availableBooks.author = $self
and availableBooks.stock > 0;
}
// In projections
entity AuthorDetails as projection on Authors {
*,
books[stock > 0] as availableBooks,
books[1: favorite = true] as favoriteBook // 1: reduces to single
}Compositions
Basic Composition
entity Orders {
key ID : UUID;
Items : Composition of many OrderItems on Items.order = $self;
}
entity OrderItems {
key ID : UUID;
order : Association to Orders;
product : Association to Products;
quantity : Integer;
}Managed Composition (Inline)
entity Orders {
key ID : UUID;
Items : Composition of many {
key pos : Integer;
product : Association to Products;
quantity : Integer;
price : Decimal(10,2);
};
}
// Auto-creates: entity Orders.Items { ... }Managed Composition (Named Aspect)
aspect OrderItem {
key pos : Integer;
product : Association to Products;
quantity : Integer;
}
entity Orders {
key ID : UUID;
Items : Composition of many OrderItem;
}Annotations
Syntax Variations
@title: 'Books'
@description: 'All books in the catalog'
entity Books { ... }
// Multiple on same line
@readonly @cds.persistence.skip
entity TempData { ... }
// Grouped
@(
title: 'Books',
description: 'All books',
UI.HeaderInfo: { TypeName: 'Book' }
)
entity Books { ... }Annotation Values
@flag // true (default)
@enabled: true // boolean
@count: 42 // integer
@rate: 3.14 // decimal
@label: 'Hello' // string
@status: #active // symbol/enum
@items: [1, 2, 3] // array
@config: { a: 1, b: 2 } // record
@expr: (price * quantity) // expressionElement-Level Annotations
entity Books {
@title: 'Book Title'
@mandatory
title : String;
@readonly
@Core.Computed
totalValue : Decimal;
}Annotation Propagation
Annotations propagate through:
- Type inheritance
- Views and projections
- Association navigation
Stop propagation with null:
annotate ChildEntity with {
field @parentAnnotation: null
}Aspects
Define Aspect
aspect tracked {
createdAt : Timestamp @cds.on.insert: $now;
createdBy : String(255) @cds.on.insert: $user;
modifiedAt : Timestamp @cds.on.insert: $now @cds.on.update: $now;
modifiedBy : String(255) @cds.on.insert: $user @cds.on.update: $user;
}Apply Aspect (Shortcut Includes)
entity Books : tracked {
key ID : UUID;
title : String;
}Extend Directive
extend Books with {
newField : String;
otherField : Integer;
}
extend Books with tracked;
extend Books:price with @title: 'Price';Annotate Directive
annotate Books with @title: 'Books';
annotate Books with {
title @title: 'Book Title';
price @Measures.Unit: currency_code;
}Services
Service Definition
service CatalogService @(path: '/catalog') {
entity Books as projection on my.Books;
entity Authors as projection on my.Authors;
}Multiple Protocols
@protocol: ['odata', 'rest']
service MultiProtocolService { ... }Bound Actions/Functions
service OrderService {
entity Orders { ... } actions {
action confirm();
action cancel(reason: String);
function getTotal() returns Decimal;
};
}Unbound Actions/Functions
service UtilityService {
action sendNotification(to: String, message: String);
function calculateTax(amount: Decimal, region: String) returns Decimal;
}Custom Events
service OrderService {
event OrderCreated {
orderID : UUID;
customer : String;
total : Decimal;
timestamp : Timestamp;
}
event OrderCancelled : projection on OrderCreated {
orderID, reason: String
}
}Internal Services
@protocol: 'none' // No external access
service InternalService { ... }CAP CLI - Complete Command Reference
Source: <https://cap.cloud.sap/docs/cli/>
Table of Contents
- Project Commands
- Build & Deploy Commands
- Development Commands
- Database Commands
- Service Commands
- Utility Commands
- Plugin Commands
- Development Workflow Examples
- Troubleshooting
- Configuration
- Advanced Options
- Best Practices
Project Commands
cds init
Create a new CAP project
# Basic project
cds init [project-name]
# With sample data and HANA support
cds init my-bookshop --add sample,hana
# With TypeScript
cds init my-project --add typescript
# With multiple features
cds init my-app --add sample,hana,xsuaa,mta,multitenancyFeatures Available:
sample- Add sample bookshop modelhana- Add SAP HANA database supportsqlite- Add SQLite for developmentxsuaa- Add authentication servicemta- Add Multi-Target Application supportapprouter- Add approuter for HTML5 appsmultitenancy- Add SaaS multitenancy supporttypescript- Add TypeScript supportFiori- Add Fiori templates
cds add
Add capabilities to existing project
# Add features to existing project
cds add hana
cds add xsuaa
cds add typescript
cds add multitenancy
cds add mta
cds add approuter
# Add multiple features
cds add hana,xsuaa,mtaBuild & Deploy Commands
cds build
Build project for deployment
# Build all targets
cds build
# Build specific target
cds build --for hana
cds build --for production
# Clean build
cds build --cleancds deploy
Deploy database schema
# Deploy to configured database
cds deploy
# Deploy to HANA
cds deploy --to hana
# Deploy with auto-migration
cds deploy --to hana --auto-migrate
# Dry run (show SQL without executing)
cds deploy --to hana --drycds compile
Compile CDS models
# Compile to CSN
cds compile db/schema.cds
# Compile to SQL
cds compile db/schema.cds --to sql
# Compile to EDMX (OData)
cds compile srv/cat-service.cds --to edmx
# Compile to YAML
cds compile db/schema.cds --to yaml
# Compile with flavor
cds compile srv/cat-service.cds --to edmx --flavor v4Development Commands
cds watch
Start development server with live reload
# Start with default port
cds watch
# Start on specific port
cds watch --port 4004
# Start with HANA
cds watch --profile development,hana
# Start with in-memory database
cds watch --in-memory
# Start with custom profile
cds watch --profile my-profilecds serve
Start production server
# Start server
cds serve
# Serve specific service
cds serve all
cds serve CatalogService
# Serve with custom port
cds serve --port 4004cds run
Run custom scripts
# Run with custom entry point
cds run my-script.js
# Run with environment variables
cds run --env NODE_ENV=productionDatabase Commands
cds migrate
Run database migrations
# Run all pending migrations
cds migrate
# Migrate to HANA
cds migrate --to hana
# Dry run migration
cds migrate --to hana --drycds load
Load initial data
# Load all CSV data
cds load
# Load specific CSV
cds load db/data/my.bookshop-Books.csv
# Load with CSV import options
cds load --delim ";" --quote '"'cds diff
Compare database schemas
# Compare model with database
cds diff
# Diff specific entity
cds diff Books
# Diff in SQL format
cds diff --to sqlService Commands
cds bind
Bind external services
# Bind to external OData service
cds bind ExternalService --to https://api.example.com
# Bind with credentials
cds bind ExternalService --to-cred '{"url":"https://api.example.com"}'cds tunnel
Create tunnel to remote services
# Create tunnel
cds tunnel
# Tunnel to specific service
cds tunnel --to my-serviceUtility Commands
cds env
Show configuration
# Show all environment
cds env
# Show specific setting
cds env get requires.db.kind
# Set environment variable
cds env set requires.db.kind hana
# Show effective configuration
cds env get --effectivecds version
Show version information
# Show version
cds version
# Show detailed version info
cds version --fullcds help
Show help information
# General help
cds help
# Help for specific command
cds help watch
cds help deploycds repl
Interactive CAP shell
# Start REPL
cds repl
# REPL with preloaded entities
cds repl srv/cat-servicecds plugins
Manage CLI plugins
# List installed plugins
cds plugins
# Install plugin
npm install -g @cap-js-community/odata-v2-adapter
# Show plugin help
cds help odata-v2Configuration
Profiles
# Use specific profile
cds watch --profile production
# Use multiple profiles
cds watch --profile development,hana
# Set default profile in package.json
{
"cds": {
"profiles": {
"development": {
"requires": {
"db": { "kind": "sqlite" }
}
},
"production": {
"requires": {
"db": { "kind": "hana" }
}
}
}
}
}Environment Variables
# Set database URL
export CDS_requires_db_credentials_url=postgresql://user:pass@host:5432/db
# Set service bindings
export CDS_requires_myService_credentials='{"url":"https://api.example.com"}'
# Set profile
export NODE_ENV=production
export CDS_ENV=production
# Set custom properties
export CDS_custom_prop=valueConfiguration Files
// .cdsrc.json
{
"requires": {
"db": {
"kind": "sqlite",
"credentials": {
"url": ":memory:"
}
},
"[production]": {
"db": {
"kind": "hana",
"credentials": {
"url": "hana-url"
}
}
}
},
"build": {
"target": "gen",
"tasks": [{
"for": "hana",
"src": "db"
}]
}
}Advanced Options
Watch Options
# Watch specific folders
cds watch --watch db srv
# Exclude files from watching
cds watch --exclude node_modules
# Live reload on changes
cds watch --livereload
# Open browser on start
cds watch --openDeploy Options
# Deploy with custom model
cds deploy db/schema.cds --to hana
# Deploy with auto-healing
cds deploy --to hana --auto-heal
# Deploy with cascading deletes
cds deploy --to hana --cascading
# Deploy in verbose mode
cds deploy --to hana --verboseCompile Options
# Compile with specific output format
cds compile db/schema.cds --to json --output model.json
# Compile with imports
cds compile db/schema.cds --to sql --with-imports
# Compile for specific flavor
cds compile srv/cat-service.cds --to edmx --flavor v4Build Options
# Build for production
cds build --production
# Build without minification
cds build --no-minify
# Build with inline sourcemaps
cds build --sourceMap
# Build for custom target
cds build --target ./distPlugin Commands
OData V2 Adapter
# Install V2 adapter
npm install -g @cap-js-community/odata-v2-adapter
# Run V2 adapter
cds odata-v2
# Run with custom port
cds odata-v2 --port 4005GraphQL Adapter
# Install GraphQL adapter
npm install @cap-js/graphql
# Run GraphQL server
cds watch --graphql
# GraphQL endpoint: http://localhost:4004/graphqlFiori Tools
# Install Fiori tools
npm install -g @sap/ux-ui5-tooling
# Generate Fiori app
cds watch --fiori
# Launch Fiori Preview
cds launch --open /previewDevelopment Workflow Examples
Initial Project Setup
# Create new project
cds init my-bookshop --add sample,hana
# Install dependencies
npm install
# Start development
cds watchAdding New Features
# Add authentication
cds add xsuaa
# Add multitenancy
cds add multitenancy
# Add TypeScript
cds add typescript
# Rebuild
npm run buildDatabase Management
# Load initial data
cds load
# Deploy to HANA
cds deploy --to hana
# Check for differences
cds diff --to sqlDeployment Preparation
# Build for production
cds build --production
# Create MTA archive
mbt build
# Deploy to Cloud Foundry
cf deploy mta_archives/my-app_1.0.0.mtarTroubleshooting
Common Issues
# Clear build cache
rm -rf node_modules/.cache
cds build --clean
# Check configuration
cds env get --effective
# Test database connection
cds deploy --to hana --dry
# Check model compilation
cds compile db/schema.cdsDebug Mode
# Enable debug logs
DEBUG=cds* cds watch
# Verbose output
cds deploy --to hana --verbose
# Show effective config
cds env get --effectiveBest Practices
Development
- Use
cds watchfor development with live reload - Use SQLite for local development
- Use profiles for different environments
- Keep models in
db/and services insrv/
Deployment
- Always run
cds buildbefore deployment - Use
cds deploy --dryto check SQL before execution - Use MTA for Cloud Foundry deployment
- Include
cds buildin CI/CD pipeline
Performance
- Use
cds compile --to sqlto review generated SQL - Use
--no-minifyfor debugging production builds - Use
--cascadingcarefully in production - Monitor build times for large models
Last Updated: 2025-11-27 CAP Version: 9.4.x Documentation: <https://cap.cloud.sap/docs/cli/>
Consuming Services & Deployment Reference
Source: https://cap.cloud.sap/docs/guides/using-services, https://cap.cloud.sap/docs/guides/deployment/
Consuming External Services
CAP enables consumption of remote services through uniform APIs.
Import External APIs
# Download from SAP Business Accelerator Hub or export from CAP
cds import <file> --as cds
# Supported formats
cds import api.edmx --as cds # OData V2/V4
cds import api.json --as cds # OpenAPI
cds import events.json --as cds # AsyncAPIFiles are placed in srv/external/.
Local Mocking
# Add mock data
srv/external/data/API_BUSINESS_PARTNER-A_BusinessPartner.csv
# Run with mocking (no HTTP)
cds watch
# Run with mock service (realistic OData)
cds mock API_BUSINESS_PARTNERQuerying Remote Services
Node.js:
const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
// Simple query
const partners = await bupa.run(
SELECT.from('A_BusinessPartner').limit(100)
)
// With filter
const filtered = await bupa.run(
SELECT.from('A_BusinessPartner')
.where({ BusinessPartnerCategory: '1' })
)Java:
@Autowired
CqnService bupa;
public List<Row> getPartners() {
return bupa.run(Select.from("A_BusinessPartner").limit(100))
.listOf(Row.class);
}Building Mashups
Expose Remote Entities:
using { API_BUSINESS_PARTNER as bupa } from './external/API_BUSINESS_PARTNER';
service MyService {
// Projection on remote entity
entity BusinessPartners as projection on bupa.A_BusinessPartner {
BusinessPartner,
BusinessPartnerFullName,
BusinessPartnerCategory
};
}Associate Local to Remote:
entity Orders {
key ID : UUID;
customer : Association to bupa.A_BusinessPartner;
}Custom Handler for Navigation:
this.on('READ', 'Orders', async (req, next) => {
const orders = await next()
// Fetch customer data from remote
const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
for (const order of orders) {
if (order.customer_BusinessPartner) {
order.customer = await bupa.run(
SELECT.one.from('A_BusinessPartner')
.where({ BusinessPartner: order.customer_BusinessPartner })
)
}
}
return orders
})Destination Configuration
package.json:
{
"cds": {
"requires": {
"API_BUSINESS_PARTNER": {
"kind": "odata-v2",
"[production]": {
"credentials": {
"destination": "S4HANA",
"path": "/sap/opu/odata/sap/API_BUSINESS_PARTNER"
}
}
}
}
}
}Authentication Methods:
| Method | Use Case |
|---|---|
| Basic | Username/password |
| OAuth2ClientCredentials | Machine-to-machine |
| OAuth2UserTokenExchange | User context propagation |
| PrincipalPropagation | SAP Cloud Connector |
---
Cloud Foundry Deployment
Prerequisites
# Install MTA build tool
npm i -g mbt
# Install CF CLI plugin
cf install-plugin multiappsSetup
cds add mta # Generate mta.yaml
cds add xsuaa # Add authentication
cds add hana # Add HANA database
cds add approuter # Add App RouterBuild & Deploy
# Build MTA archive
mbt build
# Deploy to CF
cf deploy mta_archives/*.mtarmta.yaml Structure
_schema-version: "3.1"
ID: bookshop
version: 1.0.0
parameters:
enable-parallel-deployments: true
modules:
# CAP Service Module
- name: bookshop-srv
type: nodejs
path: gen/srv
parameters:
memory: 256M
disk-quota: 1024M
requires:
- name: bookshop-db
- name: bookshop-auth
provides:
- name: srv-api
properties:
srv-url: ${default-url}
# Database Deployer
- name: bookshop-db-deployer
type: hdb
path: gen/db
parameters:
buildpack: nodejs_buildpack
requires:
- name: bookshop-db
# App Router
- name: bookshop-app
type: approuter.nodejs
path: app
parameters:
memory: 256M
requires:
- name: srv-api
group: destinations
properties:
name: srv-api
url: ~{srv-url}
forwardAuthToken: true
- name: bookshop-auth
resources:
# HDI Container
- name: bookshop-db
type: com.sap.xs.hdi-container
parameters:
service: hana
service-plan: hdi-shared
# XSUAA
- name: bookshop-auth
type: org.cloudfoundry.managed-service
parameters:
service: xsuaa
service-plan: application
path: ./xs-security.jsonxs-security.json
{
"xsappname": "bookshop",
"tenant-mode": "dedicated",
"scopes": [
{ "name": "$XSAPPNAME.Admin", "description": "Admin" }
],
"role-templates": [
{
"name": "Admin",
"scope-references": ["$XSAPPNAME.Admin"]
}
]
}---
Kyma/Kubernetes Deployment
Prerequisites
- Kyma-enabled BTP account
- kubectl, helm, pack CLI tools
- Container registry access
- HANA Cloud instance
Setup
cds add hana
cds add xsuaa
cds add kymaDeploy
cds up --to k8s -n <namespace>Helm Chart Structure
chart/
├── Chart.yaml
├── values.yaml
├── values.schema.json
└── templates/values.yaml
global:
imagePullSecret:
name: docker-registry
domain: <cluster-domain>
image:
registry: <registry>
srv:
image:
repository: bookshop-srv
tag: latest
bindings:
db:
serviceInstanceName: bookshop-db
auth:
serviceInstanceName: bookshop-auth
resources:
limits:
memory: 512Mi
cpu: 500m
env:
NODE_ENV: production
db-deployer:
image:
repository: bookshop-db-deployer
bindings:
db:
serviceInstanceName: bookshop-dbService Bindings
# Using service instance
bindings:
db:
serviceInstanceName: bookshop-db
# Using existing secret
bindings:
db:
fromSecret: my-db-secretBackend Destinations
srv:
backendDestinations:
srv-api:
service: srv
ui5:
external: true
url: https://ui5.sap.com---
CI/CD Integration
GitHub Actions
name: Deploy to CF
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Build MTA
run: |
npm i -g mbt
mbt build
- name: Deploy to CF
env:
CF_API: ${{ secrets.CF_API }}
CF_USER: ${{ secrets.CF_USER }}
CF_PASSWORD: ${{ secrets.CF_PASSWORD }}
CF_ORG: ${{ secrets.CF_ORG }}
CF_SPACE: ${{ secrets.CF_SPACE }}
run: |
cf login -a $CF_API -u $CF_USER -p $CF_PASSWORD -o $CF_ORG -s $CF_SPACE
cf deploy mta_archives/*.mtar -fGitLab CI
stages:
- build
- deploy
build:
stage: build
image: node:18
script:
- npm ci
- npm i -g mbt
- mbt build
artifacts:
paths:
- mta_archives/
deploy:
stage: deploy
image: ppiper/cf-cli
script:
- cf login -a $CF_API -u $CF_USER -p $CF_PASSWORD -o $CF_ORG -s $CF_SPACE
- cf deploy mta_archives/*.mtar -f---
Health Checks
Configuration
{
"cds": {
"server": {
"health": {
"path": "/-/health",
"timeout": 5000
}
}
}
}Kubernetes Probes
srv:
health:
liveness:
path: /-/health
initialDelaySeconds: 30
periodSeconds: 10
readiness:
path: /-/health
initialDelaySeconds: 5
periodSeconds: 5---
Multitenancy Deployment
Setup
cds add multitenancy
cds add extensibilitymta.yaml Additions
resources:
- name: bookshop-registry
type: org.cloudfoundry.managed-service
parameters:
service: saas-registry
service-plan: application
config:
xsappname: bookshop
appUrls:
getDependencies: ~{srv-api/srv-url}/-/cds/saas-provisioning/dependencies
onSubscription: ~{srv-api/srv-url}/-/cds/saas-provisioning/tenant/{tenantId}
- name: bookshop-sm
type: org.cloudfoundry.managed-service
parameters:
service: service-manager
service-plan: containerTenant Provisioning
CAP automatically handles:
- Database schema creation per tenant
- Service binding per tenant
- Tenant isolation
---
SAP Event Mesh Integration
Configuration
{
"cds": {
"requires": {
"messaging": {
"[production]": {
"kind": "enterprise-messaging"
}
}
}
}
}mta.yaml
resources:
- name: bookshop-messaging
type: org.cloudfoundry.managed-service
parameters:
service: enterprise-messaging
service-plan: defaultNamespace Prefixing
{
"cds": {
"requires": {
"messaging": {
"publishPrefix": "$namespace/",
"subscribePrefix": "$namespace/"
}
}
}
}---
Hybrid Testing
Test with cloud services locally.
Setup
# Bind to cloud services (creates .cdsrc-private.json with credentials)
cds bind --to bookshop-db
cds bind --to bookshop-auth
# Run with hybrid profile
cds watch --profile hybriddefault-env.json
{
"VCAP_SERVICES": {
"hana": [{
"credentials": {
"host": "...",
"port": "...",
"user": "...",
"password": "..."
}
}]
}
}Add default-env.json to .gitignore.
CDS Query Language (CQL) - Complete Pattern Reference
Source: https://cap.cloud.sap/docs/cds/cql
Table of Contents
- Basic Queries
- Advanced Queries
- Aggregations
- Subqueries
- Joins and Associations
- Data Manipulation
- Transaction Patterns
- Performance Optimization
Basic Queries
Simple SELECT
const { Books } = cds.entities;
// Select all fields
const books = await SELECT.from(Books);
// Select specific fields
const titles = await SELECT.from(Books).columns('title', 'author');
// Select with alias
const bookData = await SELECT.from(Books, b => {
b.title,
b.author_ID.as('author'),
b.price.as('cost')
});WHERE Conditions
// Single condition
const inStock = await SELECT.from(Books)
.where({ stock: { '>': 0 } });
// Multiple conditions (AND)
const cheapBooks = await SELECT.from(Books)
.where({
stock: { '>': 0 },
price: { '<': 20 }
});
// OR conditions
const books = await SELECT.from(Books)
.where({
or: [
{ stock: { '>': 100 } },
{ price: { '<': 10 } }
]
});
// Complex conditions
const results = await SELECT.from(Books)
.where({
and: [
{ stock: { '>': 0 } },
{
or: [
{ price: { '<': 20 } },
{ rating: { '>': 4 } }
]
}
]
});Ordering and Limiting
// Order by single field
const sortedBooks = await SELECT.from(Books)
.orderBy('title')
.limit(10);
// Order by multiple fields
const popularBooks = await SELECT.from(Books)
.orderBy('rating', 'title')
.limit(20);
// Order with direction
const expensiveBooks = await SELECT.from(Books)
.orderBy('price', 'desc')
.limit(5);
// Offset for pagination
const page2 = await SELECT.from(Books)
.orderBy('title')
.limit(10, 20); // Skip 20, take 10Advanced Queries
Functions in SELECT
// Built-in functions
const stats = await SELECT.one.from(Books)
.columns(
'count(ID) as total',
'avg(price) as avgPrice',
'min(price) as minPrice',
'max(price) as maxPrice'
);
// String functions
const bookInfo = await SELECT.from(Books)
.columns(
'upper(title) as upperTitle',
'substring(title, 1, 10) as shortTitle',
'concat(title, " by ", author.name) as fullTitle'
);
// Date functions
const recentBooks = await SELECT.from(Books)
.where({
createdAt: { '>=': '2024-01-01' }
})
.columns(
'year(createdAt) as year',
'month(createdAt) as month',
'day(createdAt) as day'
);CASE Statements
// Conditional values
const categorizedBooks = await SELECT.from(Books)
.columns(
'title',
'price',
{
expr: 'case',
args: [
{ when: { price: { '<': 10 } }, then: 'Cheap' },
{ when: { price: { '<': 20 } }, then: 'Moderate' },
{ else: 'Expensive' }
],
as: 'priceCategory'
}
);
// Complex CASE with calculations
const discountBooks = await SELECT.from(Books)
.columns(
'title',
'price',
'stock',
{
expr: 'case',
args: [
{ when: { stock: { '>': 100 } }, then: 0.1 },
{ when: { stock: { '>': 50 } }, then: 0.05 },
{ else: 0 }
],
as: 'discountRate'
},
{
expr: 'price * (1 - discountRate)',
as: 'finalPrice'
}
);Aggregations
GROUP BY
// Count by category
const booksByGenre = await SELECT.from(Books)
.columns('genre', 'count(ID) as bookCount')
.groupBy('genre')
.orderBy('bookCount', 'desc');
// Multiple grouping columns
const booksByAuthorAndGenre = await SELECT.from(Books)
.columns('author_ID', 'genre', 'count(ID) as count', 'avg(price) as avgPrice')
.groupBy('author_ID', 'genre')
.orderBy('count', 'desc');
// Aggregation with HAVING
const popularGenres = await SELECT.from(Books)
.columns('genre', 'count(ID) as bookCount')
.groupBy('genre')
.having('count(ID)', '>', 10)
.orderBy('bookCount', 'desc');Window Functions
// Row number
const rankedBooks = await SELECT.from(Books)
.columns(
'title',
'price',
{
expr: 'row_number()',
over: { orderBy: 'price', desc: true },
as: 'priceRank'
}
);
// Running totals
const salesData = await SELECT.from('Sales')
.columns(
'date',
'amount',
{
expr: 'sum(amount)',
over: {
orderBy: 'date',
rows: { unbounded_preceding: true }
},
as: 'runningTotal'
}
);
// Partition by
const rankedByGenre = await SELECT.from(Books)
.columns(
'title',
'genre',
'price',
{
expr: 'rank()',
over: {
partitionBy: 'genre',
orderBy: 'price',
desc: true
},
as: 'rankInGenre'
}
);Subqueries
EXISTS Subquery
// Books with reviews
const reviewedBooks = await SELECT.from(Books)
.where({
ID: {
in: SELECT.from('Reviews').columns('book_ID')
}
});
// Using EXISTS
const booksWithOrders = await SELECT.from(Books)
.where(`EXISTS (
SELECT 1 FROM Orders
WHERE Orders.book_ID = Books.ID
)`);Correlated Subquery
// Books with above average price
const aboveAvgPrice = await SELECT.from(Books)
.where({
price: {
'>': SELECT.one('avg(price)').from(Books)
}
});
// Latest review for each book
const booksWithLatestReview = await SELECT.from(Books)
.columns(
'title',
{
subquery: SELECT.one('comment')
.from('Reviews')
.where({ book_ID: 'Books.ID' })
.orderBy('createdAt', 'desc'),
as: 'latestReview'
}
);IN Subquery
// Books from specific authors
const featuredAuthorBooks = await SELECT.from(Books)
.where({
author_ID: {
in: SELECT.from('Authors')
.columns('ID')
.where({ featured: true })
}
});Joins and Associations
Using Associations
const { Books, Authors } = cds.entities;
// Expand association
const booksWithAuthors = await SELECT.from(Books)
.columns('title', 'price')
.columns('author.name', 'author.bio')
.where({ stock: { '>': 0 } });
// Nested expansion
const detailedBooks = await SELECT.from(Books)
.columns('title')
.columns('author.name')
.columns('reviews { rating, comment }')
.where({ genre: 'fiction' });
// Multiple associations
const fullBookInfo = await SELECT.from(Books)
.columns(
'title',
'author.name as authorName',
'publisher.name as publisherName',
'reviews.rating as avgRating'
);Manual JOINs
// Inner join
const innerJoinResult = await SELECT
.from(Books)
.innerJoin(Authors)
.on('Books.author_ID = Authors.ID')
.columns(
'Books.title',
'Authors.name as author'
);
// Left join
const leftJoinResult = await SELECT
.from(Books)
.leftJoin(Reviews)
.on('Books.ID = Reviews.book_ID')
.columns(
'Books.title',
'Reviews.rating'
);
// Multiple joins
const complexJoin = await SELECT
.from(Books)
.innerJoin(Authors)
.on('Books.author_ID = Authors.ID')
.leftJoin(Reviews)
.on('Books.ID = Reviews.book_ID')
.columns(
'Books.title',
'Authors.name',
'avg(Reviews.rating) as avgRating'
)
.groupBy('Books.ID', 'Books.title', 'Authors.name');Data Manipulation
INSERT
// Single record
const newBook = await INSERT.into(Books)
.entries({
title: 'New Book Title',
author_ID: '123',
price: 29.99,
stock: 100
});
// Multiple records
const batchInsert = await INSERT.into(Books).entries([
{ title: 'Book 1', author_ID: '123', price: 19.99, stock: 50 },
{ title: 'Book 2', author_ID: '456', price: 24.99, stock: 30 },
{ title: 'Book 3', author_ID: '123', price: 34.99, stock: 20 }
]);
// Insert from SELECT
const insertFromSelect = await INSERT.into(FeaturedBooks)
.columns('book_ID', 'featuredAt')
.select('ID', 'createdAt')
.from(Books)
.where({ rating: { '>': 4.5 } });UPDATE
// Single record
const updateResult = await UPDATE(Books, '123')
.set({
price: 39.99,
stock: 75
});
// Update with conditions
const bulkUpdate = await UPDATE(Books)
.set({ stock: { '-=': 1 } })
.where({ stock: { '>': 0 } });
// Update based on other table
const updateWithJoin = await UPDATE(Books)
.set({
discount: 0.1
})
.where({
genre: {
in: SELECT.from('Promotions').columns('genre')
}
});
// Conditional update
const conditionalUpdate = await UPDATE(Books)
.set({
price: {
case: [
{ when: { stock: { '>': 100 } }, then: 'price * 0.9' },
{ when: { stock: { '>': 50 } }, then: 'price * 0.95' },
{ else: 'price' }
]
}
});DELETE
// Single record
const deleteResult = await DELETE.from(Books, '123');
// With conditions
const bulkDelete = await DELETE.from(Books)
.where({ stock: 0 });
// Using subquery
const deleteUnpopular = await DELETE.from(Books)
.where({
ID: {
in: SELECT.from(Reviews)
.columns('book_ID')
.groupBy('book_ID')
.having('count(ID)', '<', 5)
}
});UPSERT
// Insert or update
const upsertResult = await UPSERT.into(Books)
.entries({
ID: '123',
title: 'Updated Title',
price: 29.99,
stock: 100
});
// Multiple upserts
const batchUpsert = await UPSERT.into(Books).entries([
{ ID: '123', title: 'Book 1', price: 19.99, stock: 50 },
{ ID: '456', title: 'Book 2', price: 24.99, stock: 30 }
]);Transaction Patterns
Atomic Operations
// Automatic transaction in handler
this.on('placeOrder', async (req) => {
const { bookId, quantity } = req.data;
// All operations in one transaction
const book = await SELECT.one.from(Books, bookId);
if (book.stock < quantity) {
req.error(409, 'Insufficient stock');
}
// Update stock
await UPDATE(Books, bookId)
.set({ stock: { '-=': quantity } });
// Create order
const order = await INSERT.into(Orders).entries({
book_ID: bookId,
quantity,
total: book.price * quantity,
status: 'confirmed'
});
return { orderId: order.ID, status: 'confirmed' };
});Manual Transaction
// Explicit transaction control
await cds.tx(async (tx) => {
// Multiple operations in transaction
await tx.run(
UPDATE(Books, bookId).set({ stock: { '-=': quantity } }),
INSERT.into(Orders).entries(orderData),
INSERT.into(AuditLog).entries(logData)
);
// Transaction commits automatically
});Transaction with Error Handling
// Transaction with rollback on error
await cds.tx(async (tx) => {
try {
await tx.run(UPDATE(Books, bookId).set({ stock: { '-=': quantity } }));
if (someCondition) {
throw new Error('Condition failed');
}
await tx.run(INSERT.into(Orders).entries(orderData));
} catch (error) {
// Transaction automatically rolls back
req.error(400, 'Order failed: ' + error.message);
}
});Performance Optimization
Query Optimization
// Select only needed columns
const optimizedQuery = await SELECT.from(Books)
.columns('ID', 'title', 'price') // Instead of *
.where({ stock: { '>': 0 } })
.limit(100);
// Use indexed fields in WHERE
const indexedQuery = await SELECT.from(Books)
.where({ author_ID: authorId }) // Indexed field first
.and({ price: { '<': 50 } });
// Use EXISTS instead of IN for large datasets
const existsQuery = await SELECT.from(Books)
.where(`EXISTS (
SELECT 1 FROM Reviews
WHERE Reviews.book_ID = Books.ID
AND Reviews.rating > 4
)`);Batch Processing
// Process in batches
const allBooks = await SELECT.from(Books);
const batchSize = 100;
for (let i = 0; i < allBooks.length; i += batchSize) {
const batch = allBooks.slice(i, i + batchSize);
await Promise.all(
batch.map(book =>
UPDATE(Books, book.ID)
.set({ lastProcessed: new Date() })
)
);
}Prepared Statements (CAP handles automatically)
// CAP automatically parameterizes queries to prevent SQL injection
const safeQuery = await SELECT.from(Books)
.where({
title: userProvidedTitle, // Automatically parameterized
price: { '>': userProvidedPrice }
});Best Practices
Query Writing
- Be Specific: Select only columns you need
- Use Indexes: Query on indexed fields
- Limit Results: Use LIMIT/OFFSET for pagination
- Avoid SELECT *: Explicitly list columns
Performance
- Batch Operations: Use bulk inserts/updates when possible
- Appropriate JOINs: Use associations over manual joins
- Filter Early: Apply WHERE conditions before joins
- Use EXISTS: For existence checks, EXISTS is often faster than IN
Safety
- Parameterized Queries: Let CAP handle parameterization
- Validate Inputs: Always validate user input
- Transaction Boundaries: Keep transactions short
- Error Handling: Handle errors appropriately
Last Updated: 2025-11-27 CAP Version: 9.4.x Documentation: https://cap.cloud.sap/docs/cds/cql
CDS Query Language (CQL) Complete Reference
Source: https://cap.cloud.sap/docs/cds/cql
SELECT Queries
Basic SELECT
const { Books, Authors } = cds.entities;
// All columns
const books = await SELECT.from(Books);
// Specific columns
const titles = await SELECT.from(Books).columns('ID', 'title', 'price');
// With aliases
const data = await SELECT.from(Books).columns('ID', 'title as bookTitle');Single Record
// By key - shortcut form (returns single entity or undefined)
const book = await SELECT.from(Books, bookId);
// Explicit form with WHERE clause
const book = await SELECT.one.from(Books).where({ ID: bookId });
// With non-key condition
const book = await SELECT.one.from(Books).where({ isbn: '978-123' });
// IMPORTANT: Always guard the result - returns undefined when no record found
const book = await SELECT.one.from(Books).where({ ID: bookId });
if (!book) {
return req.reject(404, 'Book not found');
}
// Now safe to access book.title, book.price, etc.WHERE Clauses
// Simple equality
await SELECT.from(Books).where({ stock: 10 });
// Comparison operators
await SELECT.from(Books).where({ stock: { '>': 0 } });
await SELECT.from(Books).where({ stock: { '>=': 10 } });
await SELECT.from(Books).where({ stock: { '<': 100 } });
await SELECT.from(Books).where({ price: { '<=': 50 } });
await SELECT.from(Books).where({ status: { '!=': 'deleted' } });
// IN operator
await SELECT.from(Books).where({ genre: { in: ['fiction', 'drama'] } });
// LIKE operator
await SELECT.from(Books).where({ title: { like: '%CAP%' } });
// BETWEEN
await SELECT.from(Books).where({ price: { between: [10, 50] } });
// IS NULL / IS NOT NULL
await SELECT.from(Books).where({ author_ID: { '=': null } });
await SELECT.from(Books).where({ author_ID: { '!=': null } });
// Multiple conditions (AND)
await SELECT.from(Books).where({
stock: { '>': 0 },
price: { '<': 100 }
});
// OR conditions
await SELECT.from(Books).where({
or: [
{ stock: { '>': 100 } },
{ price: { '<': 10 } }
]
});
// Complex nested conditions
await SELECT.from(Books).where({
and: [
{ genre: 'fiction' },
{ or: [
{ stock: { '>': 50 } },
{ featured: true }
]}
]
});ORDER BY
// Single column
await SELECT.from(Books).orderBy('title');
await SELECT.from(Books).orderBy('title asc');
await SELECT.from(Books).orderBy('price desc');
// Multiple columns
await SELECT.from(Books).orderBy('genre', 'title');
await SELECT.from(Books).orderBy({ genre: 'asc', price: 'desc' });LIMIT & OFFSET
// Limit results
await SELECT.from(Books).limit(10);
// With offset (pagination)
await SELECT.from(Books).limit(10, 20); // 10 rows, skip 20
await SELECT.from(Books).limit({ rows: 10, offset: 20 });GROUP BY & HAVING
await SELECT.from(Books)
.columns('genre', 'count(*) as count', 'avg(price) as avgPrice')
.groupBy('genre')
.having({ 'count(*)': { '>': 5 } });DISTINCT
await SELECT.distinct.from(Books).columns('genre');Associations & Expands
Path Expressions
// Navigate associations in columns
await SELECT.from(Books).columns('title', 'author.name');
// Navigate in where
await SELECT.from(Books).where({ 'author.name': 'John Doe' });Expand (Deep Read)
Using columns with expand:
// Expand syntax with columns array
await SELECT.from(Books)
.columns('ID', 'title', { ref: ['author'], expand: ['ID', 'name'] });
// Using '*' with expand
await SELECT.from(Books)
.columns('*', { ref: ['author'], expand: ['*'] });Fluent lambda syntax:
// Fluent syntax
await SELECT.from(Books, b => {
b.ID,
b.title,
b.author(a => {
a.ID,
a.name
})
});
// Nested expand
await SELECT.from(Orders, o => {
o.ID,
o.customer(c => { c.name }),
o.items(i => {
i.quantity,
i.product(p => { p.name, p.price })
})
});Infix Filters
// Filter associated entities
await SELECT.from(Authors, a => {
a.name,
a.books(b => {
b.title
}).where({ stock: { '>': 0 } })
});INSERT
Single Entry
await INSERT.into(Books).entries({
ID: cds.utils.uuid(),
title: 'New Book',
stock: 100,
price: 29.99
});Multiple Entries
await INSERT.into(Books).entries([
{ title: 'Book 1', stock: 10 },
{ title: 'Book 2', stock: 20 },
{ title: 'Book 3', stock: 30 }
]);Insert from SELECT
await INSERT.into(ArchivedBooks).as(
SELECT.from(Books).where({ stock: 0 })
);Deep Insert
await INSERT.into(Orders).entries({
ID: orderId,
customer_ID: customerId,
items: [
{ product_ID: product1Id, quantity: 2 },
{ product_ID: product2Id, quantity: 1 }
]
});UPDATE
By Key
await UPDATE(Books, bookId).set({ stock: 50 });
await UPDATE(Books, bookId).with({ stock: 50 });With WHERE
await UPDATE(Books)
.set({ featured: true })
.where({ stock: { '>': 100 } });Increment/Decrement
// Increment
await UPDATE(Books, bookId).set({ stock: { '+=': 10 } });
// Decrement
await UPDATE(Books, bookId).set({ stock: { '-=': 5 } });
// Multiply
await UPDATE(Books, bookId).set({ price: { '*=': 1.1 } });Conditional Update
// Update only if condition met
const result = await UPDATE(Books, bookId)
.set({ stock: { '-=': quantity } })
.where({ stock: { '>=': quantity } });
if (result === 0) {
throw new Error('Insufficient stock');
}Deep Update
await UPDATE(Orders, orderId).with({
status: 'confirmed',
items: [
{ ID: item1Id, quantity: 5 },
{ ID: item2Id, quantity: 3 }
]
});DELETE
By Key
await DELETE.from(Books, bookId);With WHERE
await DELETE.from(Books).where({ stock: 0 });Delete All
await DELETE.from(Books); // Caution: deletes all!Cascading Delete
Compositions automatically cascade deletes to child entities.
UPSERT
Behavior: UPSERT performs INSERT if record with key doesn't exist, otherwise UPDATE. Key fields are required to determine existence.
// Insert if not exists, update if exists
// IMPORTANT: Key field (ID) MUST be provided
await UPSERT.into(Books).entries({
ID: bookId, // Key field required
title: 'Updated or New Title',
stock: 50
});
// Multiple entries
await UPSERT.into(Books).entries([
{ ID: id1, title: 'Book 1' },
{ ID: id2, title: 'Book 2' }
]);
// UPSERT does NOT support .where() - use UPDATE for conditional updates
// UPSERT does NOT merge with existing data - all provided fields are setRaw SQL
Native Queries
// Direct SQL execution
const results = await cds.db.run(
`SELECT * FROM my_bookshop_Books WHERE stock > ?`,
[10]
);Prepared Statements
const query = `SELECT * FROM Books WHERE genre = ? AND stock > ?`;
const results = await cds.db.run(query, ['fiction', 0]);Query Building
CQN Objects
// Build query as object
const query = {
SELECT: {
from: { ref: ['Books'] },
columns: [
{ ref: ['ID'] },
{ ref: ['title'] }
],
where: [
{ ref: ['stock'] }, '>', { val: 0 }
]
}
};
const results = await cds.db.run(query);Query Modification
// Clone and modify
let query = SELECT.from(Books);
query = query.where({ stock: { '>': 0 } });
query = query.orderBy('title');
query = query.limit(10);
const results = await cds.db.run(query);Aggregations
Count
const count = await SELECT.one.from(Books)
.columns('count(*) as total');Sum, Avg, Min, Max
const stats = await SELECT.one.from(Books).columns(
'sum(stock) as totalStock',
'avg(price) as avgPrice',
'min(price) as minPrice',
'max(price) as maxPrice'
);Group By with Aggregates
const byGenre = await SELECT.from(Books)
.columns('genre', 'count(*) as count', 'avg(price) as avgPrice')
.groupBy('genre')
.orderBy('count desc');Subqueries
In WHERE
// Books by authors with more than 5 books
await SELECT.from(Books).where({
author_ID: {
in: SELECT.from(Authors)
.columns('ID')
.where({ bookCount: { '>': 5 } })
}
});EXISTS
// Authors with at least one bestseller
await SELECT.from(Authors).where({
exists: SELECT.from(Books)
.where({ 'author_ID': { ref: ['Authors', 'ID'] }, bestseller: true })
});Locking
Exclusive Lock
// For update (exclusive)
const book = await SELECT.from(Books, bookId).forUpdate();Shared Lock
// For share (shared)
const book = await SELECT.from(Books, bookId).forShareLock();Parameterized Views
// View with parameters
const filtered = await SELECT.from('FilteredBooks', {
minStock: 10,
maxPrice: 50
});Exists Predicate
// Check existence
const hasBooks = await SELECT.one.from(Authors)
.where({
ID: authorId,
exists: SELECT.from(Books).where({ author_ID: authorId })
});CDS Schema & Query Notations Reference
Source: https://cap.cloud.sap/docs/cds/csn, https://cap.cloud.sap/docs/cds/cqn, https://cap.cloud.sap/docs/cds/cxn
CSN - Core Schema Notation
CSN is a compact notation for representing CDS models as plain JavaScript objects.
Root-Level Structure
{
requires: ['@sap/cds/common', './db/schema'], // Imported models
definitions: { /* Named definitions */ }, // Types, entities, services
extensions: [ /* Unapplied aspects */ ], // Extensions array
i18n: { 'de': {}, 'fr': {} } // Translations
}Definition Kinds
| Kind | Description |
|---|---|
context | Namespace container |
service | Service definition |
entity | Entity definition |
type | Type definition |
action | Unbound action |
function | Unbound function |
annotation | Annotation definition |
Type Definitions
// Scalar types
{ type: "cds.String", length: 100 }
{ type: "cds.Decimal", precision: 11, scale: 3 }
{ type: "cds.Integer" }
{ type: "cds.UUID" }
{ type: "cds.Boolean" }
{ type: "cds.Date" }
{ type: "cds.DateTime" }
{ type: "cds.Timestamp" }
// Structured types
{ elements: {
foo: { type: "cds.Integer" },
bar: { type: "cds.String" }
}}
// Array types
{ items: { type: "cds.Integer" } }
// Enumeration types
{ enum: {
asc: {},
desc: {},
custom: { val: 'custom-value' }
}}Entity Definition
{
kind: "entity",
elements: {
ID: { type: "cds.UUID", key: true },
name: { type: "cds.String", notNull: true },
virtual_field: { type: "cds.String", virtual: true }
}
}Association Definition
{
type: "cds.Association",
target: "Books",
cardinality: { src: 1, min: 0, max: "*" },
on: [{ ref: ["author_ID"] }, "=", { ref: ["author", "ID"] }],
keys: [{ ref: ["ID"] }]
}Views with Query
{
kind: "entity",
query: { SELECT: { from: { ref: ["Books"] }, columns: ["*"] } },
// or simpler:
projection: { from: { ref: ["Books"] }, columns: ["*"] }
}Annotations
{
'@title': "Display Name",
'@readonly': true,
'@UI.LineItem': [{ Value: { '=': 'name' } }]
}Extensions
{
extensions: [
{ extend: "Books", includes: ["managed"] },
{ extend: "Books", elements: { newField: { type: "cds.String" } } },
{ annotate: "Books", '@readonly': true }
]
}---
CQN - Core Query Notation
CQN represents CDS queries as plain JavaScript objects.
Three Ways to Create Queries
// 1. CQL parsing
let query = cds.ql`SELECT from Books`
// 2. Query builder
let query = SELECT.from('Books')
// 3. Direct CQN object
let query = { SELECT: { from: { ref: ['Books'] } } }
// Execute
let results = await cds.run(query)SELECT Structure
{
SELECT: {
distinct: true, // Optional: distinct results
count: true, // Optional: adds $count to result (OData-style)
one: true, // Optional: returns single object or undefined (not array)
from: source, // Required: source table/view
columns: column[], // Optional: projection
where: expr[], // Optional: filter
having: expr[], // Optional: group filter
groupBy: expr[], // Optional: grouping
orderBy: order[], // Optional: sorting
limit: { rows, offset } // Optional: pagination
}
}Result Behavior:
- Default: Returns array of records
[{...}, {...}] one: true: Returns single object{...}orundefinedif no match (not array)count: true: Returns records with$countproperty indicating total matches- Always check for
undefined/empty results before accessing properties
Source Types
// Simple reference
{ ref: ['Books'] }
// Aliased
{ ref: ['Books'], as: 'b' }
// JOIN
{
join: 'left', // 'inner' | 'left' | 'right'
args: [source1, source2],
on: expr
}
// Subquery
{ SELECT: { from: { ref: ['Books'] } } }Column Expressions
// All columns
'*'
// Simple reference
{ ref: ['title'] }
// Aliased
{ ref: ['title'], as: 'bookTitle' }
// With expand (to-many)
{ ref: ['chapters'], expand: ['*'] }
// With inline (flattened)
{ ref: ['author'], inline: ['name', 'country'] }
// Function call
{ func: 'count', args: [{ ref: ['ID'] }], as: 'total' }WHERE Expressions
// Simple comparison
[{ ref: ['price'] }, '>', { val: 100 }]
// AND/OR
[{ ref: ['price'] }, '>', { val: 100 }, 'and', { ref: ['stock'] }, '>', { val: 0 }]
// IN list
[{ ref: ['status'] }, 'in', { list: [{ val: 'active' }, { val: 'pending' }] }]
// LIKE
[{ ref: ['name'] }, 'like', { val: '%CAP%' }]
// EXISTS subquery
['exists', { SELECT: { from: { ref: ['Orders'] }, where: [...] } }]ORDER BY
{
orderBy: [
{ ref: ['price'], sort: 'desc', nulls: 'last' },
{ ref: ['name'], sort: 'asc' }
]
}INSERT
// With entries (name-value pairs)
{
INSERT: {
into: { ref: ['Books'] },
entries: [
{ ID: 201, title: 'Book 1' },
{ ID: 202, title: 'Book 2' }
]
}
}
// With columns and values (single row)
{
INSERT: {
into: { ref: ['Books'] },
columns: ['ID', 'title'],
values: [201, 'Book 1']
}
}
// With rows (multiple records)
{
INSERT: {
into: { ref: ['Books'] },
columns: ['ID', 'title'],
rows: [
[201, 'Book 1'],
[202, 'Book 2']
]
}
}
// Deep insert
{
INSERT: {
into: { ref: ['Authors'] },
entries: [{
ID: 1, name: 'Author',
books: [
{ ID: 101, title: 'Book 1' },
{ ID: 102, title: 'Book 2' }
]
}]
}
}UPDATE
{
UPDATE: {
entity: { ref: ['Books'] },
where: [{ ref: ['ID'] }, '=', { val: 201 }],
data: { stock: 100 }, // Scalar values only
with: { stock: { xpr: [{ ref: ['stock'] }, '+', { val: 1 }] } } // Expressions allowed
}
}DELETE
{
DELETE: {
from: { ref: ['Books'] },
where: [{ ref: ['ID'] }, '=', { val: 201 }]
}
}UPSERT
{
UPSERT: {
into: { ref: ['Books'] },
entries: [{ ID: 201, title: 'Updated Title', stock: 50 }]
}
}---
CXN - Core Expression Notation
CXN defines the structure of expressions within CQN.
Expression Types
// Literal value
{ val: 'string' }
{ val: 123 }
{ val: true }
{ val: null }
// Special literals
{ val: "2024-01-15", literal: 'date' }
{ val: "13:05:23", literal: 'time' }
{ val: "2024-01-15T13:05:23Z", literal: 'timestamp' }
// Reference
{ ref: ['field'] }
{ ref: ['entity', 'field'] }
// Complex reference with filter
{ ref: [{ id: 'Books', where: [...] }] }
// Function call
{ func: 'sum', args: [{ ref: ['amount'] }] }
{ func: 'concat', args: [{ ref: ['first'] }, { val: ' ' }, { ref: ['last'] }] }
// Expression sequence
{ xpr: [{ ref: ['a'] }, '+', { ref: ['b'] }] }
// List/tuple
{ list: [{ val: 1 }, { val: 2 }, { val: 3 }] }
// Binding parameter
{ ref: ['?'], param: true }
{ ref: ['name'], param: true }
// Subquery
{ SELECT: { from: { ref: ['Books'] } } }Operators
// Comparison
'=' | '==' | '!=' | '<' | '<=' | '>' | '>='
// Logical
'and' | 'or' | 'not'
// Pattern matching
'like' | 'in' | 'between'
// Null check
'is null' | 'is not null'
// Existence
'exists'Window Functions
{
func: 'rank',
args: [],
xpr: ['over', {
partitionBy: [{ ref: ['category'] }],
orderBy: [{ ref: ['price'], sort: 'desc' }]
}]
}CASE Expression
{
xpr: [
'case',
'when', { xpr: [{ ref: ['status'] }, '=', { val: 'A' }] },
'then', { val: 'Active' },
'when', { xpr: [{ ref: ['status'] }, '=', { val: 'I' }] },
'then', { val: 'Inactive' },
'else', { val: 'Unknown' },
'end'
]
}---
Common CDS Built-in Functions
| Function | Description |
|---|---|
concat(a, b) | String concatenation |
contains(str, sub) | String contains check |
substring(str, start, len) | Extract substring |
length(str) | String length |
tolower(str) | Lowercase conversion |
toupper(str) | Uppercase conversion |
trim(str) | Remove whitespace |
year(date) | Extract year |
month(date) | Extract month |
day(date) | Extract day |
now() | Current timestamp |
sum(field) | Aggregate sum |
avg(field) | Aggregate average |
min(field) | Aggregate minimum |
max(field) | Aggregate maximum |
count(field) | Count records |
countdistinct(field) | Count distinct |
Data Privacy & Security Reference
Source: https://cap.cloud.sap/docs/guides/security/, https://cap.cloud.sap/docs/guides/data-privacy/
Security Overview
CAP applications require security configuration for:
- Authentication (who is the user?)
- Authorization (what can they access?)
- Data protection (how is data secured?)
---
Authentication
XSUAA Integration
SAP Authorization and Trust Management Service (XSUAA).
// package.json
{
"cds": {
"requires": {
"auth": {
"[production]": { "kind": "xsuaa" }
}
}
}
}xs-security.json
{
"xsappname": "bookshop",
"tenant-mode": "dedicated",
"scopes": [
{ "name": "$XSAPPNAME.Admin", "description": "Admin access" },
{ "name": "$XSAPPNAME.User", "description": "User access" }
],
"role-templates": [
{
"name": "Admin",
"description": "Admin role",
"scope-references": ["$XSAPPNAME.Admin"]
},
{
"name": "User",
"description": "User role",
"scope-references": ["$XSAPPNAME.User"]
}
]
}IAS Integration
SAP Cloud Identity Services.
{
"cds": {
"requires": {
"auth": {
"[production]": { "kind": "ias" }
}
}
}
}Mock Authentication (Development)
{
"cds": {
"requires": {
"auth": {
"[development]": { "kind": "mocked", "users": {
"alice": { "roles": ["Admin"] },
"bob": { "roles": ["User"] }
}}
}
}
}
}---
Authorization
@requires Annotation
Restrict service/entity access to users with specific roles.
@requires: 'Admin'
service AdminService { ... }
@requires: ['Admin', 'Manager']
entity SensitiveData { ... }
// Pseudo roles
@requires: 'authenticated-user' // Any authenticated user
@requires: 'system-user' // Technical users only@restrict Annotation
Fine-grained access control.
entity Orders @(restrict: [
{ grant: 'READ', to: 'User' },
{ grant: ['READ', 'WRITE'], to: 'Admin' },
{ grant: 'READ', where: 'buyer = $user' }
]) { ... }Grant Operations
| Operation | Description |
|---|---|
READ | SELECT queries |
WRITE | INSERT, UPDATE, DELETE |
CREATE | INSERT only |
UPDATE | UPDATE only |
DELETE | DELETE only |
* | All operations |
Where Conditions
// User-specific data
{ grant: 'READ', where: 'createdBy = $user' }
// Attribute-based
{ grant: 'READ', where: 'country = $user.country' }
// Exists check
{ grant: 'READ', where: 'exists assignedUsers[user = $user]' }Pseudo Variables
| Variable | Description |
|---|---|
$user | Current user ID |
$user.<attr> | User attribute |
$now | Current timestamp |
$tenant | Current tenant |
Instance-Based Authorization
entity Projects @(restrict: [
{ grant: '*', to: 'Admin' },
{ grant: 'READ', where: 'members.user = $user' },
{ grant: 'WRITE', where: 'owner = $user' }
]) {
key ID: UUID;
owner: User;
members: Association to many ProjectMembers;
}---
Data Privacy
Overview
CAP supports GDPR compliance through:
- Right of Access (Personal Data Management)
- Right to be Forgotten (Data Retention Management)
- Transparency (Audit Logging)
@PersonalData Annotations
Entity Semantics
// Data Subject - identifies a person
annotate Customers with @PersonalData: {
EntitySemantics: 'DataSubject',
DataSubjectRole: 'Customer'
};
// Data Subject Details - info about person, doesn't identify alone
annotate Addresses with @PersonalData: {
EntitySemantics: 'DataSubjectDetails'
};
// Other - related data with personal info
annotate Orders with @PersonalData: {
EntitySemantics: 'Other'
};Field Annotations
annotate Customers with {
// Required: identifies the data subject
ID @PersonalData.FieldSemantics: 'DataSubjectID';
// Personal information
firstName @PersonalData.IsPotentiallyPersonal;
lastName @PersonalData.IsPotentiallyPersonal;
email @PersonalData.IsPotentiallyPersonal;
phone @PersonalData.IsPotentiallyPersonal;
dateOfBirth @PersonalData.IsPotentiallyPersonal;
// Sensitive information (triggers access audits)
creditCard @PersonalData.IsPotentiallySensitive;
healthInfo @PersonalData.IsPotentiallySensitive;
}Complete Example
using { cuid, managed } from '@sap/cds/common';
entity Customers : cuid, managed {
firstName : String(100);
lastName : String(100);
email : String(255);
phone : String(50);
dateOfBirth : Date;
addresses : Composition of many Addresses on addresses.customer = $self;
}
entity Addresses : cuid {
customer : Association to Customers;
street : String(200);
city : String(100);
country : String(3);
zip : String(20);
}
// Privacy annotations
annotate Customers with @PersonalData: {
EntitySemantics: 'DataSubject',
DataSubjectRole: 'Customer'
} {
ID @PersonalData.FieldSemantics: 'DataSubjectID';
firstName @PersonalData.IsPotentiallyPersonal;
lastName @PersonalData.IsPotentiallyPersonal;
email @PersonalData.IsPotentiallyPersonal;
phone @PersonalData.IsPotentiallyPersonal;
dateOfBirth @PersonalData.IsPotentiallyPersonal;
}
annotate Addresses with @PersonalData: {
EntitySemantics: 'DataSubjectDetails'
} {
customer @PersonalData.FieldSemantics: 'DataSubjectID';
street @PersonalData.IsPotentiallyPersonal;
city @PersonalData.IsPotentiallyPersonal;
zip @PersonalData.IsPotentiallyPersonal;
}---
Audit Logging
Setup
npm add @cap-js/audit-loggingConfiguration
{
"cds": {
"requires": {
"audit-log": {
"[production]": { "kind": "audit-log-service" },
"[development]": { "kind": "console" }
}
}
}
}Automatic Logging
When entities are annotated with @PersonalData, CAP automatically logs:
- Read access to personal data
- Modifications to personal data
- Access to sensitive data
Programmatic Audit Logging
const audit = await cds.connect.to('audit-log')
// Log data access
await audit.log('dataAccess', {
dataSubject: { type: 'Customer', id: { ID: customerId } },
dataObject: { type: 'Customer' },
attributes: ['firstName', 'lastName', 'email']
})
// Log data modification
await audit.log('dataModification', {
dataSubject: { type: 'Customer', id: { ID: customerId } },
dataObject: { type: 'Customer' },
attributes: [
{ name: 'email', old: 'old@email.com', new: 'new@email.com' }
]
})---
Security Best Practices
Service Exposure
// Only expose what's needed
service CatalogService {
@readonly entity Books as projection on db.Books {
ID, title, author, price // Exclude internal fields
};
}
// Separate admin service
@requires: 'Admin'
service AdminService {
entity Books as projection on db.Books;
}Input Validation
this.before('CREATE', 'Orders', req => {
const { quantity, price } = req.data
if (quantity <= 0) req.reject(400, 'Quantity must be positive')
if (price < 0) req.reject(400, 'Price cannot be negative')
})SQL Injection Prevention
CAP automatically protects against SQL injection when using CQL.
// Safe - parameterized
const books = await SELECT.from(Books).where({ author_ID: authorId })
// Safe - tagged template
const books = await SELECT.from(Books).where`author_ID = ${authorId}`
// Avoid string concatenation
// DON'T: `SELECT * FROM Books WHERE author_ID = '${authorId}'`CORS Configuration
{
"cds": {
"server": {
"cors": {
"origin": "https://myapp.example.com",
"methods": ["GET", "POST", "PUT", "DELETE"],
"credentials": true
}
}
}
}Helmet Integration (Node.js)
// server.js
const helmet = require('helmet')
cds.on('bootstrap', app => {
app.use(helmet())
})---
Multitenancy Security
Tenant Isolation
CAP automatically isolates tenant data:
- Each tenant has separate database schema/container
- JWT tokens contain tenant information
- Authorization is tenant-aware
Cross-Tenant Access Prevention
// Automatic tenant filtering
entity TenantData @cds.autoexpose {
key ID: UUID;
// tenant column added automatically
}Tenant-Aware Services
this.on('READ', 'Data', async req => {
// req.tenant contains current tenant ID
const tenant = req.tenant
// CAP automatically filters by tenant
})---
Consuming Services Security
Destination Configuration
{
"cds": {
"requires": {
"API_BUSINESS_PARTNER": {
"kind": "odata-v2",
"credentials": {
"destination": "S4HANA",
"path": "/sap/opu/odata/sap/API_BUSINESS_PARTNER"
}
}
}
}
}Authentication Methods
| Method | Description |
|---|---|
| Basic | Username/password |
| OAuth2ClientCredentials | M2M authentication |
| OAuth2UserTokenExchange | User token forwarding |
| OAuth2JWTBearer | JWT bearer assertion |
| PrincipalPropagation | SAP principal propagation |
Token Forwarding
{
"credentials": {
"destination": "S4HANA",
"forwardAuthToken": true
}
}CAP Database Configuration Reference
Source: https://cap.cloud.sap/docs/guides/databases
Supported Databases
| Database | Node.js Package | Java | Use Case |
|---|---|---|---|
| SQLite | @cap-js/sqlite | Yes | Development, testing |
| SAP HANA | @cap-js/hana | Yes | Production |
| PostgreSQL | @cap-js/postgres | Yes | Production |
| H2 | N/A | Yes | Java development |
SQLite (Development)
Installation
npm add @cap-js/sqlite -DConfiguration
{
"cds": {
"requires": {
"db": {
"kind": "sqlite",
"credentials": { "url": ":memory:" }
}
}
}
}File-based SQLite
{
"cds": {
"requires": {
"db": {
"kind": "sqlite",
"credentials": { "url": "db.sqlite" }
}
}
}
}Deploy Schema
cds deploy --to sqlite:db.sqlite---
SAP HANA (Production)
Installation
npm add @cap-js/hana
cds add hanaConfiguration
{
"cds": {
"requires": {
"db": {
"[production]": {
"kind": "hana",
"deploy-format": "hdbtable"
}
}
}
}
}Hybrid Development
# Add HANA for hybrid mode
cds add hana --for hybrid
# Deploy to HANA Cloud
cds deploy --to hana
# Run locally with remote HANA
cds watch --profile hybridBuild for HANA
cds build --for hanaGenerates in gen/db/:
.hdbtablefiles.hdbviewfiles.hdbtabledatafiles (CSV data).hdiconfigand.hdinamespace
HANA-Specific Features
Vector Embeddings:
entity Documents {
key ID : UUID;
content : String;
embedding : Vector(1536); // For AI embeddings
}Geospatial:
entity Locations {
key ID : UUID;
point : hana.ST_POINT;
area : hana.ST_GEOMETRY;
}Schema Evolution
CAP handles compatible changes automatically. For incompatible changes:
@cds.persistence.journal
entity LargeTable {
key ID : UUID;
data : String;
}Generates .hdbmigrationtable for manual migration control.
Native SQL
@sql.append: 'PARTITION BY HASH (ID) PARTITIONS 4'
entity PartitionedData { ... }
@sql.prepend: 'COLUMN TABLE'
entity ColumnTable { ... }---
PostgreSQL
Installation
npm add @cap-js/postgres
cds add postgresConfiguration
{
"cds": {
"requires": {
"db": {
"kind": "postgres",
"credentials": {
"host": "localhost",
"port": 5432,
"database": "mydb",
"user": "postgres",
"password": "password"
}
}
}
}
}Docker Development
docker run -d --name postgres \
-e POSTGRES_PASSWORD=postgres \
-p 5432:5432 \
postgres:15Deploy Schema
cds deploy --to postgres---
H2 (Java Development)
pom.xml
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>Configuration
# application.yaml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver---
Database-Agnostic Queries
All databases support standard CQL operations:
// Works on all databases
await SELECT.from(Books).where({ stock: { '>': 0 } });
await INSERT.into(Books).entries({ title: 'New Book' });
await UPDATE(Books, id).set({ stock: 50 });
await DELETE.from(Books, id);Standard Functions
| Function | Description |
|---|---|
concat(x, y) | String concatenation |
contains(x, y) | Substring check |
startswith(x, y) | Prefix check |
endswith(x, y) | Suffix check |
tolower(x) | Lowercase |
toupper(x) | Uppercase |
trim(x) | Remove whitespace |
length(x) | String length |
substring(x, i, n) | Extract substring |
Session Variables
// Available across all databases
cds.context.user; // Current user
cds.context.tenant; // Current tenant
cds.context.locale; // User locale
cds.context.timestamp; // Request timestamp---
Initial Data (CSV)
File Location
db/
├── schema.cds
└── data/
├── my.bookshop-Books.csv
├── my.bookshop-Authors.csv
└── sap.common-Countries.csvNaming Convention
<namespace>-<EntityName>.csv or <namespace>.<EntityName>.csv
Format
ID;title;author_ID;stock;price
1;Wuthering Heights;101;100;12.99
2;Jane Eyre;102;50;10.99Test Data
Place in test/data/ for development-only data:
test/
└── data/
├── my.bookshop-Books.csv
└── my.bookshop-Reviews.csv---
Connection Pooling
Node.js
{
"cds": {
"requires": {
"db": {
"pool": {
"min": 0,
"max": 10,
"acquireTimeoutMillis": 10000,
"idleTimeoutMillis": 30000
}
}
}
}
}Java
spring:
datasource:
hikari:
minimum-idle: 5
maximum-pool-size: 20
idle-timeout: 30000---
Constraints
Not Null
entity Books {
title : String(100) not null;
}Unique
@assert.unique: { isbn: [isbn] }
entity Books {
isbn : String(13);
}Foreign Keys
{
"cds": {
"features": {
"assert_integrity": "db"
}
}
}---
Native Queries
Node.js
const result = await cds.db.run(
`SELECT * FROM my_bookshop_Books WHERE stock > ?`,
[10]
);Java
@Autowired
JdbcTemplate jdbc;
List<Map<String, Object>> result = jdbc.queryForList(
"SELECT * FROM my_bookshop_Books WHERE stock > ?",
10
);---
Profile-Based Configuration
{
"cds": {
"requires": {
"db": {
"[development]": {
"kind": "sqlite",
"credentials": { "url": ":memory:" }
},
"[hybrid]": {
"kind": "hana"
},
"[production]": {
"kind": "hana"
}
}
}
}
}Activate Profile
# Development (default)
cds watch
# Hybrid
cds watch --profile hybrid
# Production
NODE_ENV=production cds serveCloud Foundry Deployment Reference
Source: https://cap.cloud.sap/docs/guides/deployment/to-cf
Prerequisites
Required Tools
# Install CAP development kit
npm i -g @sap/cds-dk
# Install Cloud MTA Build Tool
npm i -g mbt
# Cloud Foundry CLI
# Download from: https://github.com/cloudfoundry/cli/releases
# Install CF plugins
cf add-plugin-repo CF-Community https://plugins.cloudfoundry.org
cf install-plugin -f multiapps
cf install-plugin -f html5-pluginBTP Requirements
- SAP BTP account with Cloud Foundry environment
- SAP HANA Cloud instance (provisioned and running)
- Space developer role in target space
Project Preparation
Add Production Capabilities
# Add SAP HANA support
cds add hana
# Add authentication
cds add xsuaa
# Add deployment descriptor
cds add mta
# Add application router (optional)
cds add approuter
# All at once
cds add hana,xsuaa,mta,approuterGenerated Files
After cds add hana,xsuaa,mta:
project/
├── db/
│ └── src/ # HANA artifacts
├── mta.yaml # Deployment descriptor
├── xs-security.json # XSUAA config
└── package.json # Updated with HANA configmta.yaml Structure
Complete Example
_schema-version: '3.1'
ID: bookshop
version: 1.0.0
description: Bookshop Application
parameters:
enable-parallel-deployments: true
build-parameters:
before-all:
- builder: custom
commands:
- npm ci
- npx cds build --production
modules:
# CAP Server
- name: bookshop-srv
type: nodejs
path: gen/srv
parameters:
buildpack: nodejs_buildpack
memory: 256M
build-parameters:
builder: npm
requires:
- name: bookshop-db
- name: bookshop-auth
provides:
- name: srv-api
properties:
srv-url: ${default-url}
# Database Deployer
- name: bookshop-db-deployer
type: hdb
path: gen/db
parameters:
buildpack: nodejs_buildpack
memory: 256M
requires:
- name: bookshop-db
# App Router (optional)
- name: bookshop-app
type: approuter.nodejs
path: app/router
parameters:
memory: 256M
disk-quota: 256M
requires:
- name: srv-api
group: destinations
properties:
name: srv-api
url: ~{srv-url}
forwardAuthToken: true
- name: bookshop-auth
resources:
# HDI Container
- name: bookshop-db
type: com.sap.xs.hdi-container
parameters:
service: hana
service-plan: hdi-shared
# XSUAA
- name: bookshop-auth
type: org.cloudfoundry.managed-service
parameters:
service: xsuaa
service-plan: application
path: ./xs-security.json
config:
xsappname: bookshop-${org}-${space}
tenant-mode: dedicatedxs-security.json
Generated Configuration
{
"xsappname": "bookshop",
"tenant-mode": "dedicated",
"scopes": [
{
"name": "$XSAPPNAME.admin",
"description": "Admin"
}
],
"role-templates": [
{
"name": "admin",
"scope-references": ["$XSAPPNAME.admin"]
}
],
"oauth2-configuration": {
"redirect-uris": [
"https://*.cfapps.*.hana.ondemand.com/**"
]
}
}Generate from CDS
cds compile srv --to xsuaa > xs-security.jsonBuild Process
Build for Production
# Clean and build
cds build --production
# Or with MTA build tool
mbt buildBuild Output
gen/
├── srv/ # Server bundle
│ ├── package.json
│ └── srv/
└── db/ # Database artifacts
└── src/
├── .hdiconfig
├── schema.cds
└── csv/Deployment
Login to CF
cf login -a https://api.cf.<region>.hana.ondemand.com
# Select org and space
# Or with SSO
cf login --ssoDeploy MTA
# Full deployment
cf deploy mta_archives/bookshop_1.0.0.mtar
# Or shortcut
cds upIncremental Deployment
# Deploy specific module
cf deploy mta_archives/bookshop_1.0.0.mtar -m bookshop-srv
# Skip unchanged modules
cf deploy --skip-unmodifiedService Bindings
Bind Services Manually
# Create HDI container
cf create-service hana hdi-shared bookshop-db
# Create XSUAA instance
cf create-service xsuaa application bookshop-auth -c xs-security.json
# Bind to app
cf bind-service bookshop-srv bookshop-db
cf bind-service bookshop-srv bookshop-authCheck Bindings
cf env bookshop-srvEnvironment Configuration
Production package.json
{
"cds": {
"requires": {
"[production]": {
"db": {
"kind": "hana",
"impl": "@cap-js/hana"
},
"auth": {
"kind": "xsuaa"
}
}
}
}
}Profile Selection
# Automatic in Cloud Foundry
NODE_ENV=production
# Or explicit
cds.env=production cds serveApp Router Configuration
xs-app.json
{
"welcomeFile": "index.html",
"authenticationMethod": "route",
"routes": [
{
"source": "^/api/(.*)$",
"target": "$1",
"destination": "srv-api",
"authenticationType": "xsuaa"
},
{
"source": "^(.*)$",
"target": "$1",
"service": "html5-apps-repo-rt",
"authenticationType": "xsuaa"
}
]
}Multitenancy Deployment
Additional Configuration
cds add multitenancy
cds add mtxmta.yaml Additions
modules:
- name: bookshop-mtx
type: nodejs
path: mtx/sidecar
requires:
- name: bookshop-db
- name: bookshop-auth
- name: bookshop-registry
- name: bookshop-sm
resources:
- name: bookshop-registry
type: org.cloudfoundry.managed-service
parameters:
service: saas-registry
service-plan: application
config:
appName: bookshop
xsappname: bookshop
appUrls:
getDependencies: ~{mtx-api/mtx-url}/-/cds/saas-provisioning/dependencies
onSubscription: ~{mtx-api/mtx-url}/-/cds/saas-provisioning/tenant/{tenantId}
onSubscriptionAsync: true
onUnSubscriptionAsync: true
callbackTimeoutMillis: 300000
- name: bookshop-sm
type: org.cloudfoundry.managed-service
parameters:
service: service-manager
service-plan: containerTroubleshooting
View Logs
cf logs bookshop-srv --recent
cf logs bookshop-srv # StreamCheck App Status
cf apps
cf app bookshop-srvRestart App
cf restart bookshop-srv
cf restage bookshop-srv # After env changesSSH into Container
cf ssh bookshop-srvCommon Issues
HANA Connection Failed
# Check HDI container
cf service bookshop-db
# Check binding
cf env bookshop-srv | grep VCAP_SERVICESAuthentication Errors
# Regenerate xs-security.json
cds compile srv --to xsuaa > xs-security.json
# Update XSUAA instance
cf update-service bookshop-auth -c xs-security.jsonMemory Issues
# Increase in mta.yaml
parameters:
memory: 512MCI/CD Integration
Basic Pipeline
# .github/workflows/deploy.yml
name: Deploy to CF
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Build
run: npx mbt build
- name: Deploy
run: |
cf login -a ${{ secrets.CF_API }} -u ${{ secrets.CF_USER }} -p ${{ secrets.CF_PASSWORD }} -o ${{ secrets.CF_ORG }} -s ${{ secrets.CF_SPACE }}
cf deploy mta_archives/*.mtar -fCAP Event Handlers for Node.js Complete Reference
Source: https://cap.cloud.sap/docs/node.js/events
Table of Contents
- Handler Registration
- Handler Phases
- Event Names
- Request Object (req)
- Error Handling
- Database Operations
- Connecting to Services
- Event Emission
- Lifecycle Events
- Context Access
- TypeScript
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 draftRequest 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 arrayResponse 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 emptyError 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);
}
}Related skills
How it compares
Pick sap-cap-capire over generic backend API skills when the target platform is SAP BTP and deliverables must be CAP CDS models with OData rather than standalone REST microservices.
FAQ
What does sap-cap-capire help developers build?
sap-cap-capire helps developers build SAP BTP cloud apps using CAP CDS models, OData services, and Node or Java handlers. It applies CAPire patterns so enterprise extensions ship with typed entities and service contracts.
Which SAP runtimes does sap-cap-capire cover?
sap-cap-capire covers SAP Cloud Application Programming Model projects on SAP BTP with Node.js and Java service handlers. Developers use it when OData APIs and CDS schemas must align with CAPire production conventions.