Search

Using Generic Providers

Define, implement, deploy, and publish services to be consumed from other applications, services and UIs.

Content

Generic Providers

We provide generic service providers, which automatically serve metadata as well as most CRUD requests out of the box. With that in place, the above service definition is all you need to run a server, for example, using the Node.js runtime locally, simple start it in your shell:

cds run

This uses a simple default server and constructs service providers according to the provided models. As no data providers are added so far, the results of payload requests are always empty, of course.

learn more about running services

The generic providers cover features as documented below

Automatic CRUD handlers

learn more about using databases

Automatic Input Validation

see input validations

Auto-exposed Entities

Composition targets will automatically be exposed in services.

Add annotation cds.autoexpose to entities, which shall be automatically exposed through services whenever there is an association referring to them from other entities exposed by this service.

@cds.autoexpose
entity Books {
  key ID : UUID;
  title : String;
  author : Association to Authors;
}

Auto-exposed entities can be accessed by the navigation path like

GET /Root/navigation/to/books

Custom handlers can be defined either by the entity directly or using the navigation path

srv.on ('READ','Books', () => {
   ...
})
srv.on ('READ','/Root/navigation/to/books', () => {
   ...
})

Suppress Fields

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 {
  key ID : UUID;
  title : String;
  @cds.api.ignore
  author : Association to Authors;
}

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.

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.

Security

The @insertOnly and @readonly annotations as well as Insertable, Updatable and Deletable annotations provide restrictions only on the top level.

Consider the example:

namespace my.bookshop;
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.bookshop.Books;
  entity Authors @insertonly as projection on my.bookshop.Authors;
}

Then the response payload from the following GET request contains the protected with @insertonly data from Authors:

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

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

Computed Properties

You can specify that the property of an entity is computed in your CDS service model.

In other words, you do not have to provide the data for the property in the payload. All you have to do is apply @Core.Computed annotation to the property. This annotation ensures that the value of the property in the payload (if any) is ignored and the calculated value of the property from your custom handler is persisted in the HANA database.

Administrative Data

In the CDS data model that you define, you can specify that the property of an entity contains administrative data.

Administrative data refers to information such as database username and timestamp with which you can track who is inserting or updating entity data in the SAP HANA database and when the operation is performed. You can specify that a property contains administrative data using the following annotations:

CDS Annotation Description
@cds.on.insert: #now A property with this annotation captures when the entity was created in the SAP HANA database. This information is obtained from the function CURRENT_TIMESTAMP.
@cds.on.insert: #user A property with this annotation captures the information of the user creating the entity in the HANA database. User information is obtained from the implementation of UserContextParams. If this implementation is not available then the function CURRENT_USER populates the database username in this field.
@cds.on.update: #now A property with this annotation captures when the entity was updated in the SAP HANA database. This information is obtained from the function CURRENT_TIMESTAMP.
@cds.on.update: #user A property with this annotation captures the information of the user updating the entity in the HANA database. User information is obtained from the implementation of UserContextParams. If this implementation is not available then the function CURRENT_USER populates the database username in this field.

When creating or updating an entity in the database, any data that you provide for such properties (containing administrative data) in the payload are overwritten with values provided by the database.

The following sample code shows how you can define administrative data in the data model:

admin-data.cds

abstract entity adminData{
  @odata.on.insert: #now
  CreatedAt : Timestamp;

  @odata.on.insert: #user
  CreatedBy : String(128);

  @odata.on.update: #now
  ChangedAt : Timestamp;

  @odata.on.update: #user
  ChangedBy : String(128);
}

Note: If both @core.computed and @cds.on.insert annotations are applied to a property, then @cds.on.insert annotation trumps the @core.computed annotation. In other words, the administrative data gets persisted to the database in this scenario.

Analytics

You can enable analytics for the application you create using the SAP Cloud Application Programming Model.

Aggregation

To enable aggregation capability for your OData V2 service, specify which entities in your service model are aggregate entities (entities for which you can execute aggregation queries). Next, you specify which properties within these entities constitute the measures and the corresponding aggregation functions.

Let’s take a look at the following sample code:

analytics.cds

service CatalogService {
  @Aggregation.ApplySupported.PropertyRestrictions: true
  entity Books @readonly as projection on bookshop.Books{
  	ID,
  	title,
  	author,

  	@Analytics.Measure: true
  	@Aggregation.default: #SUM
  	stock
  };
}

The annotation @Aggregation.ApplySupported.PropertyRestrictions: true applied on the Books entity indicate that it is an aggregated entity. The @Analytics.Measure: true annotation indicates that stock is the property to be aggregated. Whereas, the annotation @Aggregation.default: #SUM indicates that the stock property is aggregated as a sum.

You can use the following aggregations: #SUM, #MAX, #MIN, #AVG, #COUNT_DISTINCT

Limitations

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 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. The following sample code shows how you can apply the annotation:

etag.cds

entity allDataTypeModel{
  key ID: UUID;

  @odata.etag
  externalID:UUID;
}

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

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

Example 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;
}

Example 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;
}

Example 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"
}

Pagination

You can set the maximum number of records per page when returning the result set of your query.

By default, the page size or the maximum number of records that can be returned at a time by the server is 1000. This means that if your query is supposed to return more than 1000 records, the result set will contain the first 1000 records and a link to retrieve the next page of records. The following diagram shows how this server-side pagination works:

You can change this default server-side limit by applying the annotation @cds.query.limit at the service level. Since the annotation is applied at the service level, this limit is applicable for all entities defined in the service.

For example, the following sample code shows how you can apply a page size of 100 records for the CatalogService:

pagination.cds

using my.bookshop from '../db/data-model';
@cds.query.limit: 100
service CatalogService {
   entity Books @readonly as projection on bookshop.Books;
   entity Authors @readonly as projection on bookshop.Authors;
   entity Orders @insertonly as projection on bookshop.Orders;
}

In this example, let’s say you fire the following query that targets 10,000 books stored in your database:

https://host/odata/v2/CatalogService/Books

The initial result set will contain 100 records and a link to the next page of records. This link might look like the following:

<link href="Books?$skiptoken=100" rel="next">

To retrieve the next page of records from the server, you must use the next link. In this example, the URL for the next call might look like the following:

https://host/odata/v2/CatalogService/Books?$skiptoken=100

On firing this query, you get the second set of 100 records with a link to the next page and so on.

Search Capability

You can provide the capability for anyone to search your service, using plain text, to get a list of matching entity instances.

Implementation

In order to make an entity and its properties searchable:

  1. Apply the @Capabilities.SearchRestrictions.Searchable: true annotation to the entity in your CDS service model.
  2. Apply the @Search.defaultSearchElement: true annotation to the required properties within the entity in your CDS service model. This annotation must only be applied to properties of type String.

The following sample code shows how you can apply these annotations in your CDS model:

search.cds

using SalesOrder;
service MySalesOrderService {
  @Capabilities.SearchRestrictions.Searchable: true
  entity Customer as projection on SalesOrder.Customer {
    * ,
    SalesOrders: redirected to SalesOrderHeader,
    Note: redirected to Notes
  }
  excluding {
    SalesOrders, Note
  };
  annotate Customer with {
    @Search.defaultSearchElement: true
    CustomerName;
    @Search.defaultSearchElement: true
    Type;
  };
}

You can trigger a search by using the Search custom query option on a single entity only and not on expanded entities. The URL format for performing such a search looks like this: <ServiceRoot>/<EntitySetName>?Search="Some Text". The following is a sample URL:

https://host/odata/v2/MySalesOrderService/Customers?Search="Harry"

The URL format for performing a search with navigation looks like this: <ServiceRoot>/<EntitySetName>(KeyPredicate)/<1:M NavigationProperty name>?Search="Some Text". The following is a sample URL:

https://host/odata/v2/MySalesOrderService/Customer(CustomerID=1,Type='Enterprise')/SalesOrders?Search="Notes"

Note: Search using wildcard characters (such as * and ?) is not supported.

On performing a search, you can encounter a 501 Not Implemented error in the following cases:

Input Validation

Following is the index of all validations foreseen as of today. The /  prefixes indicate to what extend respective checks are covered by automatic validations in Node.js (=left) and Java (=right) runtimes today.

   Referential Integrity checks    
   CRUD constraint checks    
   Read-only fields    
   Mandatory input    
   Primary key constraints    
   Secondary key constraints    
   Value ranges / patterns    
   Constraints expressions    

CRUD Constraint Checks

/   @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;
}

/   Auto-exposed Entities – @cds.autoexposed

/   Authorization Checks – @requires, @restrict

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

/   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

Unique Fields

/   for keys

/   for unique fields

/   for @unique constraints

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';
}

Automatic Validation

learn more about generic handlers

Programmatic Validation

Default Ordering

The following annotations can be used for specifying the default order:

If $orderby is present in an OData request, it has precedence over the default order.

Example:

@cds.default.order = [{ by: country, asc }, ...]
entity someEntity;