Search

Using Generic Providers

CAP provides generic service provider implementations, which automatically serve metadata as well as most CRUD requests out of the box, including standard input validations and more.

Content

Serving CRUD Requests

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

Serving Structured Data

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.

Compositions

Compositions model a self-contained relationship between a parent entity and composition targets (child entities), that is, a target of a composition cannot exist without its parent entity.

Sample to-one composition:

//Parent entity
entity Orders {
  key ID : Integer;
  title: String;
  header  : Composition of OrderHeaders;
}

//Child entity
entity OrderHeaders {
  key ID : Integer;
  status: String;
  note  : Composition of SpecialNotes;
}

//Child of child entity
entity SpecialNotes {
  key ID : Integer;
  description: String;
}

see CDS Language Reference to learn more about compositions see Domain Modelling to learn more about advanced modelling

DEEP READ

No specialized behavior. Use the $expand query parameter to read structured documents in OData. To read all data from the example above you can use nested $expand:

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

[
  {
    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 serviceName/Orders
data: {
  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 serviceName/Orders
data: {
  ID: 1,
  title: 'new order',
  header: [
    {
    ID: 2,
    status: 'open',
    note: [
      {
      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 PATCH or PUT request to the parent entity. Depending on the body of the DEEP UPDATE request a child entity can be updated, created or deleted:

If the child entity with the specified key…

All child entities not specified in the request are deleted.

Consider the POST request with to-one composition from above. With the following request

PUT serviceName/Orders(ID=1)
data: {
  ID: 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 cannot exist without their parents, a delete of the parent entity is a cascading delete of all composition targets to level n.

Sample DELETE Request:

DELETE serviceName/Orders(ID=1)

This request deletes entity Orders, OrderHeaders and SpecialNotes from the example above.

Associations

Associations are similar to forward-declared joins with either explicitly (unmanaged) or implicitly (managed) specified join conditions.

Sample managed to-one association:

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

see CDS Language Reference to learn more about associations see Domain Modelling to learn more about advanced modelling

READ

No specialized behavior. Use the $expand query parameter to read structured documents in OData.

INSERT

Entities with the association relationships must be created with separate POST requests. It means that, unlike in DEEP INSERT case with compositions, the association targets (Authors in the example above) must be created before parent entity. Then you can send a request to create the entity Books with the body looking like DEEP INSERT. Note, that only entity keys are accepted from the association target in the request body.

POST serviceName/Authors
data: {
  ID: 12,
  name: 'Charlotte Brontë'
}

POST serviceName/Books
data: {
  ID: 121,
  title: 'Jane Eyre',
  author: {
  ID: 12
  }
}

The second POST request will create the entity Books and add a relationship to the existing entity Authors. The same can be achieved by writing the foreign keys explicitly:

POST serviceName/Books
data: {
  ID: 121,
  title: 'Jane Eyre',
  author_ID: 12
}

A POST request with the body looking like DEEP INSERT can be send 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.

UPDATE

The UPDATE of the 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

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 serviceName/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 will not be affected by it.

Node.js API

An example of DEEP INSERT using Node.js API looks as follows:

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


Alternative you can use srv.create to create parent entity Orders with composition targets OrderHeaders and SpecialNotes

const srv = cds.connect.to ('serviceName')
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

Access checks on roots only

The @readonly and @insertonly annotations as well as OData’s @Capabilities.Insert/Update/DeleteRestrictions annotations provide restrictions only on the top level.

Consider the example:

namespace my;
entity Books {
  key ID : Integer;
  title : String(5000);
  author : Association to Authors;
}

entity Authors {
  key ID : Integer;
  name : String(5000);
}

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

Then the response payload from the following GET request tries to read data from Authors which is protected with @insertonly:

GET CatalogService/Books?$expand=author

[
  { ID:111, title:'Wuthering Heights', author:{
    ID:11, name:'Emily Brontë'
  },
  { ID:121, title:'Jane Eyre', author:{
    ID:12, name:'Charlotte Brontë'
  },
  ...
]

see CRUD Constraint Checks to learn more about annotations

Serving Media Data and Streaming

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

@Core.MediaType

The presence of this annotation indicates that the annotated 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

The presence of this annotation indicates that the annotated element contains a MIME type. The @Core.MediaType annotation of another element can reference this element.

Note: When provisioning an OData V2 service, there can only be one media element per entity.

@Core.IsURL

The presence of this annotation indicates that the annotated element contains a URL pointing to the media data (redirect scenario).

Examples

The following section contains three examples.

  1. Media data is stored locally in col1 and MIME type is image/png:

     entity MyMimeEntity {
       key id : UUID;
       @Core.MediaType: 'image/png'
       col1 : LargeBinary;
     }
    
  2. Media data is stored locally in col1 and MIME type is a variable stored in col2:

     entity MyMimeEntity {
       key id : UUID;
       @Core.MediaType: col2
       col1 : LargeBinary;
       @Core.IsMediaType : true
       col2 : String;
     }
    
  3. A URL pointing to the media data is stored in col1 and MIME type is stored in col2:

     entity MyMimeEntity {
       key id : UUID;
       @Core.MediaType: col2
       @Core.IsURL: true
       col1 : String;
       @Core.IsMediaType : true
       col2 : String;
     }
    

Operations on Media Resources

Read Media Resource (OData v2)

Media stream data can be obtained using a request of the form GET /EntityName(<ID>)/$value. The media type is returned in the Content-Type response header. The following is a sample request URL:

GET: https://host/odata/v2/MediaService/Books(guid'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx')/$value

Read Media Resource (OData v4)

Media stream data can be obtained using a request of the form GET /EntityName(<ID>)/mediaProperty. The media type returned in the Content-Type response header is typically ‘application/octet-stream’. The following is a sample request URL:

GET: https://host/odata/v4/MediaService/Books(guid'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx')/coverImage

Create Media Resource (OData v2)

Media data can be created using a POST call to the entity with the entity key passed as a slug header. A new entity instance will be created with this key along with the media content. After creating the media resource, you can update the non-media properties, for example, Name, using the PUT method. The MIME type is passed in the Content-Type header. The following is a sample request:

POST: https://host/odata/v2/MediaService/Books

Request Headers:
slug: guid'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
Content-Type: image/png

Request Body : <MEDIA>

Create Media Resource (OData v4)

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: https://host/odata/v4/MediaService/Books

Request Headers:
Content-Type: application/json

Request Body : <JSON>
PUT: https://host/odata/v4/MediaService/Books(guid'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx')/coverImage

Request Headers:
Content-Type: image/png

Request Body : <MEDIA>

Update Media Resource (OData v2)

The media data for an entity can only be updated after it is created. The MIME type is passed in the Content-Type header. The following is a sample request:

PUT: https://host/odata/v2/MediaService/Books(guid'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx')/$value

Request Headers:
Content-Type: image/jpeg

Request Body : <MEDIA>

Update Media Resource (OData v4)

The media data for an entity can be updated using the PUT method. The following is a sample request:

PUT: https://host/odata/v4/MediaService/Books(guid'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx')/coverImage

Request Headers:
Content-Type: image/png

Request Body : <MEDIA>

Delete Media Resource (OData v2)

The following is a sample request:

DELETE: https://host/odata/v2/MediaService/Books(guid'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx')/$value

Delete Media Resource (OData v4)

One option is to delete the complete entity. The following is a sample request:

DELETE: https://host/odata/v4/MediaService/Books(guid'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx')

It is also possible to delete the stream property (set it to null). The following is a sample request:

DELETE: https://host/odata/v4/MediaService/Books(guid'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx')/coverImage

Request Redirected Media Resource (OData v4)

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

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

GET: https://host/odata/v4/MediaService/MyMimeEntity(guid'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx')

Response:
{
  "ID": uuid,
  "col1@odata.mediaReadLink": "http://other-server/image.jpeg",
  "col1@odata.mediaContentType": "image/jpeg",
  "col2": "image/jpeg"
}

Implicit Pagination

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

Without this implicit pagination, a simple search could return millions or even billions of hits causing extraneous network traffic and denial of service attacks.

Basic Example

Assumed a request like that would match more than 1000 records:

GET http://my.bookshop.com/catalog/Books

Then the OData response body would contain a nextLink as follows:

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

Follow-Up Requests

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

GET http://my.bookshop.com/catalog/Books?$skiptoken=1000

On firing this query, you get the second set of 1000 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 the Default Limit

You can change this default server-side limit by applying the annotation @cds.query.limit on service level or on entity level as follows:

@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.Books;  //> pages at 20
}
service AdminService {
  entity Books as projection on my.Books;    //> pages at 1000 (default)
}

Implicit Sorting

Paging requires an implied sorting, otherwise we might be skipping records accidentally when reading follow-up pages. By default the entity’s primary key is used for that.

Basic Example

Given an entity definition and a service definition like that:

entity my.Books { key ID : UUID; ... }
service CatalogService {
  entity Books as projection on my.Books;
}

On incoming requests to Books, the resulting database SELECT operation 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

With Order by in Request

If the incoming request contains a sort order query option, for example, GET .../Books?$orderby=title asc both are applied like that:

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

With Order by in Entity Definition

Now, assumed we might want to define a default order when serving books as follows:

service CatalogService {
  entity Books as select from my.Books order by title asc;
}

Remark: Since currently does not allow clauses (should become available soon), we have to deviate to as select from.

Now, the resulting orders 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

For GET .../Books?$orderby=author asc:

SELECT ... from my_Books ORDER BY
  author asc, -- 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 {
  key ID : UUID;
  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;
  ...
}

Annotations @cds.on.insert/update

The annotations are evaluated by generic handlers of REST and OData services as follows:

@cds.on.insert element values will be filled in upon CREATE operations
@cds.on.update element values will be filled in upon UPDATE operations

These rules apply:

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

Difference to Default Values

Note the differences to default values given this example:

entity Foo {
  key ID : UUID;
  managed     : Timestamp @cds.on.insert: $now;
  withDefault : Timestamp default $now;
  ...
}
  managed with default
CREATE request always filled in filled in, if no value in payload
Database INSERT filled in, if no value provided filled in, if no value provided
Value in request payload ignored(!) inserted / updated

Pseudo Variables $now and $user

The specified values use these pseudo-variables:

Using Pre-Defined Aspect managed

@sap/cds/common provides the pre-defined aspect managed, which you can use as a standard set of elements. You can achieve what was defined above:

entity Foo {
  key ID : UUID;
  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;
  ...
}

by simply using the aspect managed instead:

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

Access Control

@readonly, @insertonly

OData @Capabilities...

The following capabilities are supported:

The default value of all capabilities is true. Their combination with @readonly or @insertonly annotations isn’t supported.

Example:

service someService {
  @Capabilities: { Insertable:true, Updatable:true, Deletable:false }
  entity someEntity;
}

Input Validation

[Legend]

Node.js / Java Description
/   is available in Node.js and in Java
/   is available in Node.js
/   is available in Java
/   is not available

Uniqueness Constraints

/   for keys

/   for unique fields

/   for @unique constraints

Unsure what the symbols mean? Have a look at the legend.

Referential Integrity

/   for Associations – Association to one ...

/   for Code Lists – @ValueList.entity:

Read-only Fields

/   virtual fields

Example:

entity Book {
  virtual Author : String;
}

/   calculated fields

Unsure what the symbols mean? Have a look at the legend.

/   OData @Core.Computed

Example:

entity Book {
  Author : String @Core.Computed;
}

/   OData @Core.Immutable

Example:

entity Book {
  Author : String @Core.Immutable;
}

/   OData @FieldControl.ReadOnly

The following notations are supported:

Example:

entity Book {
  Author : String @FieldControl.ReadOnly;
  Title : String @Common.FieldControl.ReadOnly;
  Pages : Integer @Common.FieldControl: #ReadOnly;
}

Mandatory Input

/   @mandatory fields

/   not null fields

/   OData @FieldControl.Mandatory

Unsure what the symbols mean? Have a look at the legend.

Value Ranges

All value ranges annotations are currently supported only with REST. The value x of the @assert.range annotated element should be a ≤ x ≤ b, where @assert.range: [a,b]. The value of the @assert.format annotated element should be a regular expression (ECMA 262) in a String format.

/   Enum Ranges

If an Enum is annotated with @assert.enum, then the input value is checked against defined Enum values. Otherwise, any input value is allowed. Example:

entity someEntity {
@assert.enum
  EnumString : String enum {
        value1 = 'valueA';
        value2 = 'valueB';
        value3 = 'valueC';
  };
}

/   Date Ranges – @assert.range:

Example:

entity someEntity {
  rangeDate: Date @assert.range: ['2018-10-31', '2019-01-15'];
}

/   Number Ranges – @assert.range:

Example:

entity someEntity {
 rangeInteger: Integer @assert.range: [0, 3];
 rangeDecimal: Decimal(5, 2) @assert.range: [2.1, 10.25];
}

/   String Patterns – @assert.format:

Example:

entity someEntity {
  formatString: String(100) @assert.format: '[a-z]ear';
}

Concurrency Control

ETag

You can enable optimistic concurrency control using ETags.

An ETag represents a specific version of a resource found at a URL. You can provide optimistic concurrency control 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.

To enable ETag for the property of an entity, apply the @odata.etag annotation to it in your CDS data model.

Learn more about usage of ETag in Node.js runtime

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