Search

Providing Services

Learn in this guide how to define services exposed through REST and OData APIs, including an overview of generic features to serve metadata as well as most CRUD requests out of the box, input validations and more.

Defining Services

All-in-one Service Definitions

In its most basic form, a service definition simply declares the types of entities it provides access to. For example:

service BookshopService {

  entity Books {
    key ID : UUID;
    title  : String;
    author : Association to Authors;
  }

  entity Authors {
    key ID : UUID;
    name   : String;
    books  : Association to many Books on books.author = $self;
  }

}

We recommend plural forms for entities exposed by services.
→ see also: Naming Conventions

In fact, this simplistic service definition is all we need to start a full-fledged server, exposed through OData, or plain REST APIs, that serves CRUD requests automatically.

Services as Facades — exposing views on underlying entities

In contrast to all-in-one service definitions as shown before, services usually expose projections / views on domain model entities. Thereby services become facades to underlying data, exposing different aspects tailored to respective use-cases.

services

We borrowed well-known concepts of view building from SQL, which in turn uses a reflexive query language to express projections and selections of entities based on underlying entities recursively.

Example:

using { sap.capire.bookshop as my } from '../db/schema';
service CatalogService @(path:'/browse') {
  @readonly entity Books as SELECT from my.Books {*,
    author.name as author
  } excluding { createdBy, modifiedBy };
}

See this source in cap/samples

Not bound to SQL data sources
Note that even though we borrowed the well-known, powerful, and proven language query language from SQL, this doesn’t mean that such projections are bound to SQL backends. Yes, it allows to do.
Key for Generic Providers
Capturing projections declaratively like this is a key prerequisite to provide generic providers, which automatically serve CRUD requests: Upon incoming requests, the generic handlers reflect the views in their services’ models to construct corresponding queries to connected data sources, which may be a database, but can also be a remote service.

CQL, the query language used in CAP, is based on standard SQL, hence, all the standard SQL clauses and language constructs, like where, order by etc. are available to express projections as shown in the previous example.

Auto-redirected Associations

If two entities A and B from an underlying model are exposed in a service by projected entities Ax and Bx, all exposed associations from A to B and vice versa are redirected in the projection to Ax and Bx, respectively. This ensures that clients can navigate between projected entities as expected.

If there are two projections with the same ‘distance’ to the source, the algorithm to resolve the correct target may fail. For example, compiling the following model would result in this warning:

WARNING: Target “C” is exposed in service “S” by multiple projections “S.Cx”, “S.Dx” - no implicit redirection CDS (redirected-implicitly-ambiguous)

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

entity A : cuid { b: Association to B; c: Association to C; }
entity B : cuid { a: Association to A; }
entity C : cuid { a: Association to A; }

service S {
  entity Ax as projection on A;
  entity Bx as projection on B;
  entity Cx as projection on C;
  entity Dx as projection on C;
}

You can resolve this, by explicitly determining a redirection target in the projection of Ax unsing redirected to as follows:

...
service S {
  entity Ax as projection on A { *, c : redirected to Cx };
  ...
}

Auto-exposed Entities

Annotate entities with @cds.autoexpose to automatically include them in services containing entities with Association references to them.

For example, given the following entity definitions:

// schema.cds
using { sap.common.CodeList } from '@sap/cds/common';
entity Bar : CodeList { key code: Integer; }
entity Car @cds.autoexpose { key id: Integer; }

… a service definition like that:

using { Bar, Car } from './schema.cds';
service Zoo {
  entity Foo { //...
    bar : Association to Bar;
    car : Association to Car;
  }
}

… would result in the equivalent of the following unfolded service definition:

service Zoo {
  entity Foo { //...
    bar : Association to Zoo.Bar;
    car : Association to Zoo.Car;
  }
  entity Bar as projection on Bar;
  entity Car as projection on Car;
}

Learn more about CodeLists in @sap/cds/common

Handling Events

About Events and Event Handlers

As introduced in About CAP everything happening at runtime is in response to events, emphasizing the ubiquitous notion of events in CAP. Hence all service implementations take place in event handlers, as highlighted in the figure.

Events essentially comprise:

  • Synchronous Requests sent from UIs, frontends, or other services:
    • Common CRUD methods: CREATE, READ, UPDATE, UPSERT, DELETE
    • Common REST methods: POST, GET, PUT, PATCH, DELETE
    • Custom-defined actions and functions
  • Asynchronous Event Messsages received thru message brokers:

Event Handlers comprise:

So, lines are very much blurred, both with respect to synchronous and asynchronous protocols, as well as regarding generic handlers and custom handlers. In fact all generic features are essentially provided in event handlers, which are registered and implemented using the same APIs and libraries that you would use to implement your custom logic. Vice versa, you can implement your own generic handlers in the same way than we did that in CAP runtimes.

Use Cases for Custom Handlers

Most standard tasks and use cases are covered by generic handlers, so the need to implement event handlers in CAP-based projects, and hence the amount of individual boilerplate coding is greatly reduced and minified. Remaining cases which need custom handlers are:

  • Domain-specific programmatic Validations
  • Augmenting result sets, e.g. to add computed fields for frontends
  • Programmatic Authorization Enforcements
  • Triggering follow-up actions, e.g. calling other services or emitting outbound events in response to inbound events
  • etc…, in general, all the things not (yet) covered by generic handlers

Assigning Custom Implementation Classes/Modules

At runtime the CAP frameworks usually bootstrap and instantiate service provider instances automatically (→ see also options for project-specific bootsrapping in Node.js and Java). In order to register event handlers with a service provider instance, you first need to associate a respective class or module with a given service definition.

In Node.js, the easiest way to do so is by equally named .js files placed next to a service definition’s .cds file. Alternatively, you can add @impl: annotations to services, or provide implementation functions programmatically, for example:

service org.acme.Foo @(impl:'lib/foo.js') {...}
cds.serve('org.acme.Foo') .with (require('./lib/foo.js'))

In Java, you’d assign Event Handler Classes using Spring Dependency Injection with Java annotation @ServiceName as follows:

@Component
@ServiceName("org.acme.Foo")
public class FooServiceImpl implements EventHandler {...}

Registering Event Handlers

Given assigned implementation classes/modules, you can register individual event handlers for each potential event, on different hooks of the event processing cycle, for example:

const cds = require('@sap/cds')
module.exports = function (){
  this.on ('submitOrder', (req)=>{...}) //> custom actions
  this.on ('CREATE',`Books`, (req)=>{...})
  this.before ('UPDATE',`*`, (req)=>{...})
  this.after ('READ',`Books`, (each)=>{...})
}

Learn more about adding event handlers in Node.js

@Component
@ServiceName("BookshopService")
public class BookshopServiceImpl implements EventHandler {
  @On(event="submitOrder") public void onSubmitOrder (EventContext req) {...}
  @On(event="CREATE", entity="Books") public void onCreateBooks (EventContext req) {...}
  @Before(event="UPDATE", entity="*") public void onUpdate (EventContext req) {...}
  @After(event="READ", entity="Books") public void onReadBooks (EventContext req) {...}
}

Learn more about adding event handlers in Java

In general, event handlers are registered as follows:
  • <hook:on|before|after> , <events> , <entities> → handler function
  • <hook:on|before|after> , <events> → handler function
Hooks: on | before | after
  • on handlers can run instead of the generic/default handlers
  • before handlers run before the on handlers
  • after handlers run after the on handlers, and get the result set as input
Entity-bound vs unbound events
As apparent from these two variants, <events> can be bound to <entities>, as with the common CRUD or REST methods or unbound, as with custom-defined, service-level actions or functions. Entity-bound events are only triggered for requests directly targeting that entity.

For example, the following GET requests will only trigger the READ handler for MyEntity.

GET MyEntity
GET MyEntity?$expand=myNavigation

Consistently, a GET request like the following will only trigger the READ event handler for myNavigation’s target.

GET MyEntity(1)/myNavigation

Implementing Event Handlers…

Event handlers all get a uniform Request / Event Message context object as their primary argument, which — amongst other — provides access to these things:

  • the event name — i.e. a CRUD method name, or a custom-defined one
  • the target entity, if any
  • the query in CQN format, in case of CRUD requests
  • the data payload
  • the user, if identified / authenticated
  • the tenant using your SaaS application, if enabled

Learn more about implementing event handlers in Node.js Learn more about implementing event handlers in Java

Consuming other Services

Quite frequently event handler implementations send requests to other services — mostly in the form of queries constructed using the respective incarnations of cds.ql libraries. Note that from CAP’s perspective, databases are also just services, consumed in your project, as shown in these examples:

const db = await cds.connect.to('db), {Orders} = db.entities
this.on ('READ',`MyOrders`, async (req)=>{
  const tx = db.tx(req) //> ensure tenant isolation & transaction mgmt
  return tx.read (Orders) .where ({ user: req.user.id })
})

Learn more about consuming services in Node.js

import static bookshop.Bookshop_.MY_ORDERS;

@Autowired
PersistenceService db; // get access to the service

@On(event="READ", entity="MyOrders")
public Result onRead(EventContext req) {
  CqnSelect query = Select.from(MY_ORDERS).where(o -> o.user().eq(req.getUserInfo().getName()));
  return db.run(query);
}

Learn more about consuming services in Java

Emitting new Events

Event handlers may also emit new asynchronous events, as also showcased in cap/samples, here a simplified variant:

this.after (['CREATE','UPDATE','DELETE'], 'Reviews', async(_,req) => {
   const {subject,rating} = req.data
   this.emit ('reviewed', { subject, rating })
})

Learn more about emitting events in Node.js

Java: coming soon…

Using Model Reflection

As stated above, you can also implement generic custom handlers. In thise case you frequently need to reflect on related models yourself as shown in the samples below:

const m = this.model //> this = service instance
for (let each in this.entities) {
  const e = this.entities [each]
  console.log (`entity ${e.name} {`)
  for (let a of m.each (cds.Association, /*in:*/ e.elements))
    if (a.is2many)  console.log (`   ${a.name} : Association to many ${a._target.name};`)
  console.log (`}`)
}

Learn more about model reflection in Node.js

Java: coming soon…

Generic Handlers

Serving CRUD Requests

The Service SDKs for both, Node.js and Java, automatically add generic handlers to each application service, which handle all CRUD requests automatically under these conditions:

  • A default datasource is configured (for example, cds.env.requires.db in Node.js)
  • The exposed entity was not defined using UNION or JOIN

Auto-generated Primary Keys

  • On CREATE and UPSERT operations key elements of type UUID are filled in automatically. Additionally, on deep inserts and upserts respective foreign keys of nested objects are filled in accordingly.

Serving Deeply-Nested Documents

Structured documents represent the concept of relationships between entities, which can be modeled using compositions and associations. The runtime provides generic handlers covering basic CRUD requests for main (or parent) entities and their associated entities.

Deep READ

Use the $expand query parameter to read structured documents via OData. For example, this request:

GET .../Orders?$expand=header($expand=note)

… would return an array of nested structures as follows:

[{
    ID:1, title: 'first order', header: {
      ID:2, status: 'open', note: {
        ID:3, description: 'first order notes'
      }
    }
  },{
    ID:4, title: 'second order', header: {
      ID:5, status: 'payed', note: {
        ID:6, description: 'second order notes'
      }
    }
  },
  ...
]

Deep INSERT for Compositions

To create the parent entity and its child entities use a single POST request. That means that the body of the POST request must contain a complete set of data for all entities. On success, this request will create all entities and relate them. On failure, neither of the entities will be created.

Sample POST request with to-one composition:

POST .../Orders {
  ID:1, title: 'new order', header: {
    ID:2, status: 'open', note: {
      ID:3, description: 'child of child entity'
    }
  }
}

A POST request in case of to-many relationship:

POST .../Orders {
  ID:1, title: 'new order', header: {
    ID:2, status: 'open', notes: [{
      ID:3, description: 'child of child entity'
    },{
      ID:4,description: 'another child of child entity'
    }]
  }]
}

Deep UPDATE for Compositions

Similar to the deep INSERT case, the deep UPDATE of the structured document must be made with a single PUT or PATCH request to the parent entity. Depending on the request body, child entities will be created, updated, or deleted:

  • Child entities existing on the database but not in the payload will be deleted
  • Child entities existing on the database and in payload will be updated (specified fields overwrite the values on the database).
  • Child entities not existing on the database will be created.

Consider the POST request with to-one composition from above, the following request would update this Orders entry:

PUT .../Orders/1 {
  title: 'another order', header: {
    ID:4, status: 'canceled'
  }
}

The title property of the parent entity Orders will be changed, the child entities with the ID=2 and ID=3 will be deleted and a new child entity with the ID=4 will be created.

Deep DELETE for Compositions

Since child entities of compositions can’t exist without their parents, a DELETE of the parent entity is a cascading delete of all nested composition targets.

DELETE .../Orders/1

Would delete Orders, OrderHeaders, and SpecialNotes from the example above.

INSERT w/ Associations

Entities with association relationships must be created with separate POST requests. It means that, unlike in the case of Deep INSERT with compositions, the association targets (Authors in the example above) must be created before the parent entity.

Then you can send a request to create the entity Books with the body looking like Deep INSERT. The association to the existing entity Authors is created by providing the key of the entity.

POST .../Authors {
  ID:12, name: 'Charlotte Brontë'
}
POST .../Books {
  ID:121, title: 'Jane Eyre', author: { ID:12 }
}

A POST request with the body looking like Deep INSERT can be sent only for targets of managed to-one associations. In other cases, you can either send the foreign keys explicitly or implement your own custom handler.

Note, that unlike the case of composition there’s no Deep Insert/Update for associated entities, therefore all nonkey fields of author in the POST request on Books are ignored and their values aren’t changed.

UPDATE w/ Associations

UPDATE request of structured documents with association relationships can be made with a PATCH or PUT request to the parent entity. You can use it to update the parent entity’s data or to change references to association targets. The restrictions of the INSERT operation are valid for the UPDATE case as well.

DELETE w/ Associations

A DELETE operation is forbidden in case managed to-one association exists with the foreign key pointing to the entity you want to delete.

Consider the example:

entity Books {
  key ID : Integer;
  orders : Association to many Orders on orders.book = $self;
}
entity Orders {
  book : Association to Books;
}

The DELETE Request:

DELETE .../Books(ID=3)

will fail if the entity Orders has a foreign key book_ID=3. In all other cases the DELETE operation will be successful, but unlike Deep DELETE in composition case, all association targets won’t be affected by it.

Programmatic Usage w/ Node.js

Similar to the HTTP request-based examples above you can as well construct queries programmatically and send them to connected or local services for execution. Find just a few examples for that below…

Deep READ:

const srv = cds.connect.to ('<some queryable service>')
const orders = srv.run (SELECT.from ('Orders', o => { // projection
  o('*'), o.header (h => { // nested projection => 1st $expand
    h('*'), h.note('*') // nested projection => 2nd $expand
  })
}))

Alternatively, use srv.read() as a shortcut to srv.run (SELECT.from(...)):

const srv = cds.connect.to ('<some queryable service>')
const orders = await srv.read ('Orders', o => { // projection
  o('*'), o.header (h => { // nested projection => 1st $expand
    h('*'), h.note('*') // nested projection => 2nd $expand
  })
})

See Node.js API Reference to learn more about Node.js API

Deep INSERT:

const srv = cds.connect.to ('<some queryable service>')
srv.run (INSERT.into ('Orders').entries ({
   ID:1, title: 'new order', header: {
     ID:2, status: 'open', note: {
       ID:3, description: 'child of child entity'
     }
   }
}))

Alternatively, use srv.create() as a shortcut to srv.run (INSERT.from(...)):

const srv = cds.connect.to ('<some queryable service>')
srv.create ('Orders') .entries ({
   ID:1, title: 'new order', header: {
     ID:2, status: 'open', note: {
       ID:3, description: 'child of child entity'
     }
   }
})

See Node.js API Reference to learn more about Node.js API

Serving / Streaming Media Data

The following annotations can be used in the service model to indicate that an element in an entity contains media data.

@Core.MediaType
Indicates the element contains media data (directly or using redirect). The value of this annotation either is a string with the contained MIME type (as shown in the first example), or is a path to the element that contains the MIME type (as shown in the second example).
@Core.IsMediaType
Indicates the element contains a MIME type. The @Core.MediaType annotation of another element can reference this element.
@Core.IsURL @Core.MediaType
Indicates the element contains a URL pointing to the media data (redirect scenario).

The following examples show these annotations in action:

  1. Media data is stored in database with fixed media type image/png:
    entity Books { //...
      image : LargeBinary @Core.MediaType: 'image/png';
    }
    
  2. Media data is stored in database with variable media type:
    entity Books { //...
      image : LargeBinary @Core.MediaType: imageType;
      imageType : String  @Core.IsMediaType;
    }
    
  3. Media data is stored in external repository:
    entity Books { //...
      imageUrl  : String @Core.IsURL @Core.MediaType: imageType;
      imageType : String @Core.IsMediaType;
    }
    

Reading Media Resources

Read media data using GET requests of the form /Entity(<ID>)/mediaProperty.

GET ../Books(201)/image
> Content-Type: application/octet-stream

The response’s Content-Type header is typically application/octet-stream.
The media data will be streamed automatically.

Creating Media Resource

As a first step an entity without media data to be created using a POST request to the entity. After creating the entity, a media property can be inserted using the PUT method. The MIME type is passed in the Content-Type header. The following are sample requests:

POST ../Books
Content-Type: application/json
{ <JSON> }
PUT ../Books(201)/image
Content-Type: image/png
<MEDIA>

The media data will be streamed automatically

Updating Media Resources

The media data for an entity can be updated using the PUT method:

PUT ../Books(201)/image
Content-Type: image/png
<MEDIA>

The media data will be streamed automatically

Deleting Media Resources

One option is to delete the complete entity, including all media data:

DELETE ../Books(201)

Alternatively, you can delete a media data element individualy:

DELETE ../Books(201)/image

Reading External Media Resources

The following are request and response for the entity containing redirected media data from Example 3 above.

This format is used by OData-Version: 4.0. To be changed in OData-Version: 4.01.

GET: ../Books(201)
>{ ...
    image@odata.mediaReadLink: "http://other-server/image.jpeg",
    image@odata.mediaContentType: "image/jpeg",
    imageType: "image/jpeg"
}

Implicit Pagination

By default, the generic handlers for READ requests automatically truncate result sets to a size of 1,000 records max. If there are more entries available, a link is added to the response allowing clients to fetch the next page of records.

The OData response body for tuncated result sets contains a nextLink as follows:

GET .../Books
>{
  value: [
    {... first record ...},
    {... second record ...},
    ...
  ],
  @odata.nextLink: "Books?$skiptoken=1000"
}

To retrieve the next page of records from the server, the client would use this nextLink in a follow-up request like so:

GET .../Books?$skiptoken=1000

On firing this query, you get the second set of 1,000 records with a link to the next page, and so on, until the last page is returned, with the response not containing a next link.

You can configure default and maximum limits either globally via the app environment or by applying the annotation @cds.query.limit on service or on entity level.

  • The maximum limit defines the maximum number of items that can get retrieved, regardless of $top.
  • The default limit defines the number of items that are retrieved if no $top was specified.

The two limits can be specified as follows:

Environment:

query: {
  limit: {
    default: 20, //> no default
    max: 100     //> default 1,000
  }
}

@cds.query.limit:

@cds.query.limit: { default?, max? } | Number

The limit definitions for CatalogService and AdminService in the following example are equivalent.

@cds.query.limit.default: 20
@cds.query.limit.max: 100
service CatalogService {
  [...]
}
@cds.query.limit: { default: 20, max: 100 }
service AdminService {
  [...]
}

@cds.query.limit can be used as shorthand if no maximum limit needs to be specified at the same level.

@cds.query.limit: 100
service CatalogService {
  entity Books as projection on my.Books;     //> pages at 100
  @cds.query.limit: 20
  entity Authors as projection on my.Authors; //> pages at 20
}
service AdminService {
  entity Books as projection on my.Books;     //> pages at 1000 (default)
}

The closest limit applies, i.e., an entity-level limit overrides that of its service, and a service-level limit overrides the global setting. The value 0 disables the respective limit at the respective level.

@cds.query.limit.default: 20
service CatalogService {
  @cds.query.limit.max: 100
  entity Books as projection on my.Books;     //> default = 20 (from CatalogService), max = 100
  @cds.query.limit: 0
  entity Authors as projection on my.Authors; //> no default, max = 1,000 (from environment)
}

Implicit Sorting

Paging requires implied sorting, otherwise records might be skipped accidentally when reading follow-up pages. By default the entity’s primary key is used as sort criterion.

For example, given an service definition like that:

service CatalogService {
  entity Books as projection on my.Books;
}

The SQL query executed in response to incoming requests to Books will be enhanced with an additional where clause as follows:

SELECT ... from my_Books
ORDER BY ID; -- default: order by the entity's primary key

If the request specifies a sort order, for example, GET .../Books?$orderby=author, both are applied like that:

SELECT ... from my_Books ORDER BY
  author,     -- request-specific order has precedence
  ID;         -- default order still applied in addition

We can also define a default order when serving books as follows:

service CatalogService {
  entity Books as projection on my.Books order by title asc;
}

Now, the resulting order by clauses are as follows for GET .../Books:

SELECT ... from my_Books ORDER BY
  title asc,  -- from entity definition
  ID;         -- default order still applied in addition

… and for GET .../Books?$orderby=author:

SELECT ... from my_Books ORDER BY
  author,     -- request-specific order has precedence
  title asc,  -- from entity definition
  ID;         -- default order still applied in addition

Managed Data

Use the annotations @cds.on.insert and @cds.on.update to signify elements to be auto-filled by the generic handlers. For example you could add fields to track who created and updated data records and when as follows:

entity Foo { //...
   createdAt  : Timestamp @cds.on.insert: $now;
   createdBy  : User      @cds.on.insert: $user;
   modifiedAt : Timestamp @cds.on.insert: $now  @cds.on.update: $now;
   modifiedBy : User      @cds.on.insert: $user @cds.on.update: $user;
}

Equivalent definition, using pre-defined aspect managed from @sap/cds/common:

using { managed } from '@sap/cds/common';
entity Foo : managed {/* ... */}

These rules apply:

  • Data is auto-filled, that is, data is ignored if provided in the request payload.
  • Data can be filled with initial data, for example, through .csv files.
  • Data can be set explicitly in custom handlers. For example:
    Foo.modifiedBy = req.user.id
    Foo.modifiedAt = new Date()
    

In effect, values for these elements are handled fully automatically and are write-protected for external service clients.

The pseudo variables used in the annotations are resolved as follows:

  • $now is replaced by the current server time (in UTC)
  • $user is the current user’s ID as obtained from the authentication middleware

Learn more about Authentication in Node.js Learn more about Authentication in Java

Note the differences to defaults, for example, given this model:

entity Foo { //...
  managed   : Timestamp @cds.on.insert: $now;
  defaulted : Timestamp default $now;
}

While both behave identical for INSERTs on database-level operations, they differ for CREATE requests on higher-level service providers: Values for managed in the request payload will be ignored, while provided values for defaulted will be written to the database.

Input Validation

@readonly Fields

Elements annotated with @readonly as well as virtual elements and calculated fields are protected against write operations. That is, if a CREATE or UPDATE operation specifies values for such fields, these values are silently ignored.

Same applies for fields with the OData Annotations @FieldControl.ReadOnly, @Core.Computed, or @Core.Immutable (the latter only on UPDATEs).

@mandatory Fields

Elements marked with @mandatory are checked for nonempty input: null as well as (trimmed) empty strings are rejected.

Same applies for fields with the OData Annotation @FieldControl.Mandatory.

@assert.unique Constraints

Annotate an entity with @assert.unique, specifying one or more element combinations to enforce uniqueness checks on all CREATE and UPDATE operations. For example:

@assert.unique: {
  locale: [ parent, locale ],
  timeslice: [ parent, validFrom ],
}
entity LocalizedTemporalData {
  key record_ID : UUID; // technical primary key
  parent    : Association to Data;
  locale    : String;
  validFrom : Date;  validTo : Date;
}

In essence, key elements constitute special cases of @assert.unique constraints.

@assert.integrity Constraint for to-one Associations

All Association to one are automatically checked for referential integrity, that is:

  • CREATEs and UPDATEs are rejected if a reference’s target doesn’t exist
  • DELETEs are rejected if it would result into dangling references
  • … except for associations annotated with @assert.integrity:false
-- Equivalent SQL DDL statement:
CREATE TABLE Books ( -- elements ...
  CONSTRAINT FK_author FOREIGN KEY (author_ID) REFERENCES Authors (ID)
)

@assert.format Pattern Check Constraints

Allows to specify a regular expression string (in ECMA 262 format in CAP Node.js and java.util.regex.Pattern format in CAP Java) which all string input must match.

entity Foo {
  bar : String @assert.format: '[a-z]ear';
}

@assert.range Check Constraints

Allows specifying [ min, max ] ranges for elements with ordinal types — that is, numeric or date/time types. For enum elements, true can be specified to restrict all input to the defined enum values.

entity Foo {
  bar : Integer  @assert.range: [ 0, 3 ];
  boo : Decimal  @assert.range: [ 2.1, 10.25 ];
  car : Datetime @assert.range: ['2018-10-31', '2019-01-15'];
  zoo : String   @assert.range enum { high; medium; low; };
}

Specified ranges are interpreted as right-open intervals, that is, the performed checks are min ≤ input ≤ max.

@assert.notNull

Annotate a property with @assert.notNull: false to have it ignored during the generic not null check, for example if your persistence fills it automatically.

entity Foo {
  bar : String not null @assert.notNull: false;
}

Node.js runtime only.

Concurrency Control

Conflict Detection using ETags

The CAP runtimes support optimistic concurrency control and caching techniques using ETags. An ETag identifies a specific version of a resource found at a URL.

Enable it by adding the @odata.etag annotation to an element to be used to calculate an ETag value as follows:

using { managed } from '@sap/cds/common';
entity Foo : managed {...}
annotate Foo with { modifiedAt @odata.etag } 

The value of an ETag element should uniquely change with each update per row. The modifiedAt element from the pre-defined managed aspect is a good candidate, as this is automatically updated. You could also use update counters or UUIDs which are recalculated on each update.

You use ETags when updating, deleting, or invoking the action bound to an entity by using the ETag value in an If-Match or If-None-Match header. The following examples represent typical requests and responses:

POST Employees { ID:111, name:'Name' }
> 201 Created {'@odata.etag': 'W/"2000-01-01T01:10:10.100Z"',...}
//> Got new ETag to be used for subsequent requests...
GET Employees/111
If-None-Match: "2000-01-01T01:10:10.100Z"
> 304 Not Modified // Record was not changed
GET Employees/111
If-Match: "2000-01-01T01:10:10.100Z"
> 412 Precondition Failed // Record was changed by another user
UPDATE Employees/111
If-Match: "2000-01-01T01:10:10.100Z"
> 200 Ok {'@odata.etag': 'W/"2000-02-02T02:20:20.200Z"',...} 
//> Got new ETag to be used for subsequent requests...
UPDATE Employees/111
If-Match: "2000-02-02T02:20:20.200Z"
> 412 Precondition Failed // Record was modified by another user 
DELETE Employees/111
If-Match: "2000-02-02T02:20:20.200Z"
> 412 Precondition Failed // Record was modified by another user 

SELECT for Update

If you want to read and update the retrieved data within the same transaction, you need to make sure that the obtained data isn’t modified by another transaction at the same time.

The SELECT FOR UPDATE statement allows you to lock the selected records so that other transactions are blocked from changing the records in any way.

The records are locked until the end of transaction by commit or rollback statement.

Learn more about usage of SELECT FOR UPDATE statement in Node.js runtime

Protocols & APIs

Omitting Elements from APIs

Add annotation @cds.api.ignore to suppress unwanted entity fields (for example, foreign-key fields) in APIs exposed from this the CDS model, that is, OData or Open API. For example:

entity Books { ...
  @cds.api.ignore 
  author : Association to Authors;
}

OData (Open Data Protocol)

OData is an OASIS standard, which essentially enhances plain REST by standardized query options like $select, $expand, $filter, etc. CAP provides extensive support for OData.

Learn more in the OData Support guide

Best Practices

Prefer Single-purposed Services

We strongly recommend following to design your services to be tailored for single use cases. Services in CAP are cheap, so there’s no need to save on them.

DON’T:   Single Services exposing all entities 1:1

The anti-pattern to that are single services exposing all underlying entities in your app in a 1:1 fashion. While that may save you some thoughts in the beginning, likelihood is high, it result. in lots of headaches in the long run:

  • They open a huge entry door to your clients with only few restrictions
  • Individual use-cases aren’t reflected in your API design
  • You have to add numerous checks on a per-request basis…
  • Which have to reflect on the actual use cases in complex and expensive evaluations

DO:   One Service per Use Case

For example, assumed that we got a domain model defining Books and Authors more or less as above, and adds Orders, we could define the following services:

using { my.domain as my } from './db/schema';
/** Serves end users browsing books and place orders */
service CatalogService {
  @readonly entity Books as select from my.Books {
    ID, title, author.name as author
  };
  @requires: 'authenticated-user'
  @insertonly entity Orders as projection on my.Orders;
}
/** Serves registered users managing their account and their orders */
service UsersService {
  @readonly entity Orders as projection on my.Orders
    where buyer = $user; // limit to own ones
  action cancelOrder ( ID:Orders.ID, reason:String );
}
/** Serves administrators managing everything */
@requires: 'authenticated-user'
service AdminService {
  entity Books   as projection on my.Books;
  entity Authors as projection on my.Authors;
  entity Orders  as projection on my.Orders;
}

These services serve different use cases and are tailored for each. Note, for example, that we intentionally don’t expose entity Authors to end users.