Search

Using Generic Providers

CAP provides manifold generic handlers for common, recurring tasks which serve many requests out-of-the-box, thereby capturing proven best practices collected from a wealth of successful SAP applications.

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 wasn’t 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 Structured 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 using $expand

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 with 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 with 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 with 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 here:

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 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 that 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 that the element contains a MIME type. The @Core.MediaType annotation of another element can reference this element.
@Core.IsURL @Core.MediaType
Indicates that 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 individually:

DELETE ../Books(201)/image

Reading External Media Resources

The following are requests and responses for the entity containing redirected media data from the third example, “Media data is stored in external repository”.

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 truncated 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.

Configuring Defaults with cds.query.limit

You can configure default and maximum limits in the app environment as follows.

  • 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
  }
}

Annotation @cds.query.limit

You can override the defaults by applying the annotation @cds.query.limit on service or on entity level as follows:

@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)
}

Precedence

The closest limit applies, that means, 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 a 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 {/* ... */}

Generic Handlers for Managed Data

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 automatically and are write-protected for external service clients.

Pseudo Variables $user and $now

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)
)

Note
This feature is currently only available on the CAP Node.js stack.

@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 closed intervals, that means, 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;
}

Note
This feature is currently only available for the CAP Node.js runtime.

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.

Show/Hide Beta Features