Use the OData v2 client for JavaScript / TypeScript

In the following, the OData v2 features supported by the SAP Cloud SDK are explained using the Business Partner Service of SAP S/4HANA as an example.

For more details on how to execute requests using a (pre-)generated OData client refer to this documentation.

GetAll Request Builder#

The GetAll request builder allows you to create a request to retrieve all entities that match the request configuration.

BusinessPartner.requestBuilder().getAll();

The example above creates a request to get all BusinessPartner entities.

Select#

When reading entities, the API offers select( ... ) on the builders. Through it, the query parameters $select and $expand are set. It restricts the response to the given selection of properties in the request.

The properties that can be selected or expanded are represented via static fields on the entity class. So there will be a field for each property. E.g. the business partner entity has BusinessPartner.FIRST_NAME as a representation of a property and BusinessPartner.TO_BUSINESS_PARTNER_ADDRESS as a representation of a navigation property.

A navigation property means that there is a relation between a business partner and their addresses. In this case, one business partner can have multiple addresses. In SAP S/4HANA, navigation properties typically start with TO_.

BusinessPartner.requestBuilder()
.getAll()
.select(
BusinessPartner.FIRST_NAME,
BusinessPartner.LAST_NAME,
BusinessPartner.TO_BUSINESS_PARTNER_ADDRESS
)
.execute(destination);

The above translates to the following query parameters:

$select=FirstName,LastName,to_BusinessPartnerAddress/*&$expand=to_BusinessPartnerAddress

One can also select properties of the expanded navigation property:

BusinessPartner.requestBuilder()
.getAll()
.select(
BusinessPartner.FIRST_NAME,
BusinessPartner.TO_BUSINESS_PARTNER_ADDRESS.select(
BusinessPartnerAddress.ADDRESS_ID,
BusinessPartnerAddress.CITY_CODE
)
)
.execute(destination);

The above translates to the following query parameters:

$select=FirstName,to_BusinessPartnerAddress/AddressID,to_BusinessPartnerAddress/CityCode&$expand=to_BusinessPartnerAddress

Filter#

When operating on a collection of entities the API offers filter( ... ) on the builders. It directly corresponds to the $filter parameter of the request. Filters are also built via the static property fields on entities:

/*
Get all business partners that either:
- Have first name 'Alice' but not last name 'Bob'
- Or have first name 'Mallory'
*/
BusinessPartner.requestBuilder()
.getAll()
.filter(
or(
and(
BusinessPartner.FIRST_NAME.equals('Alice'),
BusinessPartner.LAST_NAME.notEquals('Bob')
),
BusinessPartner.FIRST_NAME.equals('Mallory')
)
)
.execute(destination);

The example above will translate to this filter parameter:

$filter=(((FirstName eq 'Alice') and (LastName ne 'Bob')) or (FirstName eq 'Mallory'))

Take note of the order of and and or. As or is invoked on the result of and it will form the outer expression while and is an inner expression in the first branch of or.

It is also possible to pass multiple filters to the same filter function without concatenating them with and or or. They will be concatenated with and by default. The two following examples are equal:

.filter(
and(
BusinessPartner.FIRST_NAME.equals('Alice'),
BusinessPartner.LAST_NAME.notEquals('Bob')
)
)

The example above can be shortened to:

.filter(
BusinessPartner.FIRST_NAME.equals('Alice'),
BusinessPartner.LAST_NAME.notEquals('Bob')
)

Filter on One-To-One Navigation Properties#

In addition to basic properties, filters can also be applied on one-to-one navigation properties. The example below shows how to filter on the TO_CUSTOMER, which is a one-to-one navigation property of the BusinessPartner entity. Please note, the CUSTOMER_NAME and CUSTOMER_FULL_NAME are properties of the entity Customer, which is the type of the one-to-one navigation property TO_CUSTOMER.

/*
Get all business partners that match all the following conditions:
- Have customer with the customer name 'name'
- Have customer with the customer full name 'fullName'
*/
.filter(
BusinessPartner.TO_CUSTOMER.filter(
Customer.CUSTOMER_NAME.equals('name'),
Customer.CUSTOMER_FULL_NAME.equals('fullName')
)
)

The generated $filter will be:

$filter=((to_Customer/CustomerName eq 'name' and to_Customer/CustomerFullName eq 'fullName'))

More Filter Expressions#

More advanced filter expressions can be found here.

Skip#

skip allows you to skip a number of results in the requested set. It can be useful for paging:

BusinessPartner.requestBuilder().
.getAll()
.skip(10)

Top#

top limits the number of returned results. This can also be useful for paging:

BusinessPartner.requestBuilder().
.getAll()
.top(10)

The example above retrieves the first 10 BusinessPartner entities.

Count#

Not yet available.

GetByKey Request Builder#

The GetByKey request builder allows you to create a request to retrieve one entity based on its key:

BusinessPartner.requestBuilder().getByKey('id');

The example above retrieves the BusinessPartner with the id 'id'.

The result can be restricted by applying the select function, same as in the GetAll request.

Create Request Builder#

The Create request builder allows you to send a POST request to create a new entity:

const businessPartner = BusinessPartner.builder().build();
BusinessPartner.requestBuilder().create(businessPartner);

In the example above we created an instance of BusinessPartner and sent it to the BusinessPartner service in a POST request.

Deep Create#

It is also possible to create an entity together with related entities in a single request:

// build a business partner instance with one linked address
const businessPartner = BusinessPartner.builder()
.firstName('John')
.lastName('Doe')
.businessPartnerCategory('1')
.toBusinessPartnerAddress([
BusinessPartnerAddress.builder()
.country('DE')
.postalCode('14469')
.cityName('Potsdam')
.streetName('Konrad-Zuse-Ring')
.houseNumber('10')
.build()
])
.build();
// execute the create request
BusinessPartner.requestBuilder()
.create(businessPartner)
.execute(myDestination);

You can also create an entity asChildOf another entity.

Create as Child of#

Assume you have already created a business partner and would like to add a new address to it:

const existingBusinessPartner = await BusinessPartner.requestBuilder().getByKey(myID).execute(myDestination);
const newAddress = BusinessPartnerAddress.builder()
.country('DE')
.postalCode('14469')
.cityName('Potsdam')
.streetName('Konrad-Zuse-Ring')
.houseNumber('10')
.build()

This can be done by using the asChildOf method which allows to create an entity as a child of an existing entity. You need to give the parent object and the field connecting the two entities:

BusinessPartnerAddress.requestBuilder()
.create(newAddress)
.asChildOf(existingBusinessPartner,BusinessPartner.TO_BUSINESS_PARTNER_ADDRESS)
.execute(myDestination)

Update Request Builder#

The Update request builder allows you to send PUT or PATCH requests. By default PATCH is used to only update the changed fields:

BusinessPartner.requestBuilder().update(businessPartner);

In the example above only the changed fields of the given businessPartner instance are sent to the BusinessPartner service using PATCH.

To update the whole entity use replaceWholeEntityWithPut:

BusinessPartner.requestBuilder()
.update(businessPartner)
.replaceWholeEntityWithPut();

This will send a PUT request and thereby replace the whole entity.

Entities can only be updated if ETags match. If you want to force an update of the entity regardless of the ETag configure the request to ignore version identifiers with ignoreVersionIdentifier:

BusinessPartner.requestBuilder()
.update(businessPartner)
.ignoreVersionIdentifier();

Delete Request Builder#

The Delete request builder allows you to create DELETE requests, that delete an entity.

/*
The following won't work on the real SAP S/4HANA BusinessPartner service because BusinessPartners cannot be deleted.
We added this only for the sake of the example.
*/
BusinessPartner.requestBuilder().delete(businessPartner);

Entities can only be deleted if ETags match. If you want to force deletion of the entity regardless of the ETag configure the request to ignore version identifiers with ignoreVersionIdentifier:

BusinessPartner.requestBuilder()
.delete(businessPartner)
.ignoreVersionIdentifier();

You can also overwrite ETags using setVersionIdentifier:

BusinessPartner.requestBuilder()
.delete(businessPartner)
.setVersionIdentifier('etag');

In the example above the ETag 'ETag' is being used instead of the original one.

Handling of ETags#

Handling of CSRF tokens#

To create, update, and delete requests the SDK will try to send a CSRF token. Upon execution, the request will try to fetch a token first before issuing the create request. Many services require this behavior for security reasons. However, the create request will be made without a CSRF token if none could be obtained.

Available Filter Expressions#

Filter Functions#

There are predefined filter functions e. g. length, substring, substringOf in the core library, that allow for a wide range of filter expressions:

/*
Fetch all business partners who have a first name shorter than 5 letters
*/
BusinessPartner.requestBuilder()
.getAll()
.filter(length(BusinessPartner.FIRST_NAME).lessThan(5))
.execute(destination);

Function imports#

Request builder#

The function imports request builder helps build a request for a service operation containing parameters in a typesave way. This time, as an example, the Warehouse Outbound Delivery Order Service is used, because not all the services contain function imports like the Business Partner Service. The type safe client for the Warehouse Outbound Delivery Order Service can be found in the package @sap/cloud-sdk-vdm-warehouse-outbound-delivery-order-service.

The example below creates a function import request builder for the service operation PostGoodsIssue and then execute it against your service.

postGoodsIssue({outboundDeliveryOrder: "order"})
.execute(destination);

The service operation is defined in the service metadata.

Setting ETag#

The ETag handling with the function imports is not integrated. Below is an example demonstrating how to make use of the withCustomHeaders for setting ETag by your own. In order to use versionIdentifier for ETag value, make sure you fetch the entity information via e.g., a get request.

postGoodsIssue({outboundDeliveryOrder: "order"})
.withCustomHeaders({"if-match": entity.versionIdentifier})
.execute(destination)

Batch Requests#

OData batch requests combine multiple operations into one POST operation, allowing you to execute multiple requests with just one network call. This can significantly reduce the network overhead you have to deal with when you want to execute a large number of requests.

An OData batch request can consist of a number of retrieve requests and changesets. Those can be combined arbitrarily.

Retrieve request#

A retrieve request is any HTTP GET request. In terms of the SAP Cloud SDK this includes all requests built by a GetAllRequestBuilder and GetByKeyRequestBuilder.

Retrieve requests can be passed directly to the batch function, which in turn can be executed once to execute all subrequests. Once a batch request is executed it returns a list of BatchResponses. Those contain the raw response information of each subrequest, the subresponse to a retrieve subrequest can either be a ReadResponse or an ErrorResponse. To determine if a request was successful use .isSuccess().

Successful requests can be cast to ReadResponse which contains the HTTP code, the raw body and the constructor of the entity that was parsed from the response. In order to work with an instance of the retrieved entity, you can use the .as method, that allows you to transform the raw data into an instance of the given constructor. Note, that retrieve responses can be ErrorResponses. Therefore, it is crutial to check responses for success, before casting them to ReadResponse.

In the example below, each given address id is mapped to a GetByKeyRequestBuilder. These retrieve requests are combined into one batch request and executed against a destination.

If one of the request was not successful, an error will be thrown, otherwise the responses are transformed to instances of BusinessPartnerAddress.

async function getAddressesByIds(
businessPartnerId: string,
addressIds: string[]
): Promise<BusinessPartnerAddress[]> {
const retrieveRequests = addressIds.map(addressId =>
// Create get by key request
BusinessPartnerAddress.requestBuilder().getByKey(
businessPartnerId,
addressId
)
);
// Execute batch request combining multiple retrieve requests
const batchResponses = await batch(...retrieveRequests).execute(destination);
// Error handling
if (batchResponses.some(response => !response.isSuccess())) {
throw new Error('Some of the batch subrequests were not successful.');
}
return batchResponses.reduce(
(addresses: BusinessPartnerAddress[], response: BatchResponse) => [
...addresses,
// Transform response to an instance of BusinessPartnerAddress
...(response as ReadResponse).as(BusinessPartnerAddress)
],
[]
);
}

Changeset#

A changeset is a collection of HTTP POST, PUT, PATCH and DELETE operations - requests built by any CreateRequestBuilder, UpdateRequestBuilder and DeleteRequestBuilder in terms of the SAP Cloud SDK. The order of execution within a changeset is not defined. This differs from the whole batch request itself, where the order is defined. Therefore the requests within a changeset should not depend on each other. If the execution of any of the requests within a changeset fails, the whole changeset will be reflected as an error in the response and will not be applied, much like a database transaction.

Change requests cannot be passed to a batch request directly. They have to be combined in a changset, which in turn can be passed to the batch request. Once a batch request is executed it returns a list of BatchResponses. Those contain the raw response information of each subrequest. The response to a changeset request can either be a collection of the subresponses to the subrequests of the changeset of type WriteResponses or an ErrorResponse. To determine if a request was successful use .isSuccess().

Successful requests should be cast to WriteResponses which contains all subresponses for the changeset request. Those responses can be accessed by .responses and have the type WriteResponse. Each WriteResponse contains the HTTP code and can contain the raw body and the constructor of the entity that was parsed from the response, depending on whether there was a body in the response. Create and delete requests typically do not have a response body. In order to work with an instance of an entity given in a WriteResponse, you can use the .as method, that allows you to transform the raw string body into an instance of the given constructor. Note that the response may not exist, so you should only call this method if you know that there is data. Typically the HTTP code is a good indicator for this (201 No Content probably won't have content). If you are working with TypeScript you will have to tell the compiler, that the .as! method can be used here by adding a !. Also note, that retrieve responses can be ErrorResponses. Therefore, it is crucial to check responses for success, before casting them to WriteResponses.

In the example below, a list of addresses is mapped to UpdateRequestBuilders. These change requests are combined to one changeset, which is passed to the batch request and executed against a destination.

Once the batch request is exeucted it returns a list of BatchResponses, which in this example contains one response only, namely the one for the changeset.

If the request was not successul, an error will be thrown, otherwise the subresponses are transformed to instances of BusinessPartnerAddress.

async function updateAddresses(
businessPartnerAddresses: BusinessPartnerAddress[]
): Promise<BusinessPartnerAddress[]> {
// Create update requests
const updateRequests = businessPartnerAddresses.map(address =>
BusinessPartnerAddress.requestBuilder().update(address)
);
// Execute batch request with one changeset
const batchResponses = await batch(
// Combine update requests into one changeset
changeset(...updateRequests)
).execute(destination);
// Get response for the changeset request
const changesetResonse = batchResponses[0];
// Error handling
if (!changesetResonse.isSuccess()) {
throw new Error('The changeset request was not successful.');
}
return (changesetResonse as WriteResponses).responses.map(response =>
// Transform response to an instance of BusinessPartnerAddress
response.as!(BusinessPartnerAddress)
);
}

Combining requests#

Serialization#

By default when you execute a batch request, the subrequests are serialized to a multipart represenation of the request, which is essentially a string. This is what a create request for a business partner addresses would serialize to:

Content-Type: application/http
Content-Transfer-Encoding: binary
POST /sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartnerAddress HTTP/1.1
Content-Type: application/json
Accept: application/json
{"BusinessPartner":"1","PostalCode":"10001","City":"New York","Country":"USA"}

The first lines are request headers for the multipart request, followed by a blank line. The next line contains the request method and URL, followed by the request headers, a blank line and the request payload. Every "atomic" request is serialized to a string of this kind, while GET and DELETE requests do not provide a payload.

Configure subrequest serialization#

By default, URLs in the multipart representation of a request are serialized to a path relative to the service, e. g.:

GET /sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartnerAddress?$format=json HTTP/1.1

However, some services might only understand URLs relative to the entity or even absolute URLs.

To configure the serialization of the URLs within a batch request, you can set the subrequest path type with withSubRequestPathType. You can either set it to 'relativeToService', as is the default, 'relativeToEntity', which will yield URLs relative to the entity or 'absolute', which will produce absolute URLs. See below for examples:

Serialize subrequest path relative to entity:

// GET /A_BusinessPartnerAddress?$format=json HTTP/1.1
batch(...requests).withSubRequestPathType('relativeToEntity');

Serialize subrequest path as absolute URL:

// GET https://my-s4.system.com/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartnerAddress?$format=json HTTP/1.1
batch(...requests).withSubRequestPathType('absolute');
Last updated on by Marika Marszalkowski