Core Services APIs
Class cds.Service
is the base class for all provided and connected services. As such, and complemented by cds.connect
and cds.Request
, it provides the essential APIs for implementing and consuming services.
Most frequently you would use these APIs from within custom implementations of your provided services. Here's an example from cap/samples with references to used APIs:
js
| APIs used: [Subclassing `cds.Service`](#srv-impls) [Using `cds.connect`](cds-connect) [Model Reflection](#srv-entities) [Handler Registration](#srv-on) [Using `req.data`](events#req-data) [Querying](#srv-run) [Messaging](#srv-emit) [Using `req.reply`](events#req-reply) [Using `req.error`](events#req-error) [Handler Registration](#srv-on) |
Content
How to Implement Services
Service implementations are essentially collections of event handlers registered with service instances to handle incoming requests and event messages. The sections below explain where and how to do so.
Where to Implement Services?
↳ In sibling .js files next to .cds sources
The easiest way to add service implementations is to simply place equally named .js files next to the .cds files containing the respective service definitions. In addition to direct siblings you can place them into relative subdirectories ./lib
or ./handlers
, allowing layouts like that:
srv/ # all in one foo-srv.cds foo-srv.js bar-srv.cds bar-srv.js
srv/ lib/ foo-srv.js bar-srv.js foo-srv.cds bar-srv.cds
srv/ handlers/ foo-srv.js bar-srv.js foo-srv.cds bar-srv.cds
Service implementations basically look like that:
// srv/lib/foo-srv.js
const cds = require('@sap/cds')
class FooService extends cds.Service {...}
module.exports = FooService
// srv/lib/foo-srv.js
const cds = require('@sap/cds')
class FooService extends cds.Service {...}
module.exports = FooService
↳ In files specified in @impl
annotations
Use the @impl
annotation to specify alternative files to load implementations from:
// srv/services.cds
service FooService @(impl:'./lib/foo-service.js') {}
service BarService @(impl:'./lib/bar-service.js') {}
// srv/services.cds
service FooService @(impl:'./lib/foo-service.js') {}
service BarService @(impl:'./lib/bar-service.js') {}
Learn more in configuration options on @impl
.
// srv/lib/foo-service.js
const cds = require('@sap/cds')
class FooService extends cds.Service {...}
module.exports = FooService
// srv/lib/foo-service.js
const cds = require('@sap/cds')
class FooService extends cds.Service {...}
module.exports = FooService
// srv/lib/bar-service.js
const cds = require('@sap/cds')
class BarService extends cds.Service {...}
module.exports = BarService
// srv/lib/bar-service.js
const cds = require('@sap/cds')
class BarService extends cds.Service {...}
module.exports = BarService
↳ With multiple implementations in one file
Assumed you declared multiple services in the same .cds file like that:
// srv/services.cds
service FooService {}
service BarService {}
// srv/services.cds
service FooService {}
service BarService {}
Instead of providing separate implementation files specified by @impl
annotations, you can also provide multiple implementations in a single sibling file like that:
// srv/services.js
const cds = require('@sap/cds')
class FooService extends cds.Service {...}
class BarService extends cds.Service {...}
module.exports = { FooService, BarService }
// srv/services.js
const cds = require('@sap/cds')
class FooService extends cds.Service {...}
class BarService extends cds.Service {...}
module.exports = { FooService, BarService }
Note: The exported names are expected to match the names of the service definitions in your CDS model.
How to Implement Services?
↳ As subclasses of cds.Service
You can create subclasses of cds.Service
, or subclasses thereof, from service implementation modules like so:
// for example, in srv/cat-service.js
const cds = require('@sap/cds')
class CatalogService extends cds.ApplicationService {
async init() {
const { Books } = this.entities
// register your event handlers...
this.before ('CREATE', Books, req => {...})
this.on ('UPDATE', Books, req => {...}) // overrides the default handler
// ensure to call super.init()
await super.init()
}
}
module.exports = CatalogService
// for example, in srv/cat-service.js
const cds = require('@sap/cds')
class CatalogService extends cds.ApplicationService {
async init() {
const { Books } = this.entities
// register your event handlers...
this.before ('CREATE', Books, req => {...})
this.on ('UPDATE', Books, req => {...}) // overrides the default handler
// ensure to call super.init()
await super.init()
}
}
module.exports = CatalogService
The init()
method acts like a parameter-less constructor. Ensure to call await super.init()
as in the previous example, to have the base class's handlers added.
As also shown in the previous example, you may register own handlers before the base class's ones, to intercept requests before the default handlers snap in.
You can also overload API methods of cds.Service
, or subclasses thereof.
↳ As plain functions
The simplest way to provide custom event handlers is to return a function that registers event handler with the instance of cds.Service
as follows:
module.exports = function(){ // `this` is the instance of cds.Service
this.before (['CREATE','UPDATE'],'Books', (req)=>{...})
this.on ('UPDATE','Books', (req)=>{...})
this.after ('READ','Books', (books)=>{...})
}
module.exports = function(){ // `this` is the instance of cds.Service
this.before (['CREATE','UPDATE'],'Books', (req)=>{...})
this.on ('UPDATE','Books', (req)=>{...})
this.after ('READ','Books', (books)=>{...})
}
Alternatively you can provide an arrow function like so:
module.exports = (srv)=>{ // `srv` is the instance of cds.Service
srv.on ('UPDATE','Books', (req)=>{...})
// ...
}
module.exports = (srv)=>{ // `srv` is the instance of cds.Service
srv.on ('UPDATE','Books', (req)=>{...})
// ...
}
↳ With async/await
Quite frequently you need to call async functions in your impl functions, just declare your impl function as async
to use await
inside:
const cds = require('@sap/cds')
module.exports = async function(){
const SomeOtherService = await cds.connect.to('SomeOtherService')
// ...
}
const cds = require('@sap/cds')
module.exports = async function(){
const SomeOtherService = await cds.connect.to('SomeOtherService')
// ...
}
↳ Wrapped with cds.service.impl
Wrap the impl function into cds.service.impl(...)
, which simply returns the function but gives you code assists in tools like VS Code:
const cds = require('@sap/cds')
module.exports = cds.service.impl (function(){ ... })
const cds = require('@sap/cds')
module.exports = cds.service.impl (function(){ ... })
Configuration / Annotations
Following are options used to control the construction of services, as either of:
- Annotations of corresponding service definitions
- Options in service configurations
- Options arguments in calls to
cds.serve
- Options arguments in calls to
cds.connect
@impl | o.impl = class | instance | function | module name
Use the impl
option specify the implementation used by this service.
Using the @impl
Annotation
Add an @impl
annotation to your service definition referring to a file in your project to load the implementation from:
// leading ./ -> relative to the .cds source
service FooService @(impl:'./lib/foo-service') {...}
// otherwise -> relative to project root
service BarService @(impl:'src/lib/bar-service') {...}
// leading ./ -> relative to the .cds source
service FooService @(impl:'./lib/foo-service') {...}
// otherwise -> relative to project root
service BarService @(impl:'src/lib/bar-service') {...}
Using cds.requires.<service>.impl
in Configurations
For required services you can specify a custom implementation to use like so:
"cds": {
"requires": {
"reviews-service": {
"kind": "odata",
"impl": "srv/external/reviews-service"
},
}
}
"cds": {
"requires": {
"reviews-service": {
"kind": "odata",
"impl": "srv/external/reviews-service"
},
}
}
As ad-hoc options to cds.serve
or cds.connect
You can also specify ad-hoc options in calls to cds.serve
or cds.connect
like so:
const ReviewsService = await cds.connect.to ('reviews-service', {
impl: 'srv/external/reviews-service'
})
const ReviewsService = await cds.connect.to ('reviews-service', {
impl: 'srv/external/reviews-service'
})
In this case, you can also specify an impl function, as subclass of cds.Service
or an instance of such like so:
const ReviewsService = await cds.connect.to ('reviews-service', {
impl: 'srv/external/reviews-service'
})
const ReviewsService = await cds.connect.to ('reviews-service', {
impl: 'srv/external/reviews-service'
})
@kind | o.kind = cds.requires.<kind>
Use option kind
to refer to an entry in the cds.requires
config options, which in turn contains presets for the other options, in particular for impl
.
For Required Services
In case of required services, you need to specify option kind
in respective entries to the cds.requires
config option like so:
"cds": {
"requires": {
"reviews-service": { "kind": "odata" },
"db": { "kind": "sql" },
}
}
"cds": {
"requires": {
"reviews-service": { "kind": "odata" },
"db": { "kind": "sql" },
}
}
The values usually refer to these pre-defined options:
sql
,sqlite
,hana
→ for database servicesodata
,rest
→ for external servicesenterprise-messaging
,file-based-messaging
→ for messaging services
Run cds env get requires
to see all default configurations.
For Provided Services
For services provided by your app, the default is kind = 'app-service'
, which is configured in built-in defaults like so:
"cds": {
"requires": {
"app-service": { "impl": "@sap/cds/lib/srv/ApplicationService.js" }
}
}
"cds": {
"requires": {
"app-service": { "impl": "@sap/cds/lib/srv/ApplicationService.js" }
}
}
Run cds env get requires.app-service
to see the concrete default config.
In effect, cds.serve
constructs instances of cds.ApplicationService
by default.
Using Custom Kinds
You can override this by adding a @kind
annotation to your service definition:
service FooService @(kind:'app-service') {...} // default
service BarService @(kind:'bar-service') {...} // custom
service FooService @(kind:'app-service') {...} // default
service BarService @(kind:'bar-service') {...} // custom
... backed by a corresponding entry to cds.requires
config options:
"cds": {
"requires": {
"bar-service": { "impl": "srv/bar-service.js" },
}
}
"cds": {
"requires": {
"bar-service": { "impl": "srv/bar-service.js" },
}
}
@path | o.path = string
By default cds.serve
determines the endpoints to serve individual services from the service definition's name, by 'slugifying' camel-case names, and removing 'Service'
suffixes like that:
SomeBookshopAdminService
→ some-bookshop-admin
@protocol | o.protocol = string
With the annotation @protocol
, you can configure a protocol adapter a service should be served by. By default, a service is served by the odata-v4
adapter. The supported protocol adapters are odata
(serve odata-v4
), odata-v4
, rest
, none
(to disable a service and mark it as internal).
Example:
@protocol: 'rest'
service CatalogService {
...
}
@protocol: 'rest'
service CatalogService {
...
}
@credentials | o.credentials = { url, ... }
Specific options, passed to services. For example, in case of a SQLite database service these are driver-specific options node-sqlite3
@model | o.model = csn
The model to use for this connection, either as an already parsed csn or as a filename of a model, which is then loaded with [cds.load
].
Model Reflection API
srv.name = string
The service's name, that means, the definition's name for services constructed with [cds.serve
], or the name of required services as passed to cds.connect
.
cds.serve ('CatalogService') //> .name = 'CatalogService'
cds.connect.to ('audit-log') //> .name = 'audit-log'
cds.connect.to ('db') //> .name = 'db'
cds.serve ('CatalogService') //> .name = 'CatalogService'
cds.connect.to ('audit-log') //> .name = 'audit-log'
cds.connect.to ('db') //> .name = 'db'
srv.model = csn
The linked model from which this service's definition was loaded.
This is === cds.model
by default, that is, unless you created services yourself with cds.serve
, specifying alternative models to load and construct new services from.
srv.definition ⇢ def
The linked service definition contained in the model which served as the blueprint for the given service instance.
srv.namespace ⇢ string
srv.entities (namespace) ⇢ {defs}
srv.events (namespace) ⇢ {defs}
srv.operations (namespace) ⇢ {defs}
These methods provide convenient access to the linked definitions of all entities, events, as well as actions and functions provided by this service.
const db = await cds.connect.to('db')
const { Books, Authors } = db.entities('my.bookshop')
const db = await cds.connect.to('db')
const { Books, Authors } = db.entities('my.bookshop')
These methods are actually shortcuts to their counterparts provided by linked models, with the default namespace being the service definition's name.
Using Fully Qualified Names:
Many service consumption methods, such as srv.run
expect fully qualified names of entities or an entity definition. Using these reflection methods greatly simplifies code as it avoids repeating namespaces all over the place.
For example, the fully qualified name of entity Books
in cap/samples/bookshop is sap.capire.bookshop.Books
. So, queries would have to be written like that using name strings:
const books = await cds.read ('sap.capire.bookshop.Books')
const books = await cds.read ('sap.capire.bookshop.Books')
So, rather prefer that pattern:
const { Books } = cds.entities
const books = await cds.read (Books)
const { Books } = cds.entities
const books = await cds.read (Books)
Handler Registration API
Register event handlers with instances of cds.Service
to add custom logic to serve operations or react to events emitted by these services.
srv.on (event, path?, handler) ⇢ this
Handlers registered with this method are run in sequence, with each handler being able to terminate the sequence (cf. Outcomes below). This termination capability in combination with registering handlers using [srv.prepend
], lets you register custom handlers to run instead of the generic ones. If you want to defer to the generic handlers, invoke next
in your custom handler (see Handling Examples below).
Arguments:
event
— name of a single event or an array of multiple suchpath
— entity CSN object, name of an exposed entity, a path, or an array of multiple suchhandler(req, next)
— the handler function
A service handler registered by entity name can use a relative or fully qualified name, containing the service namespace. A database handler needs a fully qualified entity name.
learn more about the req
argument in handler functions.
Registration Examples:
// Direct request, for example, GET /Books
srv.on('READ','Books', (req)=>{...})
// Registration by fully-qualified name, for example, GET /Books
srv.on('READ','serviceName.Books', (req)=>{...})
// Navigation requests, for example, GET /Authors/201/authors
srv.on('READ','Authors/books', (req)=>{ let [ID] = req.params; ...})
// Registration by CSN, for example, GET /Books
srv.on('READ', Books, (req)=>{...})
// Calls to unbound actions/functions
srv.on('cancelOrder', (req)=>{...})
// Calls to bound actions/functions
srv.on('cancel','Order', (req)=>{...})
// Direct request, for example, GET /Books
srv.on('READ','Books', (req)=>{...})
// Registration by fully-qualified name, for example, GET /Books
srv.on('READ','serviceName.Books', (req)=>{...})
// Navigation requests, for example, GET /Authors/201/authors
srv.on('READ','Authors/books', (req)=>{ let [ID] = req.params; ...})
// Registration by CSN, for example, GET /Books
srv.on('READ', Books, (req)=>{...})
// Calls to unbound actions/functions
srv.on('cancelOrder', (req)=>{...})
// Calls to bound actions/functions
srv.on('cancel','Order', (req)=>{...})
Handling Examples:
Requests can be handled in one of the following ways:
- Call [
req.reply
] synchronously or asynchronously:
srv.on('READ','Books', async (req)=> req.reply(await cds.tx(req).run(...))
srv.on('READ','Books', (req)=> req.reply(...))
srv.on('READ','Books', async (req)=> req.reply(await cds.tx(req).run(...))
srv.on('READ','Books', (req)=> req.reply(...))
- Return results or a Promise resolving to some → framework calls [
req.reply
]:
srv.on('READ','Books', (req)=> cds.tx(req).run(...))
srv.on('READ','Books', (req)=> [ ... ])
srv.on('READ','Books', (req)=> cds.tx(req).run(...))
srv.on('READ','Books', (req)=> [ ... ])
srv.on('READ','Books', ()=> SELECT.from(Books))
srv.on('READ','Books', ()=> SELECT.from(Books))
- Call
next
as in express.js to delegate to handlers down the chain:
srv.on('READ','Books', (req,next)=>{
if (...) return SELECT.from(Books) //> ... handle req my own
else return next() //> delegate to next/default handlers
})
srv.on('READ','Books', (req,next)=>{
if (...) return SELECT.from(Books) //> ... handle req my own
else return next() //> delegate to next/default handlers
})
IMPORTANT:
- Ensure to properly add calls to
next()
in your promise chain, either usingawait
or by passing through its return value as shown in the previous example. - The result of
await next()
depends onreq.query
. For example, forGET /Books
the result will be an array, whereas forGET /Books/1
the result is an object.
- Reject the request through [
req.reject
],throw
, or returning a rejecting Promise:
srv.on('READ','Books', (req)=> req.reject(409,'...'))
srv.on('READ','Books', (req)=> Promise.reject(...))
srv.on('READ','Books', (req)=> { throw ... })
srv.on('READ','Books', (req)=> req.reject(409,'...'))
srv.on('READ','Books', (req)=> Promise.reject(...))
srv.on('READ','Books', (req)=> { throw ... })
Shortcuts:
As a yet more convenient shortcut to 3. you can just pass in the cds.ql
object instead of a handler function; so, overall, these examples are equivalent:
const {Books} = srv.entities
srv.on('READ','Books', (req)=> cds.tx(req).run(SELECT.from(Books))
srv.on('READ','Books', ()=> SELECT.from(Books))
srv.on('READ','Books', SELECT.from(Books))
const {Books} = srv.entities
srv.on('READ','Books', (req)=> cds.tx(req).run(SELECT.from(Books))
srv.on('READ','Books', ()=> SELECT.from(Books))
srv.on('READ','Books', SELECT.from(Books))
Multiple handlers for same events
Arbitrary numbers of handlers for the same events can be registered. Those registered with .on
are executed in order of their registration, while those registered with .before
or .after
the execution order isn’t guaranteed; they might be executed in parallel.
cds.serve('cat-service') .with (function(){
this.on('READ', Books, ()=>{/* called first */})
this.on('READ', Books, ()=>{/* called second */})
})
cds.serve('cat-service') .with (function(){
this.on('READ', Books, ()=>{/* called first */})
this.on('READ', Books, ()=>{/* called second */})
})
Single handlers for multiple events
Omit the <entity>
argument to register handlers for all entities. Or add handlers for all events as well as standard express.js middlewares with method .use
:
cds.serve('cat-service') .with (function(){
this.on ('READ', ()=> {/* handles read events on all entities */})
this.on ('*', ()=> {/* handles all events on all entities */})
})
cds.serve('cat-service') .with (function(){
this.on ('READ', ()=> {/* handles read events on all entities */})
this.on ('*', ()=> {/* handles all events on all entities */})
})
srv.on ('error', (err, req) => {})
Using the special event name error
, you can register a custom error handler that is invoked whenever an error will be returned to the client. The handler receives the error object err
and the respective request object req
. Only synchroneous modifications of the error object are allowed.
Examples:
cds.serve('cat-service') .with (function(){
this.on ('error', (err, req) => {
// modify the message
err.message = 'Oh no! ' + err.message
// attach some custom data
err['@myCustomProperty'] = 'Hello, World!'
})
})
cds.serve('cat-service') .with (function(){
this.on ('error', (err, req) => {
// modify the message
err.message = 'Oh no! ' + err.message
// attach some custom data
err['@myCustomProperty'] = 'Hello, World!'
})
})
WARNING
Note: Depending on when the error occurs, the request's continuation (that is, cds.context
) may be undefined
or incomplete (for example, missing user information).
The error is subsequently processed for the client following OData's Error Response Body format. Hence, if you want to add custom properties, they must be prefixed with @
in order to not be purged.
srv.before (event, entity?, handler) ⇢ this
Registers a handler to run before the ones registered with .on()
, that is, before the generic handlers. Commonly used to add custom input validations.
Arguments are the same as in srv.on
, except that the handler only has the request argument (that is, handler(req)
).
Examples:
this.before ('CREATE','Order', (req)=>{
const order = req.data
if (order.quantity > 11) throw 'Order quantity must not exceed 11'
})
this.before ('CREATE','Order', (req)=>{
const order = req.data
if (order.quantity > 11) throw 'Order quantity must not exceed 11'
})
learn more about the req
argument in handler functions
You can as well trigger additional operations in before handlers:
srv.before ('CREATE','Order', (req)=>{
const order = req.data
return UPDATE(Books).set ('stock -=', order.quantity)
})
srv.before ('CREATE','Order', (req)=>{
const order = req.data
return UPDATE(Books).set ('stock -=', order.quantity)
})
Note: This example would fail assumed you have a constraint on book stocks to not become negative, so the UPDATE also acts as a validation.
Note:
- All before handlers are executed in order of their registration.
- Always return a promise if your handlers trigger asynchronous tasks.
- All promises returned are collected in a
Promise.all
and - The next phase running the
.on
handlers starts when all promises resolved.
Hence: You can terminate requests early by throwing exceptions or through [req.reject
] in the handlers' synchronous parts. Asynchronous parts however run in parallel, so you can't react on side-effects between .before handlers.
srv.after (event, entity?, handler) ⇢ this
Registers a handler to run after the generic handler. To be more precise: It runs on the results returned by the generic handler (which is asynchronous to the incoming handlers). Use it to modify the response.
Arguments are the same as in srv.on
, except that the handler has the result as first and the request as second argument (that is, handler(data, req)
).
Note: The type of the result (that means, array, object, primitive, etc.) depends on the respective request.
Example:
this.after('READ','Books', (books)=>{
for (let each of books) each.stock > 111 && each.discount='11%'
})
this.after('READ','Books', (books)=>{
for (let each of books) each.stock > 111 && each.discount='11%'
})
Name the event or the param each
as convenience shortcut for a per-row handler:
this.after('each','Books', (book)=>{
book.stock > 111 && book.discount='11%'
})
this.after('each','Books', (book)=>{
book.stock > 111 && book.discount='11%'
})
this.after('READ','Books', (each)=>{
each.stock > 111 && each.discount='11%'
})
this.after('READ','Books', (each)=>{
each.stock > 111 && each.discount='11%'
})
Note: This is only meant for synchronous code due to performance. Note: Parameter
each
is not minifier-safe.
Use the secondary param req
in case you need to reflect on the inbound request:
this.after('READ','Books', (books,req)=>{
if (req.data.ID) ...
})
this.after('READ','Books', (books,req)=>{
if (req.data.ID) ...
})
Only modifications are allowed. That is, replacing the result like in the following example isn’t possible:
this.after('READ','Books', (books)=>{
return \<something else...\>
})
this.after('READ','Books', (books)=>{
return \<something else...\>
})
Reasoning: Multiple after handlers can be added by different involved packages, including extension packages, and each of which would expect the same input. If one would exchange the results, the chain would break.
Keep in mind that all after handlers are executed in parallel via
Promise.all()
so they must not have side-effects!
To replace the result, use on
handlers as follows:
this.on('READ','Books', async (req,next)=>{
const books = await next()
return \<something else...\>
})
this.on('READ','Books', async (req,next)=>{
const books = await next()
return \<something else...\>
})
In case the result is a primitive, for example, a string, then also synchronous modifications must be done in an on
handler as the change would stay local to the after handler (pass by value).
srv.reject (event, entity?) ⇢ this
Registers a generic handler that automatically rejects incoming request with a standard error message. You can specify multiple events and entities.
Arguments:
event
— name of a single event or an array of multiple suchentity
— name of an exposed entity or an array of multiple such
Examples:
this.reject ('READ', 'Orders')
this.reject (['CREATE','UPDATE','DELETE'], ['Books','Authors'])
this.reject ('READ', 'Orders')
this.reject (['CREATE','UPDATE','DELETE'], ['Books','Authors'])
srv.prepend (function)
Use srv.prepend
in order to register handlers, which shall be executed before already registered handlers. In particular, this can be used to override handlers from reused services as in cap/samples/bookstore/srv/mashup.js:
For example, the following would register a handler for inserting Orders that runs instead of the default handlers of the connected database service.
const CatalogService = await cds.connect.to ('CatalogService')
const ReviewsService = await cds.connect.to ('ReviewsService')
//
// Delegate requests to read reviews to the ReviewsService
// Note: prepend is necessary to intercept generic default handler
//
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
console.debug ('> delegating request to ReviewsService')
const [id] = req.params, { columns, limit } = req.query.SELECT
return ReviewsService.read ('Reviews',columns).limit(limit).where({subject:String(id)})
}))
const CatalogService = await cds.connect.to ('CatalogService')
const ReviewsService = await cds.connect.to ('ReviewsService')
//
// Delegate requests to read reviews to the ReviewsService
// Note: prepend is necessary to intercept generic default handler
//
CatalogService.prepend (srv => srv.on ('READ', 'Books/reviews', (req) => {
console.debug ('> delegating request to ReviewsService')
const [id] = req.params, { columns, limit } = req.query.SELECT
return ReviewsService.read ('Reviews',columns).limit(limit).where({subject:String(id)})
}))
You could also use that to add support for custom behavior to generic database services:
const db = await cds.connect.to('db')
db.prepend (()=>{
db.on ('INSERT','Orders', req => {...})
})
const db = await cds.connect.to('db')
db.prepend (()=>{
db.on ('INSERT','Orders', req => {...})
})
For Common CRUD and REST-style Events
CAP service definitions frequently provide data-centric APIs by exposing projections on underlying domain model entities as introduced in the Getting Started guide:
using { sap.capire.bookshop as my } from '../db/schema';
service AdminService {
entity Books as projection on my.Books;
entity Authors as projection on my.Authors;
}
using { sap.capire.bookshop as my } from '../db/schema';
service AdminService {
entity Books as projection on my.Books;
entity Authors as projection on my.Authors;
}
In effect, CAP will provide generic service providers, which serve corresponding CRUD request (for OData) which map to corresponding REST Request as follows:
For AdminService at /admin
endpoint
REST Paths | CRUD Operation |
---|---|
POST /Books | CREATE Books |
GET /Books | READ Books |
PUT /Books | UPDATE Books |
DELETE /Books | DELETE Books |
In addition, CAP provides built-in support for Fiori Draft, which add additional CRUD events, like
NEW
,EDIT
,PATCH
, andSAVE
. → Learn more about Fiori Drafts
For each of which you can add custom handlers, either by specifying the CRUD operation or by specifying the corresponding REST method as follows:
// srv/admin-service.js
const cds = require('@sap/cds')
module.exports = cds.service.impl (function(){
// CRUD-style handler registration REST-style registration
this.on ('READ','Books', ...) /* or: */ this.on ('GET','Books', ...)
this.on ('CREATE','Books', ...) /* or: */ this.on ('POST','Books', ...)
//...
})
// srv/admin-service.js
const cds = require('@sap/cds')
module.exports = cds.service.impl (function(){
// CRUD-style handler registration REST-style registration
this.on ('READ','Books', ...) /* or: */ this.on ('GET','Books', ...)
this.on ('CREATE','Books', ...) /* or: */ this.on ('POST','Books', ...)
//...
})
For Custom Events, i.e., Actions and Functions
In addition to the common CRUD and REST events, you can declare custom events as actions or functions.
service CatalogService { ...
action submitOrder (book: Books:ID, quantity: Integer);
function getMyOrders() returns array of Orders;
}
service CatalogService { ...
action submitOrder (book: Books:ID, quantity: Integer);
function getMyOrders() returns array of Orders;
}
Register handlers for these as follows:
// srv/admin-service.js
const cds = require('@sap/cds')
module.exports = cds.service.impl (function(){
this.on ('submitOrder', (req) => {...})
this.on ('getMyOrders', (req) => {...})
})
// srv/admin-service.js
const cds = require('@sap/cds')
module.exports = cds.service.impl (function(){
this.on ('submitOrder', (req) => {...})
this.on ('getMyOrders', (req) => {...})
})
For Provided and Connected Services
Most frequently you would add event handlers to provided services. Yet, you can also add handlers to services consumed in your project, even to generic services provided by the CAP framework itself, like the default database service:
const db = await cds.connect.to('db')
db.before ('INSERT','Orders', req => {...})
db.after ('READ','*', each => {...})
const db = await cds.connect.to('db')
db.before ('INSERT','Orders', req => {...})
db.after ('READ','*', each => {...})
For Synchronous and Asynchronous Events
The APIs documented below are used to register handlers for synchronous requests as well as for subscription to asynchronous events.
For example, given this service definition:
service ReviewsService { //...
event reviewed : { subject: String; rating: Decimal }
}
service ReviewsService { //...
event reviewed : { subject: String; rating: Decimal }
}
... an event subscription and handling would look like that.
const ReviewsService = await cds.connect.to('ReviewsService')
ReviewsService.on ('reviewed', (msg) => {...})
const ReviewsService = await cds.connect.to('ReviewsService')
ReviewsService.on ('reviewed', (msg) => {...})
Learn more about Messaging API
Messaging API
srv.emit ({ event, data?, headers? })
This is the basic method to send both, asynchronous event messages, as well as synchronous requests. The implementation constructs an instance of [cds.Event
], which is then dispatched through all registered event handlers.
Common Usages:
You can use srv.emit
as a basic and flexible alternative to srv.run
, for example to send queries plus additional request headers
to remote services:
const query = SELECT.from(Foo), tx = srv.tx(req)
tx.run (query) //> without custom headers
tx.emit ({ query }) //> equivalent to tx.run()
tx.emit ({ query, headers:{...} })
const query = SELECT.from(Foo), tx = srv.tx(req)
tx.run (query) //> without custom headers
tx.emit ({ query }) //> equivalent to tx.run()
tx.emit ({ query, headers:{...} })
A typical usage for asynchronous messages looks like that:
this.emit ({ event:'reviewed', data:{ subject, rating }, ... })
this.emit ('reviewed', { subject, rating }) //> see below
this.emit ({ event:'reviewed', data:{ subject, rating }, ... })
this.emit ('reviewed', { subject, rating }) //> see below
Returns a Promise resolving to the response of respective event handlers.
For results of queries, see srv.run
.
srv.emit (event, data?, headers?)
Convenience variant to srv.emit
, which allows to specify the primary properties for emitting asynchronous event messages. Here's an example from cap/samples/reviews:
this.emit ('reviewed', { subject, rating })
this.emit ('reviewed', { subject, rating })
srv.on (event, handler) ⇢ this
Subscribe to asynchronous events by registering handlers using the common srv.on()
method.
For example, given this service definition:
service ReviewsService { //...
event reviewed : { subject: String; rating: Decimal }
}
service ReviewsService { //...
event reviewed : { subject: String; rating: Decimal }
}
... an event subscription and handling would look like that.
const ReviewsService = await cds.connect.to('ReviewsService')
ReviewsService.on ('reviewed', (msg) => {...})
const ReviewsService = await cds.connect.to('ReviewsService')
ReviewsService.on ('reviewed', (msg) => {...})
There's one major difference, though: Handlers for synchronous requests execute as interceptors, which pass control to subsequent handlers by calling next()
, handlers for asynchronous events execute as listeners with all registered handlers being executed without calling next()
.
REST-style API
srv.send ({ (method, path) | query | event, data?, headers?, ...}) ⇢ results
This is a convenience alternative to srv.emit
method to send both, asynchronous event messages, as well as synchronous requests. The implementation constructs an instance of cds.Request
, which is then dispatched through all registered event handlers.
Common Usages:
srv.send
can be used in a manner similar to srv.emit
to send requests using HTTP methods and additional request headers
if neccessary:
const srv = await cds.connect.to('SomeService')
const data = { ID: 111, name: 'Mark Twain' }
const headers = {...}
await srv.send({ method: 'POST', path: 'Authors', data, headers }) //> send HTTP request
srv.send({ event: 'AuthorCreated', data, headers })
const srv = await cds.connect.to('SomeService')
const data = { ID: 111, name: 'Mark Twain' }
const headers = {...}
await srv.send({ method: 'POST', path: 'Authors', data, headers }) //> send HTTP request
srv.send({ event: 'AuthorCreated', data, headers })
Learn more about cds.Requests
.Learn more about Event Messages.
Alternatively queries in CQN notation can be used:
const { Books, Authors } = srv.entities //> reflection
await srv.send({ query: INSERT.into(Authors, { ID: 111, name: 'Mark Twain' }) }) //> without custom headers
const query = [SELECT.from(Books), SELECT.from(Authors)]
const headers = {...}
const [books, authors] = await srv.send({ query, headers })
const { Books, Authors } = srv.entities //> reflection
await srv.send({ query: INSERT.into(Authors, { ID: 111, name: 'Mark Twain' }) }) //> without custom headers
const query = [SELECT.from(Books), SELECT.from(Authors)]
const headers = {...}
const [books, authors] = await srv.send({ query, headers })
Learn more about using queries in srv.run
.Learn more about service-related reflection using srv.entities
.
Returns a Promise resolving to the response of a respective request.
For results of queries, see srv.run
.
srv.send (method, path, data?, headers?) ⇢ results
A variant of srv.send
which allows to specify the primary properties for sending requests:
const twain = await srv.send('GET', 'Authors(111)')
const headers = {...}
const data = { ID: 222, title: 'The Adventures of Tom Sawyer', author: { ID: twain.ID } }
await srv.send('POST', 'Books', data, headers)
const twain = await srv.send('GET', 'Authors(111)')
const headers = {...}
const data = { ID: 222, title: 'The Adventures of Tom Sawyer', author: { ID: twain.ID } }
await srv.send('POST', 'Books', data, headers)
srv.send (method, data?, headers?) ⇢ results
A variant of srv.send
which, allows to specify the primary properties for sending requests targeting unbound actions or functions:
await srv.send('submitOrder', { book: 251, quantity: 1 })
await srv.send('submitOrder', { book: 251, quantity: 1 })
Convenient Shortcuts:
srv.get (entity | path, data?) ... ⇢ results
srv.put (entity | path, data?) ... ⇢ results
srv.post (entity | path, data?) ... ⇢ results
srv.patch (entity | path, data?) ... ⇢ results
srv.delete (entity | path, data?) ... ⇢ results
These methods are HTTP method-style counterparts to the CRUD-style convenience methods. As with these, the returned queries can be executed with await
. For invoking actions or functions, do not use .post()
or .get()
but see Actions API.
REST and OData services support these basic http methods, which are mapped to their CRUD counterpart as documented in the following. They can be used to construct data queries as with the CRUD variants as well as be used to send plain HTTP requests.
Common Usages:
await srv.get(Books).where({ID:111})
await srv.post(Books).entries({ID:111, ...})
await srv.patch(Books,111).with({...})
await srv.delete(Books,111)
await srv.get(Books).where({ID:111})
await srv.post(Books).entries({ID:111, ...})
await srv.patch(Books,111).with({...})
await srv.delete(Books,111)
These are equivalent to:
await srv.read(Books).where({ID:111})
await srv.create(Books).entries({ID:111, ...})
await srv.update(Books,111).with({...})
await srv.delete(Books,111)
await srv.read(Books).where({ID:111})
await srv.create(Books).entries({ID:111, ...})
await srv.update(Books,111).with({...})
await srv.delete(Books,111)
Note:
UPDATE
translates toPATCH
, notPUT
.
Plain REST Usages:
While the previous usage samples still constructed queries, and rather worked as using syntax aliases, you can also use these methods to send plain, arbitrary HTTP requests to remote services. Just pass a string starting with /
as the first argument to do so:
srv.post('/some/arbitrary/path', {foo:'bar'})
srv.get('/some/arbitrary/path/111')
srv.patch('/some/arbitrary/path', {foo:'bar'})
srv.delete('/some/arbitrary/path/111')
srv.post('/some/arbitrary/path', {foo:'bar'})
srv.get('/some/arbitrary/path/111')
srv.patch('/some/arbitrary/path', {foo:'bar'})
srv.delete('/some/arbitrary/path/111')
Querying API
srv.run (query) → results
This is the central function to run queries. It expects a query or an array of queries in CQN notation, and returns a Promise resolving to the queries' results.
Common Usage:
const srv = await cds.connect.to ('SomeService')
const { Books, Authors } = srv.entities //> reflection
const books = await srv.run (SELECT.from(Books))
const [books, authors] = await srv.run ([SELECT.from(Books), SELECT.from(Authors)])
const srv = await cds.connect.to ('SomeService')
const { Books, Authors } = srv.entities //> reflection
const books = await srv.run (SELECT.from(Books))
const [books, authors] = await srv.run ([SELECT.from(Books), SELECT.from(Authors)])
Learn more about service-related reflection using srv.entities
.Learn more about srv.run
variant to send native query string.
TIP
If an array of queries is passed to srv.run
, queries are run in parallel.
Returns a Promise resolving to...
- to a number-like object indicating affected rows in case of
UPDATE
,DELETE
. - an iterable allowing to reflect on (generated) primary keys
- an array of rows for
SELECT
queries
For example, you can use results like so:
const tx = cds.tx(req)
const [ Emily, Charlotte ] = await tx.run (INSERT.into (Authors,[
{name:'Emily Brontë'},
{name:'Charlotte Brontë'},
]))
await tx.run (INSERT.into(Books).columns('title','author_ID').rows(
[ 'Wuthering Heights', Emily.ID ],
[ 'Jayne Eyre', Charlotte.ID ],
))
const books = await tx.run (SELECT.from(Books))
const affectedRows = UPDATE(Books,books[0].ID)
.with(`stock -=`,order.quantity)
.where(`stock>=`,order.quantity)
if (affectedRows < 1) req.reject(409,'Sold out, sorry')
const tx = cds.tx(req)
const [ Emily, Charlotte ] = await tx.run (INSERT.into (Authors,[
{name:'Emily Brontë'},
{name:'Charlotte Brontë'},
]))
await tx.run (INSERT.into(Books).columns('title','author_ID').rows(
[ 'Wuthering Heights', Emily.ID ],
[ 'Jayne Eyre', Charlotte.ID ],
))
const books = await tx.run (SELECT.from(Books))
const affectedRows = UPDATE(Books,books[0].ID)
.with(`stock -=`,order.quantity)
.where(`stock>=`,order.quantity)
if (affectedRows < 1) req.reject(409,'Sold out, sorry')
srv.run (string, args?) → results
Variant of srv.run
which accepts native query strings, as understood by the receiving service, instead of CQN queries. For example, a SQL string in case of a connected SQL database.
Common Usage:
cds.run (`SELECT * from sap_capire_bookshop_Books`)
cds.run (`CALL Some_Stored_Procedure (11,'foo')`)
cds.run (`SELECT * from Some_Table_Function (11,'foo')`)
cds.run (`SELECT * from sap_capire_bookshop_Books`)
cds.run (`CALL Some_Stored_Procedure (11,'foo')`)
cds.run (`SELECT * from Some_Table_Function (11,'foo')`)
TIP
You need to pass fully qualified database table names.
Passing Arguments:
Argument args
is an optional array or object of binding parameters bound to respective placeholders in the query using one of these placeholders:
:name
— named parameters bound to respective entries in anargs
object?
— positional parameters bound to entries ofargs
in order of occurrence
cds.run ('SELECT * from Authors where name like ?',['%Poe%'])
cds.run ('SELECT * from Authors where name like ?',['%Poe%'])
TIP
Prefer that over concatenating values into query strings to avoid SQL injection.
Convenient Shortcuts:
srv.read (entity, key?, projection?) ... ⇢ SELECT
query
srv.create (entity, key?) ... ⇢ INSERT
query
srv.update (entity, key?) ... ⇢ UPDATE
query
srv.delete (entity, key?) ... ⇢ DELETE
query
These methods construct queries in a fluent method-call style instead of the Embedded QL style provided by cds.ql
. The returned queries can be executed with await
.
A typical usage is as follows:
const books = await cds.read('Books').orderBy('title')
const books = await cds.read('Books').orderBy('title')
Essentially, each of these methods simply starts a fluent query construction using their cds.ql
counterparts, which can be continued using the respective tail methods, and sent to the service backend upon invocation of .then()
. For example, think of the implementation of cds.read
as follows:
read (entity) {
const q = SELECT.from(entity)
return Object.assign (q, { then:(r,e) => this.run(q).then(r,e) }
}
read (entity) {
const q = SELECT.from(entity)
return Object.assign (q, { then:(r,e) => this.run(q).then(r,e) }
}
The following three variants to read Books are equivalent:
const books1 = await cds.run(SELECT.from(Books).where({ID:111}))
const books2 = await cds.read(Books).where({ID:111})
const books3 = await SELECT.from(Books).where({ID:111})
const books1 = await cds.run(SELECT.from(Books).where({ID:111}))
const books2 = await cds.read(Books).where({ID:111})
const books3 = await SELECT.from(Books).where({ID:111})
srv.insert (data) .into (entity) ... ⇢ INSERT
query
Method srv.insert
is a SQL-reminiscent variant of srv.create
with the following being equivalent:
srv.insert(data) .into (entity)
srv.create(entity) .entries (data)
srv.insert(data) .into (entity)
srv.create(entity) .entries (data)
srv.upsert (data) .into (entity) ... ⇢ UPSERT
query
Method srv.upsert
inserts an entity or updates it if it doesn't exist.
srv.upsert(data) .into (entity)
srv.upsert(data) .into (entity)
srv.exists (entity) .where(keys) ... → SELECT
query
Method srv.exists
returns a truthy value if the entity exists. It returns a falsy value otherwise:
const bookExists = await srv.exists(Books).where({ ID: 5 })
if (bookExists) {
...
}
const bookExists = await srv.exists(Books).where({ ID: 5 })
if (bookExists) {
...
}
Actions API
In case you declared custom actions and functions in the service definition of a connected or provided service, the respective instance of cds.Service
will automatically be equipped with corresponding JavaScript methods, allowing you to write code similar to srv.create/read/...
.
Unbound Actions / Functions
For example, with that service definition (taken from cap/samples/bookshop):
service CatalogService { ...
action submitOrder (book: Books:ID, quantity: Integer);
}
service CatalogService { ...
action submitOrder (book: Books:ID, quantity: Integer);
}
You can invoke the declared submitOrder
action from your code as follows:
const cats = await cds.connect.to('CatalogService')
// basic variant using srv.send
const res1 = await cats.send('submitOrder',{ book: 251, quantity: 1 })
// named args variant
const res2 = await cats.submitOrder ({ book: 251, quantity: 1 })
// positional args variant
const res3 = await cats.submitOrder (251, 1)
const cats = await cds.connect.to('CatalogService')
// basic variant using srv.send
const res1 = await cats.send('submitOrder',{ book: 251, quantity: 1 })
// named args variant
const res2 = await cats.submitOrder ({ book: 251, quantity: 1 })
// positional args variant
const res3 = await cats.submitOrder (251, 1)
Bound Actions / Functions
Bound actions have two implicit leading arguments with the target entity's name and primary key. Assumed we had a bound action like that:
service CatalogService { ...
entity Books { ... } actions {
action submitOrder (quantity: Integer);
}
}
service CatalogService { ...
entity Books { ... } actions {
action submitOrder (quantity: Integer);
}
}
Then, you'd invoke that as follows:
const cats = await cds.connect.to('CatalogService')
const res3 = await cats.submitOrder ('Books', 251, 1)
const cats = await cds.connect.to('CatalogService')
const res3 = await cats.submitOrder ('Books', 251, 1)
Streaming API
WARNING
Streaming is currently limited to database services.
srv.stream (column) .from (entity) .where (filter) ⇢ Readable Stream
This method allows streaming binary data properties. It returns a read stream which can be used to pipe to write streams, as shown in the following examples.
const stream = srv.stream().from('T', { ID: 1 }, a => a.data)
stream.pipe(process.stdout)
const stream = srv.stream().from('T', { ID: 1 }, a => a.data)
stream.pipe(process.stdout)
const stream = srv.stream('data').from('T', { ID: 1 })
stream.pipe(process.stdout)
const stream = srv.stream('data').from('T', { ID: 1 })
stream.pipe(process.stdout)
const stream = srv.stream('data').from('T').where({ ID: 1 })
stream.pipe(process.stdout)
const stream = srv.stream('data').from('T').where({ ID: 1 })
stream.pipe(process.stdout)
srv.stream (cqn) → Promise < Readable Stream >
This is a variant of srv.stream
, which accepts a SELECT
query as input and returns a Promise resolving to result stream when the query matched to an existing row in the database. The query is expected to select a single column and a single data row. Otherwise, an error is thrown.
const stream = await srv.stream( SELECT('image').from('Foo',111) )
stream.pipe(process.stdout)
const stream = await srv.stream( SELECT('image').from('Foo',111) )
stream.pipe(process.stdout)
srv.foreach (entity | query, args?, callback) → Promise
Executes the statement and processes the result set row by row. Use this API instead of cds.run
if you expect large result sets. Then they're processed in a streaming-like fashion instead of materializing the full result set in memory before processing.
Common Usages:
cds.foreach (SELECT.from('Foo'), each => console.log(each))
cds.foreach ('Foo', each => console.log(each))
cds.foreach (SELECT.from('Foo'), each => console.log(each))
cds.foreach ('Foo', each => console.log(each))
As depicted in the second line, a plain entity name can be used for the
entity
argument in which case it's expanded to aSELECT * from ...
.