Overview

The Product service helps you to manage your products. But what is a product in this context? For the Product service, a product is any kind of physical item that can be defined or described with help of some properties and that can be sold for a certain price. In other words, it is an item that you can sell and ship to your customers. Subscriptions are offstage from the Product service point of view.

The Product service supports back-office and administrative tasks using REST calls to list, view, create, edit, and delete product records. The data model supports common attributes for products. The service API separates public, consumer-facing access, which is limited to listing, querying, and viewing products marked as public, from the seller-side administrative operations that require authorization.

The API is intended to be the master-of-record repository for structured product content. It provides a scalable, fast, and easy-to-use back end for:

  • Storefronts (web and native apps) serving product content
  • Product Content Management and other back-office data editing tools for products
  • Systems integration using a flexible and rich information set that is centrally available through a well-defined API

You can use a mashup service that consolidates data from multiple services and sources to serve up the presentation in customer-facing systems.


API Reference

/{tenant}/products

/{tenant}/products

get

Get all products.

Accepted scopes:

  • hybris.product_read_unpublished - required to retrieve non-public products. If not provided only published products are returned
post

Create new product.

Accepted scopes:

  • hybris.product_create - mandatory
  • hybris.product_publish - required if product is created as published (by providing published flag true )
delete

Delete all Product entities.

Accepted scopes:

  • hybris.product_delete_all - mandatory

/{tenant}/products/{productId}

get

Retrieves a single product.

YRN: urn:yaas:hybris:product:product:{tenant};{productId}

Accepted scopes:

  • hybris.product_read_unpublished - required to retrieve non-public product
put

Update a single product.

Accepted scopes:

  • hybris.product_update - required to update product data
  • hybris.product_publish - required to publish the product (by setting published flag to true)
  • hybris.product_unpublish - required to unpublish the product (by setting published flag to false)
delete

Delete a single Product entity.

Accepted scopes:

  • hybris.product_delete - mandatory

/{tenant}/products/{productId}/media

Media files of the product.

post

Initialize process of creating new media file for product

Accepted scopes:

  • hybris.product_update - mandatory
get

Return metadata of media files. The list is ordered according to the "position" attribute set in the file metadata.

Accepted scopes:

  • hybris.product_read_unpublished - required to retrieve media metadata of non-public product

/{tenant}/products/{productId}/media/{mediaId}

get

Return media file metadata by ID.

YRN: urn:yaas:hybris:product:product-media:{tenant};{productId};{mediaId}

Accepted scope:

  • hybris.product_read_unpublished - to retrieve media metadata of non-public product
put

Update media file metadata. Note that only part of the metadata field can be updated.

Accepted scopes:

  • hybris.product_update - mandatory
delete

Delete media identified by media ID.

Accepted scopes:

  • hybris.product_update - mandatory

/{tenant}/products/{productId}/media/{mediaId}/commit

post

Confirms that the media file specified by the media id is updated and ready to be used with product.

Accepted scope:

  • hybris.product_update - required to attach media to product

/{tenant}/products/{productId}/variants

Variants of the product.

get

Get all variants for product.

Accepted scopes:

  • hybris.product_read_unpublished - required to retrieve variants of non-public product. If not provided only variants of published product are returned
post

Create new product variant.

Accepted scopes:

  • hybris.product_create - mandatory
delete

Delete all variants entities for specified product id.

Accepted scopes:

  • hybris.product_delete - mandatory

/{tenant}/products/{productId}/variants/{variantId}

get

Retrieve a single product variant.

YRN: urn:yaas:hybris:product:product-variant:{tenant};{productId};{variantId}

Accepted scopes:

  • hybris.product_read_unpublished - required to retrieve non-public product variants
put

Replace a single product variant.

Accepted scopes:

  • hybris.product_update - required to update product variant data
delete

Delete a single Product variant entity.

Accepted scopes:

  • hybris.product_delete - mandatory

/{tenant}/products/{productId}/variants/{variantId}/media

Media files of the variant.

post

Initialize process of creating new media file for variant

Accepted scopes:

  • hybris.product_update - mandatory
get

Return metadata of media files. The list is ordered according to the "position" attribute set in the file metadata.

Accepted scopes:

  • hybris.product_read_unpublished - required to retrieve media metadata of a variant that belongs to a non-public product

/{tenant}/products/{productId}/variants/{variantId}/media/{mediaId}

get

Return media file metadata by ID.

YRN: urn:yaas:hybris:product:product-variant-media:{tenant};{productId};{variantId};{mediaId}

Accepted scope:

  • hybris.product_read_unpublished - to retrieve media metadata of a variant that belongs to a non-public product
put

Update product variant media file metadata. Note that only part of the metadata field can be updated.

Accepted scopes:

  • hybris.product_update - mandatory
delete

Delete media identified by media ID.

Accepted scopes:

  • hybris.product_update - mandatory

/{tenant}/products/{productId}/variants/{variantId}/media/{mediaId}/commit

post

Confirms that the media file specified by the media id is updated and ready to be used with product variant.

Accepted scope:

  • hybris.product_update - required to attach media to product variant

/{tenant}/variants

Search for variants by code or id.

/{tenant}/variants

get

Get variants by code or id. If user provides ids and codes only the variants that fulfill both restrictions will be returned.

Accepted scopes:

  • hybris.product_read_unpublished - required to retrieve variants of non-public product. If not provided only variants of published product are returned

Search among product and variant entities


Events

For more information about events, see the PubSub service documentation.

The topic owner client is: hybris.product

EVENT TYPEDESCRIPTIONPAYLOAD SCHEMAPAYLOAD EXAMPLE
product-createdTriggered when product is successfully createdschema
{"id":"57cff211334efd001d8302dc","yrn":"urn:yaas:hybris:product:product:ngomcpq;57cff211334efd001d8302dc","location":"https://api.eu.yaas.io/hybris/product/v2/hybris/products/123"}
product-updatedTriggered when product is successfully updatedschema
{"id":"57cff211334efd001d8302dc","yrn":"urn:yaas:hybris:product:product:ngomcpq;57cff211334efd001d8302dc","location":"https://api.eu.yaas.io/hybris/product/v2/hybris/products/123"}
product-deletedTriggered when product is successfully deletedschema
{"id":"57cff211334efd001d8302dc","yrn":"urn:yaas:hybris:product:product:ngomcpq;57cff211334efd001d8302dc","location":"https://api.eu.yaas.io/hybris/product/v2/hybris/products/123"}
variant-createdTriggered when product variant is successfully createdschema
{"yrn":"urn:yaas:hybris:product:product-variant:ngomcpq;57b5be2498e0ba001d3a253a;42e5be2498e0ba001d3b303b"}
variant-updatedTriggered when product variant is successfully updatedschema
{"yrn":"urn:yaas:hybris:product:product-variant:ngomcpq;57b5be2498e0ba001d3a253a;42e5be2498e0ba001d3b303b"}
variant-deletedTriggered when product variant is successfully deletedschema
{"yrn":"urn:yaas:hybris:product:product-variant:ngomcpq;57b5be2498e0ba001d3a253a;42e5be2498e0ba001d3b303b"}


Scopes

Scopes are strings that let you specify exactly what type of access you need to resources and operations in the Product service.

You must provide a proper scope that enables users to perform certain operations. The scopes should be granted in an access token from OAuth 2.0 service. For more information about the authorization and authentication used in hybris services, and also about the scopes in general, see:

The table presents all the scopes supported by the Product service and references to the tutorials that contain examples using the proper scopes. No scope is required to read the published products.

ScopeDescriptionSelected examples of use
hybris.product_read_unpublishedUse this scope to see the unpublished products. The newly created product is in the unpublished state by default. You can see published products without using any scope. It means, that while you use the hybris.product_read_unpublished scope, you will be able to see all products: both unpublished and published.Basics Publishing Localization
hybris.product_createUse this scope to create new products or variants.Basics Extensibility Variants Localization
hybris.product_updateUse this scope to change the product or variant properties, except the published property. If you also plan to change the published property of a product, you will need hybris.product_publish or hybris.product_unpublish.Basics Extensibility Localization
hybris.product_deleteUse this scope to delete a product or a variant. If you remove a product, you automatically remove all variants originating from this product.Basics Localization Variants
hybris.product_delete_allUse this scope to delete all products. Removing all products will also remove all variants.Testing
hybris.product_publishUse this scope to publish the product. This is accomplished by setting the published property value to true. Any other changes done to the product during this operation may require other scopes.Publishing
hybris.product_unpublishUse this scope to unpublish the product. This is accomplished by setting the published property value to false. Any other changes done to the product during this operation may require other scopes.Publishing
hybris.product_migrateUse this scope to migrate the product between Product Service versions.Product Service v2 Migration

If a certain operation requires two scopes to be allowed, then your role will need to have these two scopes assigned. Let's see an example where you change some properties of the product and publish it at the same time:

  1. You want to change some of the product properties and publish it. To publish a product, your role needs the hybris.product_publish scope. It allows you to set the published property to true.
  2. However, the act of changing the product's properties - in this case, other than published property - is an update operation. The update operation requires the hybris.product_update scope.
  3. So, if you change certain product properties and publish an unpublished product by setting the published property to true, you need a role that has these two scopes: hybris.product_update and hybris.product_publish.


Mixins

About mixins

Each product contains a set of properties that uniquely identify and define it for the external world. For example, the product id identifies each particular product instance, and the description provides a brief information about the product. The published property indicates whether the product is visible for all customers in the shop or is still in an unpublished state.

There are not many properties that are directly defined in the product schema. The properties that are defined in the product schema are very common to all items of the product type. However, you can have a flexible and adjustable set of properties that vary from product to product, each one extending the information about the product item in an easy-to-read yet complex way. With a mixin, you can define such properties once and then reuse these once-defined properties in many different products.

The mixins feature can help you to define a set of properties once and reuse them in many different products. You can assign one mixin to many products, but you also can use many mixins in one product. This approach supports flexibility, re-usability, and structural consistency among all your products.

Mixin schema

A mixin defines the set of properties you can add in your product. The mixin itself is supported by the Schema service and is always referred from other services by URL. Following is an example of the mixin structure:

{
    "$schema": "http://json-schema.org/draft-04/schema#",
    "type": "object",
    "properties": {
      "type": {
        "type": "string"
      },
      "height": {
        "type": "string"
      }
    }
}

Mixins declare the properties and data types of those properties. This example contains type and height, which both accept string values.

If a mixin is in the Schema service, you can use it to extend any kind of product with two additional properties. These properties are part of the mixin schema, not part of the Product schema.

Mixin data

When you create a new product or update any product with new information, you can add these mixin-based properties to the whole product configuration.

{
  "name": "Jonak Peep toes - or",
  "metadata": {
    "mixins": {
       "categoryHeelShoes": "https://api.eu.yaas.io/hybris/schema/v1/heelshoe-v1.json"
     }
   },
   "mixins": {
      "categoryHeelShoes": {
        "type": "stiletto",
        "height": "3.5 inch"
      }
   }
}
  • You must always add the metadata property that defines the mixin alias and binds it to the URL to the mixin JSON-formatted definition. You create the alias as you like. What is important is that you always take the mixin alias you defined in the metadata section when you use the mixin.
  • You must have the mixins property, which can have properties from multiple mixins, each one referred by its alias. In this example, there is only one categoryHeelShoes mixin and it has only two properties.


Product Properties

Each product is described by a set of properties. Some of those properties are obligatory, while there may be other properties that are optional. Each product must conform to the schema coming with the Product service. Such schemas define the possible product structures, and the structure of other items related to products like variants. media, etc.

Start with some obligatory product properties:

  • id: Every product has a unique id. The id is a service-generated, constant, unique identifier of a product. It cannot be changed nor removed. You can, however, create more user-friendly identifier like code.
  • code: User-defined unique identifier of a product. This identifier is mandatory for each product item. You can set a value for this property and update it later if needed.
  • name: User-defined name of a product.

Next, optional properties:

  • description: More descriptive explanation of a product, which you can add or not, depending on your choice as the property is optional.
  • published: Defines if the product is seen as published or unpublished. By default, any product is unpublished. If you skip this property in the product definition, then it will be treated as unpublished.

Of course, any product may have much more other properties, user-defined custom properties. But since they are user-defined, it is not possible to make a list of such properties right here. You can simply check the schemas coming with the Product service and dive into schema details if you are interested in the entrails of the complex products structures and how they can be created. You can also run our tutorials to see examples of API methods that the Product service provides. These examples use some data that can provide you with good insight how the properties can be constructed and used.


Data Validation

Some of the tasks set for the Product service are:

  • Receiving data
  • Sending data

Product service will not accept input data if it is invalid or even missing while required. The validity of the provided data is checked in multiple ways, but the first and most important check is provided by schemas.

Schemas define a structure of any item that the Product service can handle like product, variant, media, and so on. Any payload users provide is checked against schemas. It must conform to the rules defined by these schemas to be successfully processed by the service. If it does not conform to the rules, the service returns an error, usually with a hint what went wrong so that users can correct the invalid input and try to send it again.

There are also some schemas that define the shape of the output the service provides. For example, a generic response of a service should include code, status, message, and data.


YaaS Resource Name

You can have a lot of products, media, and variants. The number of items can be hundreds, thousands, perhaps millions. You can, however, preserve a way to uniquely identify each of those items within the whole YaaS environment. All you need is an identifier to mark each resource. Such identifier is called: YaaS Resource Name (YRN). With YRN you can identify any resource, no matter to what tenant it belongs. It is all unique within YaaS platform.

The best with the YRN is that you do not really need to create it by yourself for all your products. No need to remember how to keep uniqueness of the product names. The Product service will do it for you. The YRN will be added to any resource definition while you create it. If you make a new product, a standard response will be returned that includes the yrn attribute. And if later you retrieve such product, the YRN identifier will already be there. Run our Basics tutorial and check the results of any GET methods. In the response payload, among many other properties, you will find the yrn attribute, holding something like urn:yaas:hybris:product:product:tempfbf21f6;57bd96ab9ef6c9001d9b73e4.

At first sight, it may look a bit confusing. Fortunately, there is a clear sense in this data structure. The YRN is simply a Uniform Resource Name (URN) supplemented with with a custom YaaS schema: urn:yaas:<organization>:<service>:<resource>:<id_part>{;<id_part>}.

Take a look at each part of YRN:

  • urn: The URN namespace used by YaaS, identified by yaas
  • organization: The organization base path used by the service owning the resource type, such as hybris in https://api.eu.yaas.io/hybris/product/v2
  • service: The identifier of the service owning the resource type, such as product in https://api.eu.yaas.io/hybris/product/v2.
  • resource: The identifier of the resource type to which the resource belongs:
    • Usually, a singular form of the resource base path, such as product in https://api.eu.yaas.io/hybris/product/v2/products/{id}
    • For a nested resource, the parent resource type can be used as a prefix separated by a hyphen, such as product-media in https://api.eu.yaas.io/hybris/product/v2/products/{id}/media/{id}. Note that usage of a prefix will not imply any particular structure on the id_part.
  • id_part: The local resource identifier. There can be one or more identifiers. For example, https://api.eu.yaas.io/hybris/product/v1/myshop/products/67

See an example of YRN identifiers in the Product service.

  • product: A single unique identifier for a product. Our YRN could look like urn:yaas:hybris:product:product;myshop;102
  • product-media: A nested unique identifier. Our YRN could look like urn:yaas:hybris:product:product-media;myshop;102;21
  • product-variant: A nested unique identifier. Our YRN could look like urn:yaas:hybris:product:product-variant;myshop;102;43
  • product-variant-media: A nested unique identifier. Our YRN could look like urn:yaas:hybris:product:product-variant-media;myshop;102;43;21

For more general description of resource identifiers in YaaS, see the Resource Identifier document.


More on Publishing

The Publishing tutorial informs you how to publish and unpublish products, and how to retrieve products that are published, unpublished, or both. This tutorial uses the Product service and additional endpoints to illustrate how it works. These are the additional endpoints that support operations related to publishing or unpublishing products:

  • /products
  • /products/productId
  • /products/productId/media
  • /products/productId/variants
  • /products/productId/variants/variantId
  • /products/productId/variants/variantId/media


Basics

With the Product service you can do all the necessary operations to manage your products: create, retrieve, update, and delete. To perform the basic operations supported by the Product service, you need to be properly authorized. This is achieved through the header that carries the correct access token. The examples in this tutorial will lead you through those operations, showing what you need to provide to the methods, and what you get in return.

Setup

Assertion

Define variable assert:

assert = chai.assert;
Get access token

To perform access-restricted operations, you need an access token. For this purpose, create an API Client for the oAuth2 service:

API.createClient('oAuth2Service',
'/services/eu/oauth2/v1/api.raml');

Get the token:

tenant = projectIdPlaceholder;

AccessToken = oAuth2Service.token.post({
  'client_id' : clientIdPlaceholder,
  'client_secret':clientSecretPlaceholder,
  'grant_type' : 'client_credentials',
  'token_type': 'Bearer',
  'scope': 'hybris.tenant='+tenant+' hybris.product_read_unpublished hybris.product_create hybris.product_update hybris.product_delete hybris.product_delete_all'
});

To make the calls simple and the code clean, assign the access token to a variable:

access_token = AccessToken.body.access_token;

Create an API client for the Product service

API.createClient('productService',
'/services/eu/product/v2/api.raml');

Cleanup

Before creating new products for the tutorial examples, delete all the products in the project:

response = productService.tenant(tenant).products.delete({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
);

assert.equal(response.status, 204);
response;

Create a simple product

In the beginning of your work the shop is empty, no products are present. To start with something, add a simple product which you can also use for other operations. The POST method is precisely what you need to do so. To create a simple product object, you need to provide just a few attributes. You can later add more attributes if you need. The method returns a unique product identifier named id, which you will use later.

The id is a service-generated unique identifier of a product. You can, however, create more user-friendly identifiers. The code is such user-defined unique identifier of a product. This identifier is mandatory for each product item. You can set a value for this property and update it later if needed.
response = productService.tenant(tenant).products.post({
    'code': 'apple_lobo',
    'name': 'Lobo',
  'description': 'A McIntosh-style apple from Canada, generally believed to be better all-round than its parent.'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    }
)

assert.equal(response.status, 201);

response;

To make the calls simple and the code examples clean, assign id of the returned object to an appleId variable.

appleId = response.body.id;

Retrieve a single product

Retrieve one single specific product you created in previous step. The GET method needs to know the id of this product. In response you get this product's data.

You get precisely what you have created, so in this case the response would include just a few attributes like code, name, and description. You did not create the id attribute, but it is here because the id attribute is auto-created and unique in the tenant for each product object.

response = productService.tenant(tenant).products.productId(appleId).get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.id, appleId);
assert.equal(response.body.code, 'apple_lobo');
assert.equal(response.body.name, 'Lobo');
assert.equal(response.body.description, 'A McIntosh-style apple from Canada, generally believed to be better all-round than its parent.');

response;

Update a simple product

Modifying products is simple. For example, change the name, code, and description of the Lobo apple to be a Cortland apple instead.

Use the PUT method to also provide the id of the product, stored in the appleId variable previously created.

response = productService.tenant(tenant).products.productId(appleId).put({
    'code': 'apple_cortland',
    'name': 'Cortland',
  'description': 'One of the more successful McIntosh offspring, with all the usual characteristics, including the sweet vinous flavour.'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    }
)

assert.equal(response.status, 200);

response;

Verify the update worked as intended.

response = productService.tenant(tenant).products.productId(appleId).get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Accept-Language': 'en'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.id, appleId);
assert.equal(response.body.code, 'apple_cortland');
assert.equal(response.body.name, 'Cortland');
assert.equal(response.body.description, 'One of the more successful McIntosh offspring, with all the usual characteristics, including the sweet vinous flavour.');

response;

Indeed, the apple is updated to be a Cortland apple now. The code, name, and description attributes are updated, but the id attribute is unchanged. This unique id is auto-created for each product upon its creation and remains constant through the whole product life-cycle.

The Product service internally uses optimistic locking to handle clients requests. Even if you do not specify the product version when updating a product, a 409 error may occur if another user changed the product data at the same time. Ensure that your product data is up to date before updating, and this type of request is successful. If not, retrieve the new product data and retry the update.

Delete a product

To delete a product that is no longer needed, use the DELETE method. This method takes the product id as a parameter, such as the appleId variable. Provide the proper authorization and execute the method. The response status verifies the operation is successful.

response = productService.tenant(tenant).products.productId(appleId).delete({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
);

assert.equal(response.status, 204);
response;


Querying

You stay face to face with the problem of huge amount of products in your shop, but you actually need to extract just a few of them. You may want to modify them, so you need to retrieve them first. Or you are just interested in some particular details of a product, or period of time when those have been created. You need to find some conditions that would help you to limit the amount of items received in response. But you also need a way to actually apply these conditions to your data. The querying feature answers these needs.

Setup

url = document.referrer

parser = document.createElement('a')
parser.href = url

possibleStagePrefix = ''

if (parser.hostname.indexOf('stage')>0)
  possibleStagePrefix = 'stage.'
Assertion

Define variable assert:

assert = chai.assert;
Get access token

To perform any operations with a specific service, you always need an access token. For this purpose, create an API Client for the oAuth2 service:

API.createClient('oAuth2Service',
'/services/eu/oauth2/v1/api.raml');

Get the token:

tenant = projectIdPlaceholder;

AccessToken = oAuth2Service.token.post({
  'client_id' : clientIdPlaceholder,
  'client_secret':clientSecretPlaceholder,
  'grant_type' : 'client_credentials',
  'token_type': 'Bearer',
  'scope': 'hybris.tenant='+tenant+ ' hybris.product_read_unpublished hybris.product_create hybris.product_update hybris.product_delete hybris.product_publish hybris.schema_manage hybris.product_delete_all'
});

To make the calls simple and the code clean, assign the id of the returned object to a variable:

access_token = AccessToken.body.access_token;
Create API client for Product service
API.createClient('productService',
'/services/eu/product/v2/api.raml');
Create API client for Schema repository service
API.createClient('schemaService', '/services/eu/schema/v1/api.raml');

Cleanup

Before creating new products for tutorial examples, delete all products in project:

response = productService.tenant(tenant).products.delete({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
);

assert.equal(response.status, 204);
response;

Tutorial data

Querying works at its best with big amount of complex data, however, for the purpose of the examples it is fair enough to create two simple products:

response = productService.tenant(tenant).products.post({
        'code': 'apple_lobo',
        'name': 'Apple Lobo',
      'description': 'A McIntosh-style apple from Canada, generally believed to be better all-round than its parent.'
  }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    });
  appleId0 = response.body.id

response = productService.tenant(tenant).products.post({
        'code': 'apple_cortland',
        'name': 'Apple Cortland',
      'description': 'Cortland is a cultivar of apple, among the fifteen most popular in the United States.'
  }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    });
  appleId1 = response.body.id

Query a string-based property

From all of your products you may want to pick up a particular one. Perhaps you want to remove it, or update its details. For whatever reason you want to find a single product, querying feature provides a way to do it.

A q query parameter accepts string values to find a match in your products. In the end you will get all your products that match this specific query string. The more specific query, the more limited in numbers is response.

Simple string property

Assume you have Lobo apples, while having many other kinds of apples in your store. But you only want to see details of this one type of apples. Use a query and set a product property as the query parameter, for example code to find only those items that you require: q=code:apple_lobo

response = productService.tenant(tenant).products.get({
      'q'      : 'code:apple_lobo'
    }, {
      headers: {
      'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.length, 1);
assert.equal(response.body[0].name, "Apple Lobo");
response;

Complex string property

The previous example with code was easy, since it is one-word property here. But what if there is a space in a complex property value like in name or description. If you have two or more words in a single property, use quotes around the value that you assign to the q query parameter, for example q=name.en:"Apple Lobo"

response = productService.tenant(tenant).products.get({
      'q'      : 'name.en:"Apple Lobo"'
    }, {
      headers: {
      'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)

assert.equal(response.status, 200)
assert.equal(response.body.length, 1);
assert.equal(response.body[0].name, "Apple Lobo");
response;

Query a number-based property

For the next examples, create two products with mixin. If a schema does not exist, create it, too.

schema = 'price-v1.json';
response = schemaService.tenant(tenant).schema(schema).get({
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)
if(response.status == 404){
  response = schemaService.tenant(tenant).schema(schema).post({
      '$schema': 'http://json-schema.org/draft-04/schema#',
      'type': 'object',
      'properties':{
          'value': {
              'type':'number'
          }
      }
    },{
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    });
    assert.equal(response.status, 201);
  }

Create the first product with mixin:


response = productService.tenant(tenant).products.post({
        'code': 'apple_gala',
        'name': 'Apple Gala',
      'description': 'One of the most widely available commercial fruit.',
    'metadata': {
                'mixins':{
                  'price': 'https://api.eu.yaas.io/hybris/schema/v1/'+tenant+'/price-v1.json'
          }
     },
    'mixins': {
                    'price': {
                    'value': 10
                }
     }
  }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    });
response;

Create the second product with mixin:


response = productService.tenant(tenant).products.post({
        'code': 'apple_mcintosh',
        'name': 'Apple McIntosh',
      'description': 'A popular, cold-tolerant eating apple in North America.',
    'metadata': {
                'mixins':{
                  'price': 'https://api.eu.yaas.io/hybris/schema/v1/'+tenant+'/price-v1.json'
          }
     },
    'mixins': {
                    'price': {
                    'value': 20
                }
     }
  }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    });
response;

Querying for a specific numeric value

Every product can have a variety of prices, depending on the producer, delivery time, quality, and so on. You want to find the Lobo apples with the price of 20. A query like this - ?q=mixins.price.value:20 - should do the trick.

response = productService.tenant(tenant).products.get({
      'q'      : 'mixins.price.value:20'
    }, {
      headers: {
      'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)
assert.equal(response.body.length, 1);
assert.equal(response.body[0].code, 'apple_mcintosh');
assert.equal(response.body[0].mixins.price.value, 20);
response;

Querying for numeric values greater than

Find those products that have prices higher than a certain threshold price. Use this query: q=mixins.price.value:>10 and check the results:

response = productService.tenant(tenant).products.get({
      'q'      : 'mixins.price.value:>10'
    }, {
      headers: {
      'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)
assert.equal(response.body.length, 1);
assert.equal(response.body[0].code, 'apple_mcintosh');
assert.equal(response.body[0].mixins.price.value, 20);
response;

Querying for numeric values greater than or equal

Find the products that have prices higher than a certain value, including that value as well. Use this query: q=mixins.price.value:>=10 and see the results:

response = productService.tenant(tenant).products.get({
      'q'      : 'mixins.price.value:>=10'
    }, {
      headers: {
      'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)
assert.equal(response.body.length, 2);
response;

Querying for numeric values less than

In this example, you will find all products that are cheaper than something at a particular price. Use the following query: q=mixins.price.value:<20:

response = productService.tenant(tenant).products.get({
      'q'      : 'mixins.price.value:<20'
    }, {
      headers: {
      'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)
assert.equal(response.body.length, 1);
assert.equal(response.body[0].code, 'apple_gala');
assert.equal(response.body[0].mixins.price.value, 10);
response;

Querying for numeric values less than or equal

You may want to find all products that are cheaper than something at a particular price, but you actually are interested in that "something" as well. Modify the query just a little bit: q=mixins.price.value:<=20:

response = productService.tenant(tenant).products.get({
      'q'      : 'mixins.price.value:<=20'
    }, {
      headers: {
      'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)
assert.equal(response.body.length, 2);
response;

Querying for numeric values within a range of values

Assume you are not interested in the cheapest products, neither are you looking for the most expensive ones. You want something in between. You can use a query that establishes a numeric range to find only those products for which the price is equal or higher than a minimum, and lower or equal a certain maximum. Modify the query one more time: q=mixins.price.value:(>=10 AND <=20):

response = productService.tenant(tenant).products.get({
      'q'      : 'mixins.price.value:(>=10 AND <=20)'
    }, {
      headers: {
      'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)
assert.equal(response.body.length, 2);
response;

Query a boolean property

Sometimes you may want to find a yes or no answer to some question. For example, is the product information published in the store, or maybe it is not? It can be either published or not published. A query like this - q=published:true - will help you to determine which of your products are published. Surely, the remaining products not returned by this query are not published.

Assume the Lobo information was not published. You can update the published property of this product. The result of such update will be a published product.

response = productService.tenant(tenant).products.productId(appleId0).put({
    'published': true,
  'code': 'apple_lobo',
    'name': 'Apple Lobo',
  'description': 'A McIntosh-style apple from Canada, generally believed to be better all-round than its parent.'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    }
)

response;

You can run a query in a hope that you find your published product. Actually, at this moment there is one: Lobo. You have just published it in the previous step. Other product, that has not yet been published, is not returned.

response = productService.tenant(tenant).products.get({
      'q'      : 'published:true'
    }, {
      headers: {
      'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)
assert.equal(response.body.length, 1);
assert.equal(response.body[0].name, 'Apple Lobo');
assert.isTrue(response.body[0].published);
response;

Query a date property

Currently, the Product service does not support using the date object property as a query parameter.

Checking non-existing or empty properties

Someone created a product but forgot to add certain property or left the property empty, for example description:

response = productService.tenant(tenant).products.post({
        'code': 'apple_champion',
        'name': 'Apple Champion'
  }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    });

You may run a query that helps you to find out if everything is as it should, or perhaps some products remain without a proper description. Simply run a query like this: q=description.en:null

response = productService.tenant(tenant).products.get({
      'q'      : 'description.en:null'
    }, {
      headers: {
      'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)
assert.equal(response.body.length, 1);
assert.equal(response.body[0].name, 'Apple Champion');
response;

If you extend your product properties with mixin-based properties, you may want to check which of your products actually have mixins. You can accomplish this by using such a query: q=mixin:exists

response = productService.tenant(tenant).products.get({
      'q'      : 'mixins.price:exists'
    }, {
      headers: {
      'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)
assert.equal(response.body.length, 2);
response;

Now, find those products that actually do not have mixins. Your query will look like: q=mixin:missing

response = productService.tenant(tenant).products.get({
      'q'      : 'mixins.price:missing'
    }, {
      headers: {
      'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)
assert.equal(response.body.length, 3);
response;



Sorting

You know how to create, retrieve, and group products in page form. But if you want the search results sorted in a certain order, follow the instructions in this tutorial.

Setup

Assertion

Define an assert variable:

assert = chai.assert;
expect = chai.expect;

Get an access token

To perform access-restricted operations, you need an access token. Create an API Client for the OAuth2 service:

API.createClient('oAuth2Service',
'/services/eu/oauth2/v1/api.raml');

Get the token:

tenant = projectIdPlaceholder;

AccessToken = oAuth2Service.token.post({
  'client_id' : clientIdPlaceholder,
  'client_secret':clientSecretPlaceholder,
  'grant_type' : 'client_credentials',
  'token_type': 'Bearer',
  'scope': 'hybris.tenant='+tenant+ ' hybris.product_read_unpublished hybris.product_create hybris.product_update hybris.product_delete hybris.product_publish hybris.schema_manage hybris.product_delete_all'
});

To make the calls simple and the code clean, assign the access token of the returned object to a variable:

access_token = AccessToken.body.access_token;

Create an API client for the Product service

API.createClient('productService',    '/services/eu/product/v2/api.raml');

Create an API client for the Schema repository service with a valid token

API.createClient('schemaService', '/services/eu/schema/v1/api.raml');

Cleanup

Before you create new products for the tutorial examples, clean up all the products in the project:

response = productService.tenant(tenant).products.delete({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
);

assert.equal(response.status, 204);
response;
In some of the code samples, the Accept-Language header is used strictly for the purpose of this tutorial. Use this header with the en value to change the default settings of the browsers, and support the sorting operation presented in this tutorial.

Tutorial data

To sort multiple objects, you need more than one of them. Create four product objects and a schema, if one does not exist yet. This schema is used with products extended by mixin-based properties.

schema = 'apple_mcintosh-v1.json';
response = schemaService.tenant(tenant).schema(schema).get({
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)
if(response.status == 404){
  response = schemaService.tenant(tenant).schema(schema).post({
      '$schema': 'http://json-schema.org/draft-04/schema#',
      'type': 'object',
      'properties':{
          'parentage': {
              'type':'string'
          }
      }
    },{
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    });
    assert.equal(response.status, 201);
  }

The first product is a Jubilee, which is a McIntosh-style apple, or a McIntosh ancestor. You can use a mixin-based property to keep information about what type of apple is a parent to the Jubilee apple. In this example, the parentage property holds this information.

response = productService.tenant(tenant).products.post({
        'code': 'apple_jubilee_apple_mixin',
        'name': 'Jubilee apple',
    'published': true,
      'description': 'A modern cultivar of dessert apple, which was developed in the Canadian province of British Columbia by the "Summerland Research Station".',
    'metadata': {
                'mixins':{
                  'apple_mcintosh':    'https://api.eu.yaas.io/hybris/schema/v1/'+tenant+'/apple_mcintosh-v1.json'
          }
     },
    'mixins': {
                    'apple_mcintosh': {
                    'parentage': 'McIntosh'
                }
     }
}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    });
response;

Create another product, a Spartan apple, which is also a McIntosh-style apple. You use one mixin to add the same property to two products, referencing a schema.

response = productService.tenant(tenant).products.post({
        'code': 'apple_spartan_mixin',
        'name': 'Spartan',
    'published': true,
      'description': 'The Spartan is an apple cultivar developed by Dr. R.C Palmer and introduced in 1936 from the Federal Agriculture Research Station in Summerland, British Columbia, now known as the Pacific Agri-food Research Centre - Summerland.',
    'metadata': {
                'mixins':{
                  'apple_mcintosh':    'https://api.eu.yaas.io/hybris/schema/v1/'+tenant+'/apple_mcintosh-v1.json'
          }
     },
    'mixins': {
                    'apple_mcintosh': {
                    'parentage': 'McIntosh'
                }
     }
  }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    });
response;

Create a third product, a Lobo apple.

response = productService.tenant(tenant).products.post({
    'code': 'apple_lobo',
    'name': 'Lobo',
  'published': false,
  'description': 'A McIntosh-style apple from Canada, generally believed to be better all-round than its parent.'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    }
)

assert.equal(response.status, 201);
response;

Create the fourth product, a Jonagold apple.

response = productService.tenant(tenant).products.post({
    'code': 'apple_jonagold',
    'name': 'jonagold',
  'published': false,
  'description': 'Jonagold has a green-yellow basic color with crimson, brindled covering color. The apple has a fluffily crisp fruit. It is juicy and aromatic and has a sweet-sour taste.'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    }
)

assert.equal(response.status, 201);
response;

Sort with ordering modifiers

Use the sorting feature to arrange your product items in a certain sequence. Select any attribute of a product to be a sort key, such as name. The results can be ordered in an ascending or descending order.

  • Use the asc modifier with your sort key for ascending order.
  • Use the desc modifier with your sort key for descending order.
response = productService.tenant(tenant).products.get({
      'sort'      : 'name.en:asc'
    }, {
      headers: {
        'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body[0].name, 'Jubilee apple');
assert.equal(response.body[1].name, 'Lobo');
assert.equal(response.body[2].name, 'Spartan');
assert.equal(response.body[3].name, 'jonagold');
response;

Use the desc modifier to the sort key to get the same list of products, but in the reverse order.

response = productService.tenant(tenant).products.get({
      'sort'      : 'name.en:desc'
    }, {
      headers: {
        'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)
assert.equal(response.status, 200);
assert.equal(response.body[0].name, 'jonagold');
assert.equal(response.body[1].name, 'Spartan');
assert.equal(response.body[2].name, 'Lobo');
assert.equal(response.body[3].name, 'Jubilee apple');
response;

Sort without ordering modifiers

If you omit a modifier for a sort key and only use the sort key itself, such as name, the default ascending ordering is used.

response = productService.tenant(tenant).products.get({
            'sort'      : 'name.en'
    }, {
      headers: {
        'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body[0].name, 'Jubilee apple');
assert.equal(response.body[1].name, 'Lobo');
assert.equal(response.body[2].name, 'Spartan');
assert.equal(response.body[3].name, 'jonagold');
response;

Sort by values not defined

You can sort your products by a certain attribute, such as the mixin attribute: sort=mixins.apple_mcintosh.parentage:asc. If your products do not have the mixin attribute defined, note the results:

  • You get all the products that do not have the attribute you used in your query. These products are sorted by default.
  • You also get all the products that have the attribute you used in your query. These products are sorted according to the modifier used in the query. In this case, the asc modifier responsible for the ascending sort order.
response = productService.tenant(tenant).products.get({
            'sort'      : 'mixins.apple_mcintosh.parentage:asc'
    }, {
      headers: {
        'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)

assert.equal(response.status, 200);
expect(response.body[0].mixins).to.be.empty;
expect(response.body[1].mixins).to.be.empty;
assert.equal(response.body[2].mixins.apple_mcintosh.parentage, 'McIntosh');
assert.equal(response.body[3].mixins.apple_mcintosh.parentage, 'McIntosh');
assert.includeMembers([response.body[0].code,response.body[1].code],['apple_lobo','apple_jonagold']);
assert.includeMembers([response.body[2].code, response.body[3].code],['apple_jubilee_apple_mixin','apple_spartan_mixin']);

response;

Use multiple sort keys

You are not limited to sorting your products by just one property. Make a more complex query, using more sort keys. For example, use two sort keys typical for two dimensional sorting: published:asc,code:desc. The first published attribute is sorted in an ascending order, while the second code attribute is sorted in descending order.

response = productService.tenant(tenant).products.get({
            'sort'      : 'published:asc,code:desc'
    }, {
      headers: {
        'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body[0].published, false);
assert.equal(response.body[1].published, false);
assert.equal(response.body[2].published, true);
assert.equal(response.body[3].published, true);
assert.equal(response.body[0].code, 'apple_lobo');
assert.equal(response.body[1].code, 'apple_jonagold');
assert.equal(response.body[2].code, 'apple_spartan_mixin');
assert.equal(response.body[3].code, 'apple_jubilee_apple_mixin');
response;

Skip the sort query parameter

If you do not use the sort query parameter, you are still able to retrieve the list of your products. Typically, each time you retrieve the products this way, the order is the same. However, there is no guarantee that the order of the data retrieved without the sort parameter is always the same.

Sort by a non-existing property

If you try to sort by a product property that is not defined in the property definition, such as sort=maxSpeed, it is ignored. The status code of 200 is still returned, along with the product listing, but the ordering is the same as if no sort query parameter was used.

Limitations

Results are case sensitive. You get the products starting with uppercase letter first, and the results starting with lowercase letter come next.

response = productService.tenant(tenant).products.get({
            'sort'      : 'name.en:asc'
    }, {
      headers: {
        'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body[0].name, 'Jubilee apple');
assert.equal(response.body[1].name, 'Lobo');
assert.equal(response.body[2].name, 'Spartan');
assert.equal(response.body[3].name, 'jonagold');
response;


Projection

Having many properties that define a single product, you may sometimes feel lost while retrieving long list of items, each of which defined with many properties. Moreover, you may not always be interested in all of those properties, but just in a few or even a single one. The purpose of the projection feature is just to help you extract just the information you need. As you will see you may limit amount of data returned with your products by simply selecting and retrieving those properties that are meaningful to you.

Setup

Assertion

Define variable assert:

assert = chai.assert;
Get access token

To perform any operations with a specific service, you always need an access token. For this purpose, create an API Client for the oAuth2 service:

API.createClient('oAuth2Service',
'/services/eu/oauth2/v1/api.raml');

Now, get the token:

tenant = projectIdPlaceholder;

AccessToken = oAuth2Service.token.post({
  'client_id' : clientIdPlaceholder,
  'client_secret':clientSecretPlaceholder,
  'grant_type' : 'client_credentials',
  'token_type': 'Bearer',
  'scope': 'hybris.tenant='+tenant+ ' hybris.product_read_unpublished hybris.product_create hybris.product_update hybris.product_delete hybris.product_delete_all'
});

To make the calls simple and the code clean, assign the id of the returned object to a variable:

access_token = AccessToken.body.access_token;
Create API client for Product service
API.createClient('productService',
'/services/eu/product/v2/api.raml');

Cleanup

Before creating new products for the tutorial examples, delete all products in project:

response = productService.tenant(tenant).products.delete({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
);

assert.equal(response.status, 204);
response;

Tutorial data

As the projection supports selective display of the product properties, it can easily be presented by the example of one product. Create a product that you will use in next sections. It should be a simple entity, having just a few properties: code, name, and description. In real life, the products in your shop would have many more properties, however, the mechanism of projection works the same, no matter by how many properties your products are defined.

response = productService.tenant(tenant).products.post({
    'code': 'apple_lobo',
    'name': 'Lobo',
  'description': 'A McIntosh-style apple from Canada, generally believed to be better all-round than its parent.'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    }
)
assert.equal(response.status, 201);

response;

Use projection to get one property per product

You only want to list the names of your products. How? There is a fields parameter you can use to do this. Assign any of the product properties to this parameter, like fields = name. You only receive the names of your products and nothing else. But really? Well, that's the tricky part. There is one product property that is auto-created and constant through the whole product life cycle. It cannot be changed or removed. That property is the id of the product, unique identifier that you will always see in the list of your results, even if you have not requested this property explicitly by using the projection feature.

response = productService.tenant(tenant).products.get({
      'fields'      : 'name'
    }, {
      headers: {
        'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)

assert.equal(response.status, 200);
assert.isDefined(response.body[0].name);
assert.isDefined(response.body[0].id);
assert.isUndefined(response.body[0].code);
assert.isUndefined(response.body[0].description);

response;

Use projection without fields parameter

There is also a scenario where you skip the fields parameter at all. You may wonder if that is a default projection. Actually, that is no projection at all. You simply request all your products, and that is precisely what you receive: all products and all properties for each of these products.

response = productService.tenant(tenant).products.get({
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Accept-Language': 'en'
      }
    }
)

assert.equal(response.status, 200);
assert.isDefined(response.body[0].code);
assert.isDefined(response.body[0].name);
assert.isDefined(response.body[0].description);
assert.isDefined(response.body[0].id);

response;

Use projection with empty fields parameter

You add the fields parameter in your request call, but you forgot to assign any value to it. There is no product property assigned to this parameter, instead of that, it is empty. If this is the case, then you will only see the id of your products, and nothing else. On the other hand, you may be interested in getting only the id of your products. If so, then this is the way to accomplish that.

Of course, you can always request the id explicitly, as the fields= assignment is equivalent to fields=id, and both constructions return only the id of your products.

response = productService.tenant(tenant).products.get({
      'fields'      : ''
    }, {
      headers: {
        'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)

assert.equal(response.status, 200);
assert.isDefined(response.body[0].id);
assert.isUndefined(response.body[0].name);
assert.isUndefined(response.body[0].code);
assert.isUndefined(response.body[0].description);

response;

Use a non-existing property to create projection

If you assign the fields parameter a product property that does not exist, then as a result you will receive only the id of your products and nothing else. It is very similar behavior to the previous one, when the fields parameter has been left empty.

response = productService.tenant(tenant).products.get({
      'fields'      : 'nonExisting'
    }, {
      headers: {
        'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)

assert.equal(response.status, 200);
assert.isDefined(response.body[0].id);
assert.isUndefined(response.body[0].name);
assert.isUndefined(response.body[0].code);
assert.isUndefined(response.body[0].description);

response;

Projecting nested properties

Some properties are simple in their shape like name or code. Some may, however, show more signs of complexity, for example metadata.createdAt. It is like property inside another property. You should treat them the same way as the simple ones. Just assign such a property to the fields parameter.

response = productService.tenant(tenant).products.get({
            'fields'      : 'metadata.createdAt'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Accept-Language': 'en'
      }
    }
)

assert.equal(response.status, 200);
assert.isDefined(response.body[0].metadata.createdAt);
assert.isDefined(response.body[0].id);
assert.isUndefined(response.body[0].name);
assert.isUndefined(response.body[0].code);
assert.isUndefined(response.body[0].description);
assert.isUndefined(response.body[0].metadata.modifiedAt);
assert.isUndefined(response.body[0].metadata.version);

response;


Partial Update

Winter season is at its end, and you still have some remaining stuff in your stock. You may start thinking how to make space for a new assortment. Perhaps I can sell these products at a discounted rate, you may think. Does it mean you need to update all the information for all those products? Not at all. You may update just one piece of product information, for example description or even single mixin properties, while other pieces remain intact. You do not really need to update all product information, but just a tiny bit of it. The process is called: partial update.

Setup

Define variable assert:

assert = chai.assert;
Get access token

To perform any operations with a specific service, you always need an access token. For this purpose, create an API Client for the oAuth2 service:

API.createClient('oAuth2Service', '/services/eu/oauth2/v1/api.raml');

Get the token:

tenant = projectIdPlaceholder;

AccessToken = oAuth2Service.token.post({
  'client_id' : clientIdPlaceholder,
  'client_secret':clientSecretPlaceholder,
  'grant_type' : 'client_credentials',
  'token_type': 'Bearer',
  'scope': 'hybris.tenant='+tenant+' hybris.product_read_unpublished hybris.product_create hybris.product_update hybris.product_delete hybris.product_delete_all  hybris.schema_manage'
});

To make the calls simple and the code clean, assign the id of the returned object to a variable:

access_token = AccessToken.body.access_token;
Create API client for Product service
API.createClient('productService', '/services/eu/product/v2/api.raml');
Create API client for Schema repository service
API.createClient('schemaService', '/services/eu/schema/v1/api.raml');

Cleanup

Before creating new products for the tutorial examples, delete all products in project:

response = productService.tenant(tenant).products.delete({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
);

assert.equal(response.status, 204);
response;

Tutorial data

Begin with preparing a schema for the metadata. If you do not have any schema for your metadata, start with creating one.

schema = 'apple_mcintosh-version1.json';
response = schemaService.tenant(tenant).schema(schema).get({
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)
if(response.status == 404){
  response = schemaService.tenant(tenant).schema(schema).post({
      '$schema': 'http://json-schema.org/draft-04/schema#',
      'type': 'object',
      'properties':{
          'flavor': {
              'type':'string'
          },
          'colour': {
              'type':'string'
          }
      }
    },{
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    });

    assert.equal(response.status, 201);
  }

Having the schema prepared, create some example products. Use mixins here. Add only those properties in the mixins that are pre-defined in the schema.

First product: Apple Cortland

response = productService.tenant(tenant).products.post({
    'metadata':{
           'mixins':{
               'apple_mcintosh': 'https://api.eu.yaas.io/hybris/schema/v1/'+tenant+'/apple_mcintosh-version1.json'
           }
    },
    'code': 'apple_cortland',
    'name': {'en': 'Cortland',
             'pl': 'Cortland'},
      'description': {'en':'Cortland is a cultivar of apple, among the fifteen most popular in the United States.',
                      'de':'Cortland ist eine Sorte von Apfel, unter den fünfzehn beliebtesten in USA'},
    'mixins': {
        'apple_mcintosh': {
            'flavor': 'sweet',
            'colour': 'red'
         }
    }
  }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
  });
  appleId0 = response.body.id

response;

Next product: Apple Lobo

response = productService.tenant(tenant).products.post({
        'metadata':{
            'mixins':{
                'apple_mcintosh': 'https://api.eu.yaas.io/hybris/schema/v1/'+tenant+'/apple_mcintosh-version1.json'
            }
        },
        'code': 'apple_lobo',
        'name': {'en': 'Apple Lobo',
                 'pl': 'Jabłko Lobo'},
          'description': {'en':'A McIntosh-style apple from Canada, generally believed to be better all-round than its parent.',
                      'pl':'Jabłko typu mcintosh pochodzące z Kanady, powszechnie uważane za lepsze od odmian z których się wywodzi.'},
        'mixins': {
            'apple_mcintosh': {
                'flavor': 'sweet',
                'colour': 'red'
            }
        }
  }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
  });
  appleId1 = response.body.id

response;

Update set of translations

Begin with translation. In the example, a translation is a specific language version of a certain property, for instance name. You sell your apples in England, so you have the English version of the name. You may want, however, to sell your apples in Germany, and for your new customers you prepare the German version of your products, by adding the translation for the name property, and other localizable properties.

You check the product details of your Cortland product, and discover that the name property does not have a German version yet. Moreover English translation requires an update. You also realize that all other product traits like code or even description may remain unchanged.

You can use the PUT method for this operation. You need to provide the id of the product. Here, it is stored in the appleId0 variable, created in the first example in this tutorial.

response = productService.tenant(tenant).products.productId(appleId0).put({
    'name': {'en': 'Apple Cortland', 'de':'Apfel Cortland'}
    }, {
        headers: {
            'Authorization': 'Bearer ' + access_token,
            'Content-Type' : 'application/json'
        },
        query: {
            'partial': 'true'
        }
    }
)

assert.equal(response.status, 200);

response;

To be sure, check if there really is a new language version of the product:

response = productService.tenant(tenant).products.productId(appleId0).get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
          'hybris-languages': '*'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.id, appleId0);
assert.equal(response.body.code, 'apple_cortland');
assert.deepEqual(response.body.name, {'en': 'Apple Cortland', 'de':'Apfel Cortland', 'pl':'Cortland'});
assert.deepEqual(response.body.description, {'en':'Cortland is a cultivar of apple, among the fifteen most popular in the United States.',
                                               'de':'Cortland ist eine Sorte von Apfel, unter den fünfzehn beliebtesten in USA'});
response;

Consider the following case: you perform the partial update to provide a new translation set for the name property. The result of such a partial update will be a merged collection. Translations not provided in the partial update request are not changed. Translations which were provided and existed before the update are overridden. New translations are persisted.

Update a single translation

As you could have seen in the previous example, the three language versions of the name attribute are not alike. Modify the Polish translation, since it is the only one here that deviates from others. You can simply send a new value in the payload like in the following example. The other, rather important fact, is that the information about the language is being sent in the Content-Language header:

response = productService.tenant(tenant).products.productId(appleId0).put({
    'name': 'Jabłko Cortland'
    }, {
        headers: {
            'Authorization': 'Bearer ' + access_token,
            'Content-Type' : 'application/json',
            'Content-Language' : 'pl'
        },
        query: {
            'partial': 'true'
        }
    }
)

assert.equal(response.status, 200);

response;

You can update one language version per call this way, as the Content-Langauge can carry over just one language code per call. The example above replaces only the Polish translation of the name attribute, keeping the other language versions intact. However, if you want to be sure the update has worked as intended, verify it by retrieving the partially updated product:

response = productService.tenant(tenant).products.productId(appleId0).get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
          'hybris-languages': '*'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.id, appleId0);
assert.equal(response.body.code, 'apple_cortland');
assert.equal(response.body.name.pl, 'Jabłko Cortland');
assert.equal(response.body.name.en, 'Apple Cortland');
assert.equal(response.body.name.de, 'Apfel Cortland');
assert.deepEqual(response.body.description, {'en':'Cortland is a cultivar of apple, among the fifteen most popular in the United States.',
                                              'de':'Cortland ist eine Sorte von Apfel, unter den fünfzehn beliebtesten in USA'});

response;

As before, the result of such partial update will be a merged collection. Translations not provided in the partial update request are not changed. As you can see, the Polish translation has been modified: 'Cortland' is replaced by 'Jabłko Cortland'.

Update mixin-based attributes

You have some customized product attributes defined for mixins. Generally, working with mixins requires you to provide the mixin definition (in the metadata/mixins section). This is, however, not the case of the partial update. If the attributes you are going to update are coming from mixins, you do not need to provide the mixin definition in the metadata section. Take a look at the example, run the code:

response = productService.tenant(tenant).products.productId(appleId1).put({
        'mixins':{
           'apple_mcintosh': {
               'flavor': 'VERY sweet'
           }
        }
    }, {
        headers: {
            'Authorization': 'Bearer ' + access_token,
            'Content-Type' : 'application/json'
        },
        query: {
            'partial': 'true'
        }
    }
)

assert.equal(response.status, 200);

response;

Verify if everything has worked as planned, by retrieving the product:

response = productService.tenant(tenant).products.productId(appleId1).get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
          'Accept-Language': 'en'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.id, appleId1);
assert.equal(response.body.code, 'apple_lobo');
assert.equal(response.body.name, 'Apple Lobo');
assert.deepEqual(response.body.mixins, {'apple_mcintosh': {'flavor': 'VERY sweet', 'colour': 'red'}});

response;

As you can see, the partial update only modifies the value of those attributes that have been used in the operation. The value of other attributes remains intact, precisely as intended.

Remove values with partial update

You can also use partial update to remove specific property or section from your product definition. Assume you no longer need a mixin-based property: flavor. This may seem a little bit counter-intuitive, but to remove one or some selected properties, you actually perform partial update operation. More specifically, you use the PUT method with partial query parameter.

If you nullify an attribute, then its value will be gone, in other words deleted. This way you can use partial update to remove selected attribute values from the product definition.

Perhaps an example could make this fully clear, so take a look how to remove a single mixin-based attribute:

response = productService.tenant(tenant).products.productId(appleId1).put({
    'mixins':{
       'apple_mcintosh': {
           'flavor': null
       }
    }
    }, {
        headers: {
            'Authorization': 'Bearer ' + access_token,
            'Content-Type' : 'application/json'
        },
        query: {
            'partial': 'true'
        }
    }
)

assert.equal(response.status, 200);

response;

Verify:

response = productService.tenant(tenant).products.productId(appleId1).get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
          'Accept-Language': 'en'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.id, appleId1);
assert.equal(response.body.code, 'apple_lobo');
assert.equal(response.body.name, 'Apple Lobo');
assert.isUndefined(response.body.mixins.apple_mcintosh.flavor);
assert.isDefined(response.body.mixins.apple_mcintosh.colour);

response;

Only those attribute values will be removed that you specify in this operation: in this example it is flavor. All other attributes remain unchanged.

Remove all mixin-based properties

What you do in this section is removing the values of the properties, in other words attributes. Only the values will be gone by the end of this operation, while the mixin definition (metadata/mixins/apple_mcintosh) stays as it is, unmodified.

Example:

response = productService.tenant(tenant).products.productId(appleId1).put({
    'mixins':{
       'apple_mcintosh': null
    }
    }, {
        headers: {
            'Authorization': 'Bearer ' + access_token,
            'Content-Type' : 'application/json'
        },
          query: {
            'partial': 'true'
          }
    }
)

assert.equal(response.status, 200);

response;

Quick verification if everything is ok:

response = productService.tenant(tenant).products.productId(appleId1).get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
          'Accept-Language': 'en'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.id, appleId1);
assert.equal(response.body.code, 'apple_lobo');
assert.equal(response.body.name, 'Apple Lobo');
assert.isUndefined(response.body.mixins.apple_mcintosh);
assert.isDefined(response.body.metadata.mixins.apple_mcintosh);

response;

Remove mixin definition

If you no longer need a certain mixin, you can simply delete its definition declared in the metadata section. Such operation also removes mixin-based properties.

response = productService.tenant(tenant).products.productId(appleId1).put({
    'metadata':{
        'mixins':{
               'apple_mcintosh': null
        }
    }
    }, {
        headers: {
            'Authorization': 'Bearer ' + access_token,
            'Content-Type' : 'application/json'
        },
          query: {
            'partial': 'true'
          }
    }
)

assert.equal(response.status, 200);

response;

Verify the results:

response = productService.tenant(tenant).products.productId(appleId1).get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
          'Accept-Language': 'en'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.id, appleId1);
assert.equal(response.body.code, 'apple_lobo');
assert.equal(response.body.name, 'Apple Lobo');
assert.isUndefined(response.body.metadata.mixins.apple_mcintosh);
assert.isUndefined(response.body.mixins.apple_mcintosh);

response;

Remove single translation

You can remove all translations, mixin definitions, but what about one single translation? Recall the example for a single property update. You use the PUT method and nullify the property to remove a property value that is no longer needed.

Be reminded that you should use the Content-Language header to indicate the language, so that a proper translation will be removed, while other translations remain unmodified.

See and run the code sample to see what's required here and to check the outcomes of this action:

response = productService.tenant(tenant).products.productId(appleId1).put({
    "name": null
    }, {
    headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
        'Content-Language' : 'pl'
    },
          query: {
            'partial': 'true'
          }
    }
)

assert.equal(response.status, 200);

response;

Verification:

response = productService.tenant(tenant).products.productId(appleId1).get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'hybris-languages' : '*'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.id, appleId1);
assert.equal(response.body.code, 'apple_lobo');
assert.isDefined(response.body.name.en);
assert.isUndefined(response.body.name.pl);
assert.isDefined(response.body.description);


response;

Remove all translations

Briefly speaking, you use the PUT method and nullify all properties that contain translations. Compare this example with the previous one. The only difference is that the current example does not have any language-related header.

Consider two cases:

  • Do not use the Content-Language header if you want to remove all translations
  • Always use the Content-Language header if you want to remove specific translation in a certain language

Example:

response = productService.tenant(tenant).products.productId(appleId1).put({
    "description": null
    }, {
        headers: {
            'Authorization': 'Bearer ' + access_token,
            'Content-Type' : 'application/json'
        },
          query: {
            'partial': 'true'
          }
    }
)

assert.equal(response.status, 200);

response;

Verification:

response = productService.tenant(tenant).products.productId(appleId1).get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'hybris-languages' : '*'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.id, appleId1);
assert.equal(response.body.code, 'apple_lobo');
assert.isUndefined(response.body.description);


response;

Data validation in partial update

Partial update does not only modify selected properties of a product, but it also involves a data validation.

Before saving the results, the effective product data is validated against the schema. If the partial update operation caused object to be invalid, then it will not be successfully saved and you will retrieve the 400 error.

Do a final check and see if this is true. Try to remove the required name property from the product:

response = productService.tenant(tenant).products.productId(appleId1).put({
        name: null
    }, {
        headers: {
            'Authorization': 'Bearer ' + access_token,
            'Content-Type' : 'application/json'
        },
        query: {
            'partial': 'true'
        }
    }
)

assert.equal(response.status, 400);

response.body.message;


Publishing

You can create products as published products, or unpublished products. Create unpublished products that are not ready for customers yet, and then decide when to publish them. You can update the products at any time. In summary:

  • Products are created as unpublished products, or published products
  • Published products can be unpublished any time
  • Unpublished products can be published any time
  • If you publish an unpublished product without changing any of the other product properties, your role needs only the hybris.product_publish scope. You can run a partial update since not all of the product attributes need updating. With a partial update, only the values you wish to change are sent. In this case, the published property is set to true which publishes the product.
  • To perform a full update, you need the hybris.product_update scope. Note the following two cases:
    • If you do not change the published property, then you only need the hybris.product_update scope.
    • If you change some of the product's properties and the published property, then you need these two scopes:
      • The hybris.product_update scope allows you to update all properties, except the published property.
      • The hybris.product_publish or the hybris.product_unpublish scopes allow you to publish the product, or unpublish it.



Setup

Assertion

Define variable assert:

assert = chai.assert;
Get access token

To perform access-restricted operations, you need an access token. For this purpose, create an API Client for the oAuth2 service:

API.createClient('oAuth2Service',
'/services/eu/oauth2/v1/api.raml');

Get the token:

tenant = projectIdPlaceholder;

AccessToken = oAuth2Service.token.post({
  'client_id' : clientIdPlaceholder,
  'client_secret':clientSecretPlaceholder,
  'grant_type' : 'client_credentials',
  'token_type': 'Bearer',
  'scope': 'hybris.tenant='+tenant+ ' hybris.product_read_unpublished hybris.product_create hybris.product_update hybris.product_delete hybris.product_publish hybris.product_unpublish hybris.product_delete_all'
});
AccessTokenWithoutReadUnpublishedScope = oAuth2Service.token.post({
  'client_id' : clientIdPlaceholder,
  'client_secret':clientSecretPlaceholder,
  'grant_type' : 'client_credentials',
  'token_type': 'Bearer',
  'scope': 'hybris.tenant='+tenant+ ' hybris.product_create hybris.product_update hybris.product_delete'
});

To make the calls simple and the code clean, assign the access tokens to variables:

access_token = AccessToken.body.access_token;
access_token_without_unpublished = AccessTokenWithoutReadUnpublishedScope.body.access_token;

Create an API client for the Product service

API.createClient('productService',
'/services/eu/product/v2/api.raml');

Cleanup

Before creating new products for the tutorial examples, delete all the products in the project:

response = productService.tenant(tenant).products.delete({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
);

assert.equal(response.status, 204);
response;

New products unpublished by default

If you create a product without the published attribute in the product definition, the product is unpublished and the customer cannot see it. By default, all new products are unpublished, unless you specifically indicate to publish it with the published attribute.

response = productService.tenant(tenant).products.post({
    'code': 'apple_lobo',
    'name': 'Lobo',
  'description': 'A McIntosh-style apple from Canada, generally believed to be better all-round than its parent.'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Rype' : 'application/json',
      'Content-Language': 'en'
      }
    }
)
assert.equal(response.status, 201);
productId = response.body.id;

response;

Your customer can search for the product you just created, but it won't be found because it isn't published yet. No anonymous user can find it until it is published.

response = productService.tenant(tenant).products.productId(productId).get({
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token_without_unpublished,
        'Content-Type' : 'application/json',
      'Accept-Language': 'en'
      }
    }
)

assert.equal(response.status, 403);

New products unpublished by choice

To choose to create new products as unpublished, set the published attribute in the product definition to false. The result is the same as the example above, but anyone reading the product definition realizes that the product is unpublished.

response = productService.tenant(tenant).products.post({
    'code': 'apple_jonagold',
    'name': 'Jonagold',
  'published': false,
  'description': 'Jonagold has a green-yellow basic color with crimson, brindled covering colour. The apple has a fluffily crisp fruit. It is juicy and aromatic and has a sweet-sour taste.'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    }
)

assert.equal(response.status, 201);

response;

If you search for the product as an anonymous user, it is not found since the product is currently unpublished.

response = productService.tenant(tenant).products.productId(productId).get({
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token_without_unpublished,
        'Content-Type' : 'application/json',
      'Accept-Language': 'en'
      }
    }
)

assert.equal(response.status, 403);

Publish a product

If you have a product that is not published, such as the example Lobo apples, you can publish the product at any time once they become available. After publishing the product, customers can find them with a search, and purchase them.

response = productService.tenant(tenant).products.productId(productId).put({
    'name': 'Lobo',
  'published': true
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      },
    query: {
        'partial': 'true'
    }
    }
)
assert.equal(response.status, 200);

response;

Assume the role of an anonymous user and verify that you can search for the product and find it.

response = productService.tenant(tenant).products.productId(productId).get({
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token_without_unpublished,
        'Content-Type' : 'application/json',
      'Accept-Language': 'en'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.published, true);

response;

Create published product

If you create a product definition for a product that is already in stock and want to start selling it right away, set the published property to true: published=true. Customers can search for the product and find it from the moment it is created.

response = productService.tenant(tenant).products.post({
    'code': 'apple_spartan',
    'name': 'Spartan',
  'published': true,
  'description': 'The Spartan is an apple cultivar developed by Dr. R.C Palmer and introduced in 1936 from the Federal Agriculture Research Station in Summerland, British Columbia, now known as the Pacific Agri-food Research Centre - Summerland.'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    }
)

assert.equal(response.status, 201);

response;

Verify the product is found as an anonymous user.

response = productService.tenant(tenant).products.productId(productId).get({
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token_without_unpublished,
        'Content-Type' : 'application/json',
      'Accept-Language': 'en'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.published, true);

response;

Unpublish product

At any time a product becomes unavailable, you can unpublish the product without deleting it, in case the product will be in stock again later. Set the published attribute to false and the product is no longer available to customers, yet the definition is still available for future use.

response = productService.tenant(tenant).products.productId(productId).put({
    'name': 'Lobo',
  'published': false
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      },
    query: {
        'partial': 'true'
    }
    }
)

assert.equal(response.status, 200);

response;

Assume the role of the anonymous user one last time and verify the Lobo apple product is unpublished and concealed from customers.

response = productService.tenant(tenant).products.productId(productId).get({
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token_without_unpublished,
        'Content-Type' : 'application/json',
      'Accept-Language': 'en'
      }
    }
)

assert.equal(response.status, 403);

Retrieve a published product

Retrieve products either with an authorization token, or without. To retrieve products without using an access token, the tenant name from the service URL is used. Without authorization, only those products that are currently published are retrieved, and unpublished products remain hidden. This enables your customers to browse the shop for available items without having to log in, or create an account.

Create one published and one unpublished product.

response = productService.tenant(tenant).products.post({
    'code': 'pear_conference',
    'name': 'Conference',
  'published': true,
  'description': 'The Conference pear is a medium sized pear with an elongated bottle, similar in appearance to the Bosc pear'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    }
)
assert.equal(response.status, 201);
response;
conferencePear = response.body.id;
response = productService.tenant(tenant).products.post({
    'code': 'pear_williams',
    'name': 'Williams',
  'published': false,
  'description': 'The pear exhibits a pyriform pear shape, with a rounded bell on the bottom half of the fruit, and then a definite shoulder with a smaller neck or stem end.'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    }
)
assert.equal(response.status, 201);
response;
williamsPear = response.body.id;

Run the GET request without using an authorization token, and check the results. Only the published products should be retrieved.

response = productService.tenant(tenant).products.get({
    }, {
      headers: {
        'Content-Type' : 'application/json',
      'Accept-Language': 'en'
      }
    }
)
assert.equal(response.status, 200);
response;

Retrieve published and unpublished products

When you use scopes for retrieve requests in the Product service, the use of the proper tenant is crucial. Since the scope is tenant-specific, the proper tenant must be combined with the proper scopes. The tenant provided in the access token must be the same as the one specified in the request URL. The service validates that the correct scopes and tenant combination is used.

If the name of the tenant given in the authorization token is identical to the one provided in the request URL, the service handles the request without any issues. If the token holds a tenant name that is different from the one carried by the URL, the service returns an error.

In this example, retrieve a product while the tenant name in the authorization token is identical to the one located in the URL. The request should retrieve all products you have previously created in this tutorial.

response = productService.tenant(tenant).products.get({
      'sort'      : 'name.en:asc'
    }, {
      headers: {
        'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)
assert.equal(response.status, 200);
response;

The retrieval was successful. Next, use a tenant in the token that is not identical to the one given in the URL, and check the results.

response = productService.tenant('ancient_tenant').products.get({
      'sort'      : 'name.en:asc'
    }, {
      headers: {
        'Accept-Language': 'en',
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',

      }
    }
)
assert.equal(response.status, 400);
response.body.message;


Extensibility

Each product is identified by a few basic attributes. However, thinking about your customers you may want to enhance the product description by adding customized attributes that would, in a more detailed way, describe the product characteristics. By such customized extra attributes you enhance products descriptions and you can target the customers groups with significantly improved accuracy.

Setup

Assertion

Define variable assert:

assert = chai.assert;
expect = chai.expect;
Get access token

To perform any operations with a specific service, you always need an access token. For this purpose, create an API Client for the oAuth2 service:

API.createClient('oAuth2Service',
'/services/eu/oauth2/v1/api.raml');

Now, get the token:

tenant = projectIdPlaceholder;

AccessToken = oAuth2Service.token.post({
  'client_id' : clientIdPlaceholder,
  'client_secret':clientSecretPlaceholder,
  'grant_type' : 'client_credentials',
  'token_type': 'Bearer',
  'scope': 'hybris.tenant='+tenant+ ' hybris.product_read_unpublished hybris.product_create hybris.product_update hybris.product_delete hybris.product_publish hybris.schema_manage hybris.product_delete_all'
});

To make the calls simple and the code clean, assign the id of the returned object to a variable:

access_token = AccessToken.body.access_token;
Create API client for Product service
API.createClient('productService',
'/services/eu/product/v2/api.raml');
Create API client for Schema repository service
API.createClient('schemaService', '/services/eu/schema/v1/api.raml');

Cleanup

Before creating new products for the tutorial examples, delete all products in project:

response = productService.tenant(tenant).products.delete({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
);

assert.equal(response.status, 204);
response;

Create a product with all additional attributes

You have a product, but the basic product information can be a little sketchy. You may want to add some additional attributes to it. But to do so, you need to design a schema first. The schema is a declaration of attributes that you intend to use to improve product description.

schema = 'apple_mcintosh-version1.json';
response = schemaService.tenant(tenant).schema(schema).get({
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)
if(response.status == 404){
  response = schemaService.tenant(tenant).schema(schema).post({
      '$schema': 'http://json-schema.org/draft-04/schema#',
      'type': 'object',
      'properties':{
          'flavor': {
              'type':'string'
          },
          'colour': {
              'type':'string'
          }
      }
    },{
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    });
    assert.equal(response.status, 201);
    response.body ;
  }

Create a product: an apple. This will be a Jubilee apple, which is a McIntosh-style apple. To say about an apple: "it is Jubilee apple" sounds a little dry. Wouldn't you like to add some additional description that could wake up your customers imagination?

Use a mixin-based property to add some spice to the basic product description. Each apple has some color, each apple has some flavor. Why not tell it your customers?

Previously, you created a schema that declared the required attributes: flavor and colour. Now, in real life it is solely up to you to define as many attributes as you need in your schema, and than to use such schema to create products with extended information. For now, take a look at the Jubilee apple described with help of two mixin-based attributes.

response = productService.tenant(tenant).products.post({
        'code': 'apple_jubilee_apple_mixin',
        'name': 'Jubilee apple',
    'published': true,
      'description': 'A modern cultivar of dessert apple, which was developed in the Canadian province of British Columbia by the "Summerland Research Station".',
    'metadata': {
                'mixins':{
                  'apple_mcintosh':    'https://api.eu.yaas.io/hybris/schema/v1/'+tenant+'/apple_mcintosh-version1.json'
          }
     },
    'mixins': {
                    'apple_mcintosh': {
                    'flavor': 'sweet',
                'colour': 'red'
                }
     }
}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    });

assert.equal(response.status, 201);
appleId1 = response.body.id;

response;

Run the code sample and see the results:

response = productService.tenant(tenant).products.productId(appleId1).get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Accept-Language': 'en'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.name, 'Jubilee apple');
assert.equal(response.body.mixins.apple_mcintosh.flavor, 'sweet');
assert.equal(response.body.mixins.apple_mcintosh.colour, 'red');

response;

Create a product with selected additional attributes

This case is similar to the previous one. You have a schema declaring multiple attributes. But when it comes to creating a real product, you realize that you actually do not need all attributes you have predefined in your mixin schema. You really need only a few of them, or even just a single one. Here is such a case: use just one attribute out of all declared in the mixin schema.

As you can see you can use all mixin-based attributes, or you can just use a few selected ones. Generally, you use only those you need for a particular product.

response = productService.tenant(tenant).products.post({
        'code': 'apple_spartan_mixin',
        'name': 'Spartan',
    'published': true,
      'description': 'The Spartan is an apple cultivar developed by Dr. R.C Palmer and introduced in 1936 from the Federal Agriculture Research Station in Summerland, British Columbia, now known as the Pacific Agri-food Research Centre - Summerland.',
    'metadata': {
                'mixins':{
                  'apple_mcintosh':    'https://api.eu.yaas.io/hybris/schema/v1/'+tenant+'/apple_mcintosh-version1.json'
          }
     },
    'mixins': {
                    'apple_mcintosh': {
                    'colour': 'red'
                }
     }
  }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    });


assert.equal(response.status, 201);
appleId2 = response.body.id;

response;

Run the code sample and see the results:

response = productService.tenant(tenant).products.productId(appleId2).get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Accept-Language': 'en'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.name, 'Spartan');
assert.equal(response.body.mixins.apple_mcintosh.colour, 'red');

response;

Create a product with an attribute not declared in mixins

Generally, you can add an attribute to the product, even if it has not been declared in the mixin schema. However, this is not recommended. Imagine you start doing things this way, and keep adding attributes not declared in schemas. Very soon you lose track of what you really have and where. Chaotic, not well-organized structures - it would not really help you in effective product management. Nevertheless, it is possible, so better check the example and see how it works:

response = productService.tenant(tenant).products.post({
        'code': 'apple_lobo',
        'name': 'Lobo',
    'published': true,
  'description': 'A McIntosh-style apple from Canada, generally believed to be better all-round than its parent.',
    'metadata': {
                'mixins':{
                  'apple_mcintosh':    'https://api.eu.yaas.io/hybris/schema/v1/'+tenant+'/apple_mcintosh-version1.json'
          }
     },
    'mixins': {
                    'apple_mcintosh': {
                    'flavor': 'sweet',
                'colour': 'red',
                'crunch': 'crisp'
                }
     }
}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
    }
    });

assert.equal(response.status, 201);
appleId3 = response.body.id;

response;

The operation with a status code 201 was successful. To be sure, however, retrieve the newly created product. The schema did not declare crunch attribute, but it is added to the product upon its creation.

response = productService.tenant(tenant).products.productId(appleId3).get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Accept-Language': 'en'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.name, 'Lobo');
assert.equal(response.body.mixins.apple_mcintosh.flavor, 'sweet');
assert.equal(response.body.mixins.apple_mcintosh.colour, 'red');
assert.equal(response.body.mixins.apple_mcintosh.crunch, 'crisp');

response;

Update additional attributes in product

You have your products and keep selling them to your customers. But in time some property of your product may change. Change may be huge, or it may be slight. But in any case it requires you to modify relevant attribute by assigning it a new value. You do not need to remove your product and make a new one. The only thing to be done is a simple update operation where you send the modified attribute value. Check the payload example and run the script to see the outcome of this action.

Note the PUT method used for this update and that you still need to use the appropriate mixin schema. Additionally, there is a partial query parameter used in this update. It is necessary if you want the payload to include only those attributes you want to modify.

response = productService.tenant(tenant).products.productId(appleId3).put({
        'name': 'Lobo',
    'metadata': {
                'mixins':{
                  'apple_mcintosh':    'https://api.eu.yaas.io/hybris/schema/v1/'+tenant+'/apple_mcintosh-version1.json'
          }
     },
    'mixins': {
                    'apple_mcintosh': {
                'colour': 'red-purple'
                }
     }
}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      },
    query: {
        'partial': true
      }
    });

assert.equal(response.status, 200);

response;

Check the result of this update:

response = productService.tenant(tenant).products.productId(appleId3).get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Accept-Language': 'en'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.name, 'Lobo');
assert.equal(response.body.mixins.apple_mcintosh.flavor, 'sweet');
assert.equal(response.body.mixins.apple_mcintosh.colour, 'red-purple');
assert.equal(response.body.mixins.apple_mcintosh.crunch, 'crisp');

response;

Delete mixin-based attributes from a product

You can add new attributes, you can modify the value of your attributes, and in the end you can also remove them. From the product point of view, any change you make to its attributes is a product update, thus the PUT operation is used here.

Assume you want to remove all mixin-based attributes from a product. This is pretty simple operation, but it needs good grasp. You send the payload that defines the product the way you want it to be. After the whole operation the product definition is replaced with a new one that you have just sent, as shown in the example.

Note that all other data must be the same as in the original product definition. Keep code, name, published, description attributes the same way as they were before the operation.

response = productService.tenant(tenant).products.productId(appleId3).put({
        'code': 'apple_lobo',
        'name': 'Lobo',
    'published': true,
      'description': 'A McIntosh-style apple from Canada, generally believed to be better all-round than its parent.'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    });

assert.equal(response.status, 200);

response;

Verify that the mixin attributes are no longer in the product:

response = productService.tenant(tenant).products.productId(appleId3).get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Accept-Language' : 'en'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.name, 'Lobo');
expect(response.body.mixins).to.be.empty;

response;

Create attributes with no schema reference

You have seen several scenarios where schemas are used while working with products. You could also see a metadata section in the payload. This was the place for a schema reference. What if you forget adding this section, but you still provide a mixins section and custom attributes for the product? As you can assume, it does not end very well, since the result will be most certainly an error.

response = productService.tenant(tenant).products.post({
    'code': 'apple_lobo',
    'name': 'Lobo',
  'published': true,
  'description': 'A McIntosh-style apple from Canada, generally believed to be better all-round than its parent.',
  'mixins': {
              'apple_mcintosh': {
                'flavor': 'sweet',
            'colour': 'red'
              }
  }
},{
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
    }
    });

assert.equal(response.status, 400);

response.body.message;

Create a product with additional properties while not allowed

Create another schema. The schema will be used in the next example. This time you are going to use the additionalProperties attribute. This attribute can be used in the schema, or not. It accepts boolean values like true and false, which can have two outcomes:

  • true: If you set this attribute to true, you will be able to add additional properties while creating the product, even if they are not declared in the schema. This is a default behavior. The same will happen if you do not add this attribute at all in your schema.
  • false: If the additionalProperties in your schema is set to false, you cannot add any new properties while creating your product. Only those declared in the schema are allowed. Any attempt will result in bad request error. This setting is actually recommended as it can prevent you from creating additional attributes in the product without declaring them in the mixin schema.

The previously used schema 'version1' does not have additionalProperties attribute, so outcome was the same as if it was set to true. But this time, use a 'version2' schema with the additionalProperties attribute set to false:

schema = 'apple_mcintosh-version2.json';
response = schemaService.tenant(tenant).schema(schema).get({
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)
if(response.status == 404){
  response = schemaService.tenant(tenant).schema(schema).post({
      '$schema': 'http://json-schema.org/draft-04/schema#',
      'type': 'object',
      'properties':{
          'flavor': {
              'type':'string'
          },
          'colour': {
              'type':'string'
          }
      },
      'additionalProperties': false
    },{
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    });
    assert.equal(response.status, 201);
  }

response;

A mixin schema defines the number of attributes, their names, and types. Create a product that has two mixin-based attributes, but it also has one extra attribute: crunch, which is not mixin-based.

A question arises: what will happen if you try to create such product. Run the script and find out. Just make sure you notice that the following example uses the 'version2' schema that includes the following entry: 'additionalProperties': false.

response = productService.tenant(tenant).products.post({
    'code': 'apple_jonagold',
    'name': 'Jonagold',
  'published': true,
  'description': 'Jonagold has a green-yellow basic color with crimson, brindled covering colour. The apple has a fluffily crisp fruit. It is juicy and aromatic and has a sweet-sour taste.',
      'metadata': {
                'mixins':{
                  'apple_mcintosh':    'https://api.eu.yaas.io/hybris/schema/v1/'+tenant+'/apple_mcintosh-version2.json'
          }
     },
  'mixins': {
              'apple_mcintosh': {
                'flavor': 'sweet-sour',
            'colour': 'green-yellow',
            'crunch': 'crisp'
              }
  }
},{
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
    }
    });

assert.equal(response.status, 400);

response.body.message;

When the additionalProperties in your schema is set to false, you cannot add any new properties while creating a product. Any attempt will result in bad request error. This setting is actually recommended as it can prevent you from creating additional attributes in the product without declaring them in the mixin schema.

Now, create the same product, but with the additional attributes declared in schema.

response = productService.tenant(tenant).products.post({
    'code': 'apple_jonagold',
    'name': 'Jonagold',
  'published': true,
  'description': 'Jonagold has a green-yellow basic color with crimson, brindled covering colour. The apple has a fluffily crisp fruit. It is juicy and aromatic and has a sweet-sour taste.',
      'metadata': {
                'mixins':{
                  'apple_mcintosh':    'https://api.eu.yaas.io/hybris/schema/v1/'+tenant+'/apple_mcintosh-version1.json'
          }
     },
  'mixins': {
              'apple_mcintosh': {
                'flavor': 'sweet-sour',
            'colour': 'green-yellow',
              }
  }
},{
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
    }
    });

assert.equal(response.status, 201);
appleId4 = response.body.id;

response;

Run the code sample and see the results:

response = productService.tenant(tenant).products.productId(appleId4).get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Accept-Language': 'en'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.name, 'Jonagold');
assert.equal(response.body.mixins.apple_mcintosh.flavor, 'sweet-sour');
assert.equal(response.body.mixins.apple_mcintosh.colour, 'green-yellow');

response;


Localization

You may want to communicate information on your products not only to the customers who speak your language, but also to all those who live in other parts of the world, speaking other languages. Localization is the feature that can help you to achieve this.

You always describe a product with words that come from some language. But what if you want to reach out for customers who are spread geographically in multiple countries, and speak multiple languages? With localization you can keep several language versions that describe the product to the customers. You can use these versions to display the information in the very language your customer requires.

The localization feature covers also the concept of the fallback language. If by any reason you do not have a certain description for a particular product in the customer-selected language, you can use a fallback language to make sure the customer can always see the product-related information, even if it is displayed not in the language the customer originally asked for.

Setup

Assertion

Define variable assert:

assert = chai.assert;
Get access token

To perform any operations with a specific service, you always need an access token. For this purpose, create an API Client for the oAuth2 service:

API.createClient('oAuth2Service',
'/services/eu/oauth2/v1/api.raml');

Get the token:

tenant = projectIdPlaceholder;

AccessToken = oAuth2Service.token.post({
  'client_id' : clientIdPlaceholder,
  'client_secret':clientSecretPlaceholder,
  'grant_type' : 'client_credentials',
  'token_type': 'Bearer',
  'scope': 'hybris.tenant='+tenant+ ' hybris.product_read_unpublished hybris.product_create hybris.product_update hybris.product_delete hybris.product_publish hybris.product_unpublish hybris.product_delete_all'
});
AccessTokenWithoutReadUnpublishedScope = oAuth2Service.token.post({
  'client_id' : clientIdPlaceholder,
  'client_secret':clientSecretPlaceholder,
  'grant_type' : 'client_credentials',
  'token_type': 'Bearer',
  'scope': 'hybris.tenant='+tenant+ ' hybris.product_create hybris.product_update hybris.product_delete'
});

To make the calls simple and the code clean, assign the id of the returned object to a variable:

access_token = AccessToken.body.access_token;
access_token_without_unpublished = AccessTokenWithoutReadUnpublishedScope.body.access_token;
Create API client for Product service
API.createClient('productService',
'/services/eu/product/v2/api.raml');

Cleanup

Before creating new products for the tutorial examples, delete all products in the project:

response = productService.tenant(tenant).products.delete({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
);

assert.equal(response.status, 204);
response;

New product for localization

At the beginning you would want to create products that you can offer to your customers. It is obvious that anything you write is written in some language. Every product is described by the attributes holding the values created in a certain language. The product attributes must always have a value, so that your customer can always have access to the product information.

response = productService.tenant(tenant).products.post({
    'code': 'apple_lobo',
    'name': 'Apple Lobo',
  'published': true,
  'description': 'A McIntosh-style apple from Canada, generally believed to be better all-round than its parent.'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    }
)

assert.equal(response.status, 201);
productIdLobo = response.body.id;

response;

New language version for your products

As your business grows, you may want to reach customers from other language areas in the world. You may want to provide your customers the product information in their own language. For example, you want to add Polish language as the new language version of the product description.

There are several ways to create a localized information about your product. Check the next two examples.

Add new language by using header and payload

As this example shows, one way to do so is to use the Content-Language header to carry over the information about new language used for the product description. The header tells you what language it is. The actual content in this language should be sent in the payload.

What is important, you can update only selected properties this way simply by using partial update. Make sure that in your query you set the partial URL query parameter to true.

You can later add another language version to your product description. However, you can only add one new language version per call. Adding, for example, five language versions requires five calls, similar to the one presented in the example.

response = productService.tenant(tenant).products.productId(productIdLobo).put({
  'name': 'Jabłko Lobo',
    'description': 'Jabłko w stylu McIntosh z Kanady, na ogół uważa się, że jest bardziej uniwersalne od swoich poprzedników.'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language' : 'pl'
      },
    query: {
        'partial': 'true'
    }
    }
)
assert.equal(response.status, 200);

response;

Add new language by using payload only

The time has come to expand even further, to all European countries or even conquer other continents with your products. Count the number of languages first. Many. It is, however, not an issue for your business. You can add as many language versions for your product as you like.

Last time you added a language version with help of some header, but you only could add one language per call. What if you like to add, for example another two language versions for you product. You have something you can offer in France and Sweden, and you like to add those two languages in one step to make the life of your customers more convenient and the whole purchase process smooth and easy. Here is a tip: the same way you can add 10 language versions for a product, which - assuming a huge number of products - could be a time saver.

Forget language-related headers. All you need is the proper payload that would contain all the information in required languages. One warning: you cannot do a partial update here. You rather need to put all the product attributes in the payload and you need to provide all the language versions for these attributes.

response = productService.tenant(tenant).products.productId(productIdLobo).put({
    'code': 'apple_lobo',
    'name': {
     'en': 'Apple Lobo',
     'pl': 'Jabłko Lobo',
     'fr': 'Pomme Lobo',
     'se': 'äpple Lobo'
  },
  'published': true,
  'description': {
     'en': ' A very popular commercial variety, with a good flavour. Inherits many of the good qualities of its parents Jonathan and Golden Delicious.',
     'pl': 'Bardzo popularna odmiana handlowa, z dobrym smakiem. Dziedziczy wiele dobrych cech swoich rodziców Jonathan i Golden Delicious.',
     'fr': ' Une variété commerciale très populaire, avec une bonne saveur. Hérite de nombreuses des bonnes qualités de ses parents, Jonathan et Golden Delicious.',
     'se': ' En mycket populär kommersiell sort, med en god smak. Ärver många av de goda egenskaperna hos sina föräldrar Jonathan och Golden Delicious.'
      }
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)

assert.equal(response.status, 200);
productIdJonagold = response.body.id;

response;

Retrieve all language versions

Time for using headers again. You have your products. You have your language version and want to see those at once, in order to edit them. You must retrieve the products and their language versions from the tenant. The hybris-languages header is quite handy to accomplish this task. You just need to set the proper value to the header: hybris-languages: *, and voila: all language versions of the product information start coming as requested. It is up to you to use all those language versions in such a way that best suits the needs of your business.

response = productService.tenant(tenant).products.productId(productIdLobo).get({
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token_without_unpublished,
        'Content-Type' : 'application/json',
      'hybris-languages' : '*'
      }
    }
)

assert.equal(response.status, 200);
assert.include(response.body.name.en, 'Apple Lobo');
assert.include(response.body.name.pl, 'Jabłko Lobo');
assert.include(response.body.name.fr, 'Pomme Lobo');
assert.include(response.body.name.se, 'äpple Lobo');
assert.include(response.body.description.en, 'A very popular commercial variety');
assert.include(response.body.description.pl, 'Bardzo popularna odmiana handlowa');
assert.include(response.body.description.fr, 'Une variété commerciale très populaire');
assert.include(response.body.description.se, 'En mycket populär kommersiell sort');

response;

Retrieve one language version

You may say: OK, but what I really want is one language version retrieved, so I can show it to my customers. Well, no problem there. You still need to use a header, but this time it is a new one: Accept-Language.

To be more precise, with this header you can define a set of languages, indicating the preferred ones to be applied for the response. For each language you can add a quality value that would represent the user preference for the language. A request without any Accept-Language header field implies that the user agent will accept any language in response.

Use previously created product: Lobo. The example: Accept-Language: se, would mean: "I prefer Swedish".

response = productService.tenant(tenant).products.productId(productIdLobo).get({
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token_without_unpublished,
        'Content-Type' : 'application/json',
      'Accept-Language' : 'se'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.name, 'äpple Lobo');
assert.include(response.body.description, 'En mycket populär kommersiell sort');

response;

Retrieve the product with fallback language

You can design nice product information in any language version. But what would happen if at some point some language version is missing. In such case, a customer requesting the product would not see anything. And the blank space is not what you want in such situations.

To prevent this confusion from happening, you can set up a sequence of languages that will be used in case the first one is missing somewhere. In other words, if a product description in Faroese language does not exist, then use Danish. And if there is no Danish description, then use Swedish, and so on. Simply speaking, your customer never is left with blank space, and is always shown some product information. It is of course your task to set up such language sequence that would in a best possible way support your customers. It is something that is impossible to predict in this example, as it always depends on your specific location and the primary language of your customer. For example, there can be another language that is similar, that could be used as the next backup language: as it was in case of Faroese and Danish, and than Swedish.

Use previously created product: Lobo. The example: Accept-Language: da, en-gb;q=0.8, en;q=0.7, would mean: "I prefer Danish, but I will also accept British English and other types of English".

response = productService.tenant(tenant).products.productId(productIdLobo).get({
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token_without_unpublished,
        'Content-Type' : 'application/json',
      'Accept-Language' : 'da, en-gb;q=0.8, pl;q=0.7'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.name, 'Jabłko Lobo');
assert.include(response.body.description, 'Bardzo popularna odmiana handlowa');

response;

The order of the fallback languages is determined by the quality factor: q=0,8. It does not depend on the sequence appearance in the header. The quality value defaults to "q=1". For example: Accept-Language: , en;q=0.7, en-gb;q=0.8, da, should be interpreted as "I prefer Danish, but will accept British English and other types of English."

Retrieve partially localized product

Check what happens if you have only one attribute localized in a certain language while other attributes are not. For this example, make a quick partial update to the product, by adding a Spanish value for the name attribute.

response = productService.tenant(tenant).products.productId(productIdLobo).put({
  'name': 'La manzana Lobo'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language' : 'es'
      },
    query: {
        'partial': 'true'
    }
    }
)
assert.equal(response.status, 200);

response;

There should be a Spanish version for the name attribute available. However, other attributes do not have this version. What happens if you now try to retrieve the product in Spanish where only one single attribute is localized in that language. Perhaps you run the script and find out?

response = productService.tenant(tenant).products.productId(productIdLobo).get({
    }, {
      headers: {
      'Authorization': 'Bearer ' + access_token_without_unpublished,
        'Content-Type' : 'application/json',
      'Accept-Language' : 'es'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.name, 'La manzana Lobo');

response;

Only the name attribute is returned, as it is the only attribute localized in Spanish language that you just requested.


Optimistic Locking

You should not have a problem with data consistency if only one specific person is responsible for performing updates of your products. But as your company expands, number of items available for your customers grows, one person may simply be not enough to keep everything up to date. The time may come for you to get another employee onboard who will be responsible for product updates.

This expansion, however, creates another possible scenarios that must be taken into account. Assume you have two employees: John and Bob. Both are responsible for updating a huge number of product information. In this tutorial, you will investigate possible issue and see how it can be prevented by means of optimistic locking.

Setup

Assertion

Define variable assert:

assert = chai.assert;
Get access token

To perform any operations with a specific service, you always need an access token. For this purpose, create an API Client for the oAuth2 service:

API.createClient('oAuth2Service', '/services/eu/oauth2/v1/api.raml');

Get the token:

tenant = projectIdPlaceholder;

AccessToken = oAuth2Service.token.post({
  'client_id' : clientIdPlaceholder,
  'client_secret':clientSecretPlaceholder,
  'grant_type' : 'client_credentials',
  'token_type': 'Bearer',
  'scope': 'hybris.tenant='+tenant+' hybris.product_read_unpublished hybris.product_create hybris.product_update hybris.product_publish hybris.product_delete hybris.product_delete_all'
});

To make the calls simple and the code clean, assign the id of the returned object to a variable:

access_token = AccessToken.body.access_token;
Create API client for Product service
API.createClient('productService', '/services/eu/product/v2/api.raml');

Cleanup

Before creating new products for the tutorial examples, delete all products in project:

response = productService.tenant(tenant).products.delete({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
);

assert.equal(response.status, 204);
response;

Creating data for examples

To work with this example, first create some useful data.

First product is Apple Cortland:

response = productService.tenant(tenant).products.post({
    'code': 'apple_cortland',
    'name': {'en': 'Cortland',
             'pl': 'Cortland'},
      'description': {'en':'Cortland is a cultivar of apple',
                      'de':'Cortland ist eine Sorte von Apfel'}
  }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
  });
  appleId0 = response.body.id

response;

Update product with optimistic locking

John and Bob are your two employees who are responsible for updating products. Huge amount of products. They work simultaneously, however, they both update separate set of products. Easy to say. But what would happen if Bob starts updating a product, and John gets an idea to update the same products in the same time?

Now they come to the point: John and Bob update one and the same product item simultaneously. There is the Cortland apple with some data, and both employees try to modify this data. To do so, both of them retrieve the product so that it could be updated in next step:

response = productService.tenant(tenant).products.productId(appleId0).get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
          'hybris-languages': '*'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.id, appleId0);
assert.equal(response.body.code, 'apple_cortland');
assert.equal(response.body.metadata.version, 1);
response;

Nope an important property retrieved above with all other data: version. In the example, it is version 1, which tells us the product has not been updated yet, since created.

A situation may happen that John and Bob work on the same product, the same one they have retrieved in previous step. However, while Bob decided to make a coffee first, John starts to work on his updates at once.

See John's modifications:

response = productService.tenant(tenant).products.productId(appleId0).put({
    'name': {'en': 'Apple Cortland', 'de':'Apfel Cortland'},
    'metadata':{
       'version': 1
    }
    }, {
        headers: {
            'Authorization': 'Bearer ' + access_token,
            'Content-Type' : 'application/json'
        },
        query: {
            'partial': 'true'
        }
    }
)

assert.equal(response.status, 200);

response;

Again, do not fail to notice again an important property: version. When John updated the product data, he also included version property and set the value to 1. It simply shows which version of the product John wanted to update. In this case, it was the original version.

Now, Bob returns with a coffee. As you remember, Bob and John retrieved the product item data. But John additionally updated this data already. Bob-with-the-coffee now also modifies the data and tries to update the product:

response = productService.tenant(tenant).products.productId(appleId0).put({
    'name': {'en': 'Jucy Cortland', 'de':'Saftig Cortland'},
    'metadata':{
       'version': 1
    }
    }, {
        headers: {
            'Authorization': 'Bearer ' + access_token,
            'Content-Type' : 'application/json'
        },
        query: {
            'partial': 'true'
        }
    }
)

assert.equal(response.status, 409);

response;

Check the outcome of the latest action. Bob modified the data and tried to update the product, but he got 409, a conflict exception. It is not really that surprising, as the service does everything exactly as intended.

While Bob was away for a coffee, John updated version 1 of the product, and when Bob tried to also update version 1 of the product he got exception - there can be only one version 1.

We could really be the most optimistic people and hope that John and Bob do not overwrite their updates. It is, however, much better to use Optimist Locking mechanism to make sure the product version is not overwritten by another update.

As each update increments the version number, you can be sure that if you always retrieve the latest version, you update the latest version. You can, however, update some of the previous versions as well. Also in such situation, this particular product version would be prevented from being overwritten by someone else.

Few notes and conclusions to remember:

  • If you update a product, always retrieve the latest version and add your modifications to this version.
  • Optimistic locking is optional. If you do not provide version number in your update payload, then you update the latest version by default. This also means, you do not really care that someone else updates the same product version. In some situations it is probably ok, for example if you are the only person having the right to perform the product update.
  • If you do not provide version in your update operation, you may overwrite the changes made to this product version by someone else, who concludes the update before you do.


Testing

In your development phase you most certainly create a lot of data that will not necessarily be required after development has ended. When this happens, you may want to remove all data in one fell swoop rather than to remove each product separately.

Here you will see how to remove everything at once. Do not forget to note the necessary scope that is required to perform this operation: hybris.product_delete_all.

Setup

Assertion

Define variable assert:

assert = chai.assert;
Get access token

To perform any operations with a specific service, you always need an access token. For this purpose, create an API Client for the oAuth2 service:

API.createClient('oAuth2Service',
'/services/eu/oauth2/v1/api.raml');

Now, get the token:

tenant = projectIdPlaceholder;

AccessToken = oAuth2Service.token.post({
  'client_id' : clientIdPlaceholder,
  'client_secret':clientSecretPlaceholder,
  'grant_type' : 'client_credentials',
  'token_type': 'Bearer',
  'scope': 'hybris.tenant='+tenant+ ' hybris.product_create hybris.product_delete_all hybris.product_publish hybris.product_read_unpublished'
});

To make the calls simple and the code clean, assign the id of the returned object to a variable:

access_token = AccessToken.body.access_token;
Create API client for the Product service
API.createClient('productService',
'/services/eu/product/v2/api.raml');

Cleanup

Before creating new products for the tutorial examples, delete all products in project, just to start clean:

response = productService.tenant(tenant).products.delete({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
);

assert.equal(response.status, 204);
response;

Create some data for examples

Add two simple products in this section.

A Lobo apple:

response = productService.tenant(tenant).products.post({
    'code': 'apple_lobo',
    'name': 'Lobo',
  'description': 'A McIntosh-style apple from Canada, generally believed to be better all-round than its parent.'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
        'Content-Language': 'en'
          }
    }
)

assert.equal(response.status, 201);

response;

A Cortland apple:

response = productService.tenant(tenant).products.post({
    'code': 'apple_cortland',
    'name': 'Cortland',
    'published': true,
  'description': 'One of the more successful McIntosh offspring, with all the usual characteristics, including the sweet vinous flavour.'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
        'Content-Language': 'en'
          }
    }
)

assert.equal(response.status, 201);

response;

In the next step, you will see how to delete all those products with one call. Also note, that this call will work the same way no matter how many other products you might have previously created.

Delete all products

The time has come to get rid of unwanted data and to start from scratch. Reasons are many, for example you have just ended testing the test data and you want to clean this up after the test phase has finished.

The process is quick and consists of one step really. Run delete method on the /products endpoint. Do not forget about the required scope: hybris.product_delete_all. That's all what it takes:

response = productService.tenant(tenant).products.delete({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
);

assert.equal(response.status, 204);
response;
After successful deletion, you send a special event called hybris.all-products-deleted. Have you subscribed to this event, you will be informed when all the products have been deleted. This functionality is designed for tests reasons. There is not internal reaction to it. This means that when you use this endpoint to delete all products, those products will not be deleted from the Algolia Index or Stripe Relay.

Verifying product deletion

You have had a lot of products in your tenant, and than you have removed them all. Verify it to remove any traces of doubt:

response = productService.tenant(tenant).products.get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.length, 0);

response;

As you can see by the response, the products have been successfully removed and are no longer available in the Product service.


Uniqueness

In the Basics tutorial, you create, update, and delete products. In this tutorial, you discover if you can create two products that are exactly the same, or clone a product into a new one, using the Product service.

Setup

Assertion

Define an assert variable:

assert = chai.assert;

Get an access token

To perform access-restricted operations, you need an access token. Create an API Client for the OAuth2 service:

API.createClient('oAuth2Service',
'/services/eu/oauth2/v1/api.raml');

Get the token:

tenant = projectIdPlaceholder;

AccessToken = oAuth2Service.token.post({
  'client_id' : clientIdPlaceholder,
  'client_secret':clientSecretPlaceholder,
  'grant_type' : 'client_credentials',
  'token_type': 'Bearer',
  'scope': 'hybris.tenant='+tenant+' hybris.product_read_unpublished hybris.product_create hybris.product_update hybris.product_delete hybris.product_delete_all'
});

To make the calls simple and the code clean, assign the access token of the returned object to a variable:

access_token = AccessToken.body.access_token;

Create an API client for the Product service

API.createClient('productService',
'/services/eu/product/v2/api.raml');

Cleanup

Before creating new products for the tutorial examples, clean up the project:

response = productService.tenant(tenant).products.delete({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json'
      }
    }
);
assert.equal(response.status, 204);
response;

Examples

You may wonder when two products are actually the same. The simplest, and true, answer is that two products are exactly the same if each and any of their properties have the same value. But is it possible with Product service? Well, the answer is: no. When you create a new product, the service automatically adds an id to each item. This id is always unique and cannot be changed. So, why do you need a code attribute than?

If a shop has a huge amount of data, such as hundreds or even thousands of products, there is a risk of product duplication. The service automatically creates a unique id, but it does not prevent you from adding new products with duplicated attributes. As a result, some products could have all the same attributes except for the id property.

In contrast to the product id, the code property is not added automatically, but manually. This property supports uniqueness of a product, with a meaningful value to the creator. If you add a product and your manually-created code duplicates an existing one, the service returns an error.

Create a simple product

In the first step, create a simple product.

response = productService.tenant(tenant).products.post({
    'code': 'apple_lobo',
    'name': 'Lobo',
  'description': 'A McIntosh-style apple from Canada, generally believed to be better all-round than its parent.'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    }
)

assert.equal(response.status, 201);
response;

The response includes id, link, and yrn properties. Store the id in a variable:

appleLoboId = response.body.id;

Check the value of the code property in your first product.

response = productService.tenant(tenant).products.productId(appleLoboId).get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Accept-Language': 'en'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.code, 'apple_lobo');
response;

Check the uniqueness of a new product

To check the uniqueness of a new product, create a new item using the same code value from the product created previously. Verify the service prevents you from creating a new product with an existing code.

response = productService.tenant(tenant).products.post({
    'code': 'apple_lobo',
    'name': 'jonagold',
  'published': false,
  'description': 'Jonagold has a green-yellow basic color with crimson, brindled covering color. The apple has a fluffily crisp fruit. It is juicy and aromatic and has a sweet-sour taste.'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    }
)

assert.equal(response.status, 409);

The attempt to create a product with a non-unique code value fails, and an error is returned. This is an intended behavior. To create a second product, change the code value to something unique, such as the name of the product.

response = productService.tenant(tenant).products.post({
    'code': 'apple_jonagold',
    'name': 'jonagold',
  'published': false,
  'description': 'Jonagold has a green-yellow basic color with crimson, brindled covering color. The apple has a fluffily crisp fruit. It is juicy and aromatic and has a sweet-sour taste.'
    }, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Content-Language': 'en'
      }
    }
)

assert.equal(response.status, 201);
response;

Store the id in a variable:

appleJonagoldId = response.body.id;

This time you successfully created a second product using a unique value for the code property. The service responds with the id, link, and yrn, but to see the code value, retrieve the product.

response = productService.tenant(tenant).products.productId(appleJonagoldId).get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Accept-Language': 'en'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.code, 'apple_jonagold');
response;

At this point, you have two unique products, both with unique code attributes.

Check the uniqueness of an updated product

If you create a product with a non-unique code value, the service returns a 409 error code. The same thing happens if you update a product to an existing code value. To verify this, use your two products to try and make the code attributes be equal. Run a partial update operation since you only want to update a single attribute.

response = productService.tenant(tenant).products.productId(appleJonagoldId).put({
        'code': 'apple_lobo'
    }, {
        headers: {
            'Authorization': 'Bearer ' + access_token,
            'Content-Type' : 'application/json'
        },
        query: {
            'partial': 'true'
        }
    }
)

assert.equal(response.status, 409);

Again, the service responds with a 409 error code. The product cannot be updated, since it is impossible for two products to have the same code property.

Re-run the update operation and modify the code to be unique.

response = productService.tenant(tenant).products.productId(appleJonagoldId).put({
        'code': 'apple_jonagold_extra'
    }, {
        headers: {
            'Authorization': 'Bearer ' + access_token,
            'Content-Type' : 'application/json'
        },
        query: {
            'partial': 'true'
        }
    }
)

assert.equal(response.status, 200);

Finally, check the code property of the updated product:

response = productService.tenant(tenant).products.productId(appleJonagoldId).get({}, {
      headers: {
        'Authorization': 'Bearer ' + access_token,
        'Content-Type' : 'application/json',
      'Accept-Language': 'en'
      }
    }
)

assert.equal(response.status, 200);
assert.equal(response.body.code, 'apple_jonagold_extra');
response;

The update verifies that no same code properties can exist in the same tenant, at the same time.