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 all products.
Accepted scopes:
hybris.product_read_unpublished
- required to retrieve non-public products. If not provided only published products are returned
Create new product.
Accepted scopes:
hybris.product_create
- mandatoryhybris.product_publish
- required if product is created as published (by providing published flag true )
Delete all Product entities.
Accepted scopes:
hybris.product_delete_all
- mandatory
/{tenant}/products/{productId}
Retrieves a single product.
YRN: urn:yaas:hybris:product:product:{tenant};{productId}
Accepted scopes:
hybris.product_read_unpublished
- required to retrieve non-public product
Update a single product.
Accepted scopes:
hybris.product_update
- required to update product datahybris.product_publish
- required to publish the product (by settingpublished
flag totrue
)hybris.product_unpublish
- required to unpublish the product (by settingpublished
flag tofalse
)
Delete a single Product entity.
Accepted scopes:
hybris.product_delete
- mandatory
/{tenant}/products/{productId}/media
Media files of the product.
Initialize process of creating new media file for product
Accepted scopes:
hybris.product_update
- mandatory
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}
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
Update media file metadata. Note that only part of the metadata field can be updated.
Accepted scopes:
hybris.product_update
- mandatory
Delete media identified by media ID.
Accepted scopes:
hybris.product_update
- mandatory
/{tenant}/products/{productId}/media/{mediaId}/commit
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 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
Create new product variant.
Accepted scopes:
hybris.product_create
- mandatory
Delete all variants entities for specified product id.
Accepted scopes:
hybris.product_delete
- mandatory
/{tenant}/products/{productId}/variants/{variantId}
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
Replace a single product variant.
Accepted scopes:
hybris.product_update
- required to update product variant data
Delete a single Product variant entity.
Accepted scopes:
hybris.product_delete
- mandatory
/{tenant}/products/{productId}/variants/{variantId}/media
Media files of the variant.
Initialize process of creating new media file for variant
Accepted scopes:
hybris.product_update
- mandatory
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}
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
Update product variant media file metadata. Note that only part of the metadata field can be updated.
Accepted scopes:
hybris.product_update
- mandatory
Delete media identified by media ID.
Accepted scopes:
hybris.product_update
- mandatory
/{tenant}/products/{productId}/variants/{variantId}/media/{mediaId}/commit
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 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
/{tenant}/search
Search among product and variant entities
/{tenant}/search
Search products and variants by yrn. If you would like to embed product data into requested variant then use params.variant.expand parameter with value product. Using fields parameter user can limit the properties returned with response entities. If you would like to limit the properties of product embedded in variant, use params.variant.fields parameters prefixed with product., please see the example for more details.
Accepted scopes:
hybris.product_read_unpublished
- required to retrieve non-public products and variants. If not provided only published products or variants are returned
Events
The topic owner client is: hybris.product
EVENT TYPE | DESCRIPTION | PAYLOAD SCHEMA | PAYLOAD EXAMPLE |
---|---|---|---|
product-created | Triggered when product is successfully created | schema |
|
product-updated | Triggered when product is successfully updated | schema |
|
product-deleted | Triggered when product is successfully deleted | schema |
|
variant-created | Triggered when product variant is successfully created | schema |
|
variant-updated | Triggered when product variant is successfully updated | schema |
|
variant-deleted | Triggered when product variant is successfully deleted | schema |
|
Scopes
Scopes are strings that let you specify exactly what type of access you need to resources and operations in the Product service.
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.
Scope | Description | Selected examples of use |
---|---|---|
hybris.product_read_unpublished | Use 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_create | Use this scope to create new products or variants. | Basics Extensibility Variants Localization |
hybris.product_update | Use 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_delete | Use 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_all | Use this scope to delete all products. Removing all products will also remove all variants. | Testing |
hybris.product_publish | Use 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_unpublish | Use 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_migrate | Use 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:
- 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.
- 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.
- 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.beta.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.beta.yaas.io/hybris/product/v2
- service: The identifier of the service owning the resource type, such as product in
https://api.beta.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.beta.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.beta.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.
- Usually, a singular form of the resource base path, such as product in
- id_part: The local resource identifier. There can be one or more identifiers. For example,
https://api.beta.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/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/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.
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.
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/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/product/v2/api.raml');
Create API client for Schema repository service
API.createClient('schemaService', '/services/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.beta.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.beta.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;
Pagination
You may sometimes get a huge amount of product entries and the number of results may be overwhelming. There is, however, a way to sort them out into smaller groups thanks to process called pagination. You can decide how many product entries you want to have in one page, you can also display a required page.
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/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 id of the returned object to a variable:
access_token = AccessToken.body.access_token;
Create API client for Product service
API.createClient('productService',
'/services/product/v2/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
To perform any kind of pagination, you need to have enough data to work with. To do so, create at least 17 new products. Having those results split into pages, your search becomes more specific.
for (i=0; i<17; i++) {
response = productService.tenant(tenant).products.post({
'code': 'apple_lobo' + i,
'name': 'Lobo' + i,
'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'
}
});
}
Get first page with two products
You can retrieve any particular page and your retrieved page can contain some number of products. It is totally up to you, since you define it yourself by assigning the required values to the attributes. For the purpose of the next sample, assume you want to have 2 products in 1 page.
In your query, assume you request the first page with 2 elements, which is done by assigning the appropriate values to two attributes:
pageNumber=1
pageSize=2
response = productService.tenant(tenant).products.get({
'pageNumber' : '1',
'pageSize' : '2'
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.length, 2);
var pageLinks = response.headers.link.split(",");
assert.equal(pageLinks.length, 2);
//verify if link pointing to previous page is not available in header
assert.equal(pageLinks.some(function(pageLink){
return (new RegExp('.*/products.*pageNumber=1.*rel=\"prev\"')).test(pageLink);
}), false)
//verify if link pointing to current page is available in header and contains pageNumber=1
assert.equal( pageLinks[0].search(".*/products.*pageNumber=1.*rel=\"self\"") > -1, true);
//verify if link pointing to next page is available in header and contains pageNumber=2
assert.equal( pageLinks[1].search(".*/products.*pageNumber=2.*rel=\"next\"") > -1, true);
response;
The status of 200
and the list of retrieved products is a sign of success. Since there are just two products, the return payload contains two elements. The response also includes page one, which was requested. You can check the returned page links. There is a self
link, next
link, but there is no prev
link. The first page has none.
Get two products in second page
As for now, you have seen everything in page 1. But what about the next pages. Surely there can be more products than one page can hold. Using appropriate values for the parameters, you can decide which page you want to see and how many products you want to have in it. Assume you want to have 2 products in page 2.
response = productService.tenant(tenant).products.get({
'pageNumber' : '2',
'pageSize' : '2'
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.length, 2);
var pageLinks = response.headers.link.split(",");
assert.equal(pageLinks.length, 3);
//verify if link pointing to previous page is available in header and contains pageNumber=1
assert.equal( pageLinks[0].search(".*/products.*pageNumber=1.*rel=\"prev\"") > -1, true);
//verify if link pointing to current page is available in header and contains pageNumber=2
assert.equal( pageLinks[1].search(".*/products.*pageNumber=2.*rel=\"self\"") > -1, true);
//verify if link pointing to next page is available in header and contains pageNumber=3
assert.equal( pageLinks[2].search(".*/products.*pageNumber=3.*rel=\"next\"") > -1, true);
response;
The status of 200
and the list of retrieved products is a sign of success. You may also want to verify the page links: self
, prev
, and next
. For example, being in page 2, the prev
link should bring you back to the page 1 which you have seen in previous section. The next
link would have a value of 3 and refer to the next, third page.
Call without parameters
You have used different values for parameters defining the page and number of products displayed per page. The example shows what would happen if you omit those parameters and simply try to get results without indicating which page you want to have or how many products the page should contain.
response = productService.tenant(tenant).products.get({
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.length, 16);
var pageLinks = response.headers.link.split(",");
assert.equal(pageLinks.length, 2);
//verify if link pointing to previous page is not available in header
assert.equal(pageLinks.some(function(pageLink){
return (new RegExp('.*/products.*pageNumber=1.*rel=\"prev\"')).test(pageLink);
}), false)
//verify if link pointing to current page is available in header and contains pageNumber=1
assert.equal( pageLinks[0].search(".*/products.*pageNumber=1.*rel=\"self\"") > -1, true);
//verify if link pointing to next page is available in header and contains pageNumber=2
assert.equal( pageLinks[1].search(".*/products.*pageNumber=2.*rel=\"next\"") > -1, true);
response;
As always, successful operation is signalized by a status code 200
. Additionally, you will see now the page 1. This is a default behavior in situations, when you do not use any parameters. One page holds 16 product entries by default, if pageSize is not modified. As a result, you'll get 16 products in response, which is everything that the page 1 is able to contain.
parameter only
While trying out many possible situations by using just one parameter, you may come to this: you only specify which page you want. But how many products will be there? To answer it, the page can hold 16 products by default.
response = productService.tenant(tenant).products.get({
'pageNumber' : '1'
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.length, 16);
var pageLinks = response.headers.link.split(",");
assert.equal(pageLinks.length, 2);
//verify if link pointing to previous page is not available in header
assert.equal(pageLinks.some(function(pageLink){
return (new RegExp('.*/products.*pageNumber=1.*rel=\"prev\"')).test(pageLink);
}), false)
//verify if link pointing to current page is available in header and contains pageNumber=1
assert.equal( pageLinks[0].search(".*/products.*pageNumber=1.*rel=\"self\"") > -1, true);
//verify if link pointing to next page is available in header and contains pageNumber=2
assert.equal( pageLinks[1].search(".*/products.*pageNumber=2.*rel=\"next\"") > -1, true);
response;
It seems you have received 16 products in the requested page. Remember that no pageSize attribute has been specified and the end result is the effect of default behavior.
parameter only
You may also wonder about something different. What if you do not specify any page number, but only the size of the page? Also here, there is some default answer: page 1. Of course, you get the 2 products in a returned page. To see any other page, you must use the pageNumber parameter.
response = productService.tenant(tenant).products.get({
'pageSize' : '2'
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.length, 2);
var pageLinks = response.headers.link.split(",");
assert.equal(pageLinks.length, 2);
//verify if link pointing to previous page is not available in header
assert.equal(pageLinks.some(function(pageLink){
return (new RegExp('.*/products.*pageNumber=1.*rel=\"prev\"')).test(pageLink);
}), false)
//verify if link pointing to current page is available in header and contains pageNumber=1
assert.equal( pageLinks[0].search(".*/products.*pageNumber=1.*rel=\"self\"") > -1, true);
//verify if link pointing to next page is available in header and contains pageNumber=2
assert.equal( pageLinks[1].search(".*/products.*pageNumber=2.*rel=\"next\"") > -1, true);
response;
Invalid parameter values
Anyone can make a typo sometimes. Result is an invalid value. You may try yourself to consciously use some invalid values and see how it works.
Define a negative number of products in a page, or even the page below zero to see what happens:
response = productService.tenant(tenant).products.get({
'pageNumber' : '-2',
'pageSize' : '-2'
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.status, 400);
response.body.message;
Define a zero number of products in a page to see what happens:
response = productService.tenant(tenant).products.get({
'pageNumber' : '0',
'pageSize' : '0'
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.status, 400);
response.body.message;
Set the value that is not a number. For example, try to assign abc
to pageNumber or pageSize:
response = productService.tenant(tenant).products.get({
'pageNumber' : 'abc',
'pageSize' : 'abc'
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.status, 400);
response.body.message;
Use only pageNumber or pageSize without setting any value:
response = productService.tenant(tenant).products.get({
'pageNumber' : '',
'pageSize' : ''
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.status, 400);
response.body.message;
Parameters without any values, zero or negative numbers, and non-number values assigned to the attributes result in status 400
error. Also, the response payload contains informative message.
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/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/product/v2/api.raml');
Create an API client for the Schema repository service with a valid token
API.createClient('schemaService', '/services/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;
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.beta.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.beta.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/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/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/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/product/v2/api.raml');
Create API client for Schema repository service
API.createClient('schemaService', '/services/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.beta.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.beta.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/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/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/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/product/v2/api.raml');
Create API client for Schema repository service
API.createClient('schemaService', '/services/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.beta.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.beta.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.beta.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.beta.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.beta.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.beta.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/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/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/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/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/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/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;
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/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/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.
Media
Having a shop and selling goods requires you to provide all the necessary information to your customers. Name of the product, availability, product properties, descriptive information. All this is of course an important part of your communication. But as you know a picture is worth a thousand words. You could write long lines about your product, but why not simply show it to potential buyers. Let's buy a camera, make a picture, and attach it to the product, so that your customers can really see what they are buying.
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/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 id of the returned object to a variable:
access_token = AccessToken.body.access_token;
Create API client for Product service
API.createClient('productService',
'/services/product/v2/api.raml');
Create API client for Media service
API.createClient('mediaService',
'/services/media/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;
Create simple products
At the beginning you need a product. You can later use it while performing examples from this tutorial. Those examples are meant to demonstrate a few things that Product service can provide and to show how media are handled in the service.
Let's start with creating a simple product without any media. Just basic product attributes should be 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;
To make the calls simple and the code examples clean, assign the id of the returned object to a variable:
appleId = response.body.id;
response = productService.tenant(tenant).products.post({
'code': 'apple_jubilee',
'name': 'Jubilee',
'description': 'A modern cultivar of dessert apple, which was developed in the Canadian province of British Columbia by the "Summerland Research Station".'
}, {
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 a variable.
appleId2 = response.body.id;
Create media for product
As you already know picture can sometimes say more than any word. Why not add the media to your product? You can create media, you can store them internally in the YaaS media repository, and you can bind them to the product, so that your customers can not only read about it, but also see it.
As from version 2 of the Product service, you can only use internally stored media, in other words, media stored in the YaaS media repository.
You can create media for a product that does not have any yet or you can add media to the product that already has some. Such media are ordered in a kind of list where each item has its position and the whole list can be ordered. You can influence this order by defining the position property of each media item. If you skip this property, than each new media item will be added at the end of the list. Of course, if the product has only one element, this will be both the first and the last element in the list.
Overview possible scenarios considered in the next sections:
- Creating the first media item with a defined position
- Creating the first media item without a defined position
- Creating additional media item without a defined position
- Creating additional media with a defined position
Each act of creating the media item for a product consists of three steps. To avoid unnecessary repetitions, describe these three steps fully in the very next scenario: Create first media item with position. For the purpose of this tutorial, call those 3 steps a 3-steps-procedure, referred as such in later sections.
The other three create-media scenarios receive the simplified description, which will focus mainly on details that make them different from the first one.
Create the first media item with a defined position
In this scenario, define the position property of the media item.
Step one
Create the upload link, which will be used later to upload the media item. To do so, you need:
response = productService.tenant(tenant).products.productId(appleId).media.post({
'contentType' : 'image/gif',
'position' : 2,
'tags' : ['one']
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
mediaId = response.body.id;
uploadLink = response.body.uploadLink;
response;
Step two
Having the upload link, use it to actually add the media item. Use the PUT method on the uploadLink, created in step 1.
var status = jQuery.ajax({
url: uploadLink,
type: 'PUT',
contentType: 'image/gif',
data: atob('R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='),
processData: false,
async: false
}).status;
assert.equal(status, 200);
status;
Step three
In the final step, confirm the upload and receive the link to the created medium item.
You do not send anything in the payload now. All the necessary information is already in the request URL and the headers:
response = productService.tenant(tenant).products.productId(appleId).media.mediaId(mediaId).commit.post({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
}
)
assert.equal(response.status, 202);
link = response.body.link;
response;
true
attribute.Put those steps into a function to use it later in the tutorial:
//Step one
var createMediaForProduct = function(productId, position, tagName) {
var requestBody = {'contentType' : 'image/gif', 'tags' : [tagName]};
if (position != null) {
requestBody.position = position;
}
response = productService.tenant(tenant).products.productId(productId).media.post(requestBody, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
);
assert.equal(response.status, 200);
var mediaId = response.body.id;
uploadLink = response.body.uploadLink;
//Step two
var status = jQuery.ajax({
url: uploadLink,
type: 'PUT',
contentType: 'image/gif',
data: atob('R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='),
processData: false,
async: false
}).status;
assert.equal(status, 200);
//Step three
response = productService.tenant(tenant).products.productId(productId).media.mediaId(mediaId).commit.post({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
}
);
assert.equal(response.status, 202);
var mediaLink = response.body.link;
return {"mediaLink":mediaLink, "mediaId": mediaId};
};
You may wonder what happens if you set the position property to -53, or perhaps , 0, or maybe 213 and how it does impact the product media. In the current scenario you created a media item for a product that originally did not have any media at all. When this operation is completed, there is one single media item in the product.
You could safely draw a conclusion that it does not really matter at this point what number the position property has. One single item will always be one and single, no matter what kind of number you assign to the position property. The media item is the first in the list, but it is also the last.
Run the code sample and verify available media for product:
response = productService.tenant(tenant).products.productId(appleId).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
assert.isDefined(response.body.media[0].url);
assert.isUndefined(response.body.media[0].uncommittedMedia);
assert.equal(response.body.media[0].tags[0], 'one');
response;
The retrieved media does not contain uncommittedMedia equals true
property as the 3-steps-procedure for adding media is finished. Additionally, the media url attribute is present and contains a link to media content.
Creating the first media item without a defined position
This operation may seem the same as the previous one, but there is a slight, yet important, difference. You still have a product without any media and you still want to create a media item for this product. As you plan to have only one media for the product anyway, you do not need to care of the position property.
To create first media item for a product, perform three steps described in the previous sections, with one exception: do not use the position property in the payload. It does not matter for ordering as long as you only have one media item per product.
var result = createMediaForProduct(appleId2, null, 'one');
link1 = result.mediaLink;
mediaId1 = result.mediaId;
result;
Create additional media without position
Some time passed and you decided that to encourage more clients to buy your product you will add more pictures. Generally, you can perform the 3-steps-procedure, described fully in previous sections. There is no need to modify anything. There are, however, two things you should note:
- At this time, you create new media item for a product that already has some existing media. The product list of these media has some order.
- If you create new media item without defining a position property, then the media item will be added at the end of the existing media list.
var result = createMediaForProduct(appleId2, null, 'two');
link2 = result.mediaLink;
mediaId2 = result.mediaId;
result;
Retrieve created product with media
response = productService.tenant(tenant).products.productId(appleId2).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.media[0].tags[0], 'one');
assert.equal(response.body.media[1].tags[0], 'two');
response;
Creating additional media with a defined position
You still create media here, but this time it is different. You have a list of media in your product. You have a new media item you want to add to a product. And you also want to add a new item in a specific position in the media list.
All you need is your 3-steps-procedure of creating media and one important thing: the position property in the payload, having a numerical value. The position defines where the new media item is inserted in the list. Begin with the position equal 1
:
var result = createMediaForProduct(appleId2, 1, 'three');
link3 = result.mediaLink;
mediaId3 = result.mediaId;
result;
Retrieve a product with media
The position value tells you where the media item is inserted in the list. When the item is inserted, there is no other application for this value anymore, so its not persisted.
2
.Examples:
- If position=1, the item will be inserted as second media, and all other media with higher index will be moved one position up.
- If position=7, the same will happen, but only for the items at the position 7 and higher.
- If position=-53, the item will be inserted at the position 0, and all other media with higher index will be moved one position up.
response = productService.tenant(tenant).products.productId(appleId2).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.media[0].tags[0], 'one');
assert.equal(response.body.media[1].tags[0], 'three');
assert.equal(response.body.media[2].tags[0], 'two');
assert.isUndefined(response.body.media[0].position);
assert.isUndefined(response.body.media[1].position);
assert.isUndefined(response.body.media[2].position);
response;
Edit media metadata - full update
You have a catalog of products, where each item is illustrated with a nice images. Once you assign an image to the product, it stays there. But of course, you may sometimes be interested in changing some of its characteristics. Perhaps you like to make a certain image bigger, or be re-positioned. Or you want to bring it from the back to the front. You can accomplish this task with a simple update operation.
You can do a full update by providing the payload that fully describes the updated product, in other words a payload that includes all the attributes you want to see after update:
response = productService.tenant(tenant).products.productId(appleId2).media.mediaId(mediaId2).put({
'position' : 0,
'tags' : ['two']
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
}
)
assert.equal(response.status, 200);
response;
Retrieve created product with media
response = productService.tenant(tenant).products.productId(appleId2).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.media[0].tags[0], 'two');
assert.equal(response.body.media[1].tags[0], 'one');
assert.equal(response.body.media[2].tags[0], 'three');
response;
After the update the media with tag two
is ordered as first. Next sample shows what happens if you fully update a media without specifying its position.
response = productService.tenant(tenant).products.productId(appleId2).media.mediaId(mediaId2).put({
'tags' : ['two']
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
}
)
assert.equal(response.status, 200);
response;
Verify the update:
response = productService.tenant(tenant).products.productId(appleId2).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.media[0].tags[0], 'two');
assert.equal(response.body.media[1].tags[0], 'one');
assert.equal(response.body.media[2].tags[0], 'three');
response;
As you verified, the media position did not change. The conclusion is that when performing media update without media position specified, the media order does not change.
Edit media metadata - partial update
You can also update your media by performing a partial update. This way you can only modify a selected attribute, and the payload only contains new values for the attributes you want to modify. Do not forget to use the partial query parameter.
The following is an example of how to update only media position, without changing any other media attributes:
response = productService.tenant(tenant).products.productId(appleId2).media.mediaId(mediaId2).put({
'position' : 2
}, {
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(appleId2).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.media[0].tags[0], 'one');
assert.equal(response.body.media[1].tags[0], 'three');
assert.equal(response.body.media[2].tags[0], 'two');
response;
The tags parameter is present for the product media, which is partially updated. The media with tag 'two' is ordered as third. Media position is indexed from 0, so when you specify the media position as 2
, the media with tag 'two' gets ordered as third.
Retrieve all media of a product
With huge amount of products you may not remember what kind of media each product has. The example shows how to retrieve all media that belong to a certain product. Media are ordered by their position in media collection.
response = productService.tenant(tenant).products.productId(appleId2).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.media[0].tags[0], 'one');
assert.equal(response.body.media[1].tags[0], 'three');
assert.equal(response.body.media[2].tags[0], 'two');
response;
Retrieve a particular media item of a product
Assume you want to get full information about a specific media item. A product may have many images, but the one particular image is really what you are interested in. Extracting a particular media media is not harder than the previous example above.
response = productService.tenant(tenant).products.productId(appleId2).media.mediaId(mediaId1).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.tags[0], 'one');
response;
Delete media
Sooner or later you find yourself in a need to remove an image that is no longer needed for the product. Perhaps reality changed and the image is outdated. Or you just found another one, better responding to your requirements.
response = productService.tenant(tenant).products.productId(appleId2).media.mediaId(mediaId2).delete({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 204);
response;
Retrieve product with media
After deleting media with tags=second, which was in the first position, there are only two media left for the specific product. The order of the remaining media is now adjusted as shown below:
response = productService.tenant(tenant).products.productId(appleId2).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.isDefined(response.body.media[0]);
assert.equal(response.body.media[0].tags[0], 'one');
assert.isDefined(response.body.media[1]);
assert.equal(response.body.media[1].tags[0], 'three');
assert.isUndefined(response.body.media[2]);
response;
Delete product with media
When you remove a product with media, you simply delete it as any other product:
response = productService.tenant(tenant).products.productId(appleId).delete({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
);
assert.equal(response.status, 204);
response;
When the product is deleted, all its media are deleted as well:
var parts = link.split('/');
var id = parts[parts.length - 1];
response = mediaService.public.files.fileId(id).get();
assert.equal(response.status, 404)
response;
Variants
Use product variants to define additional attributes for products without changing the basic product definition. Product variants help you to further differentiate items within a type. The examples in this tutorial show how to manage product variants.
For the purposes of this tutorial, imagine you have a small web shop that offers a single type of product: t-shirts. With only one product, you could simply create that product and sell it. In time, however, as your business grows, you add more products, and more variations on those products, such as differences in size and color. In the Product service, "variants" are different versions of the same product. You can think of your product in YaaS as a set of basic attributes that, combined with one or more variable attributes, becomes one of many product variants.
In this tutorial, product attributes are called "basic attributes". A variant schema provides its own attributes. When you create a variant, you can use new attributes defined in the variant schema, or you can use an attribute such as name or code to overwrite the value of the same attribute in the product's basic attributes.
For example, you define your t-shirt products by the basic attributes name and code. However, t-shirts are usually available in different sizes and colors, and you don't want to create many individual products for every type of t-shirt you sell. You can store additional attributes to differentiate your t-shirts, such as size and color, in a variant schema. The variant schema declares the size and color attributes. Then, you create a product variant using the previously-declared, available product attributes.
Setup
url = document.referrer
parser = document.createElement('a')
parser.href = url
possibleStagePrefix = ''
if (parser.hostname.indexOf('stage')>0)
possibleStagePrefix = 'stage.'
Assertion
Define the help variables for this tutorial:
assert = chai.assert;
expect = chai.expect;
Get access token
To perform any operation with a specific service, you need an access token. Create an API Client for the OAuth2 service:
API.createClient('oAuth2Service',
'/services/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 to a variable:
access_token = AccessToken.body.access_token;
Create an API client for the Product service
Follow this example to create an API client for the Product service:
API.createClient('productService',
'/services/product/v2/api.raml');
Create an API client for the Schema repository service
Follow this example to create an API client for the Schema repository service:
API.createClient('schemaService', '/services/schema/v1/api.raml');
Cleanup
Before creating new products and variants 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;
Create a product without variants
Because variants are optional, you can create a product without variants. To learn how to create a product without variants, see the Basics topic.
Create a product with variants
To start, create a variant schema definition. Define the size and color properties that will help define your variants.
schema = 'tshirt_variant-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':{
'size': {
'type':'string'
},
'color': {
'type':'string'
}
}
},{
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
});
assert.equal(response.status, 201);
}
Create a product. Add some basic attributes such as name, description, and define it as a public product. Include possible variant options in the metadata section of the product definition. The t-shirt property allows you to use the variant properties from the schema you created in the preceding step.
response = productService.tenant(tenant).products.post({
'metadata': {
'variants':{
'options':{
't-shirt': 'https://api.'+possibleStagePrefix+'yaas.io/hybris/schema/v1/'+tenant+'/tshirt_variant-version1.json'
},
}
},
'code': 'emperors_new_clothes_cuzco_1',
'name': "The Emperor's New Clothes - Cuzco king",
'published': true,
'description': "A t-shirt with The Emperor's New Clothes story motive"
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
response;
Store the product id in the tShirtId1 variable, so you can use it later in this tutorial.
tShirtId1 = response.body.id;
You are ready to create a product variant. Previously, you:
- Set up the size and color properties in the variant schema
- Included the metadata section in the product definition, which allows you to create variants by providing values for the properties you declared in the variant schema
In this exercise, make the t-shirt variant more specific. For example, a black t-shirt in size small.
response = productService.tenant(tenant).products.productId(tShirtId1).variants.post({
'code': 'emperors_new_clothes_cuzco_1_black',
'name': "The Emperor's New Clothes - Cuzco king Black",
'options': {
't-shirt': {
'size': 'small',
'color': 'black'
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
response;
You created a brand-new product variant. You can always create more product variants on a need-to-have basis. The name and code attributes have new values in your new variant. There is no description attribute, since it was defined in the product and cannot be overwritten. If you need to present it to your customers, your business-level front-end application must merge the appropriate attributes in the product item with the appropriate attributes in the product variant.
Store this new product variant id in another variable to use it later:
tShirtBlackSmallVariant1 = response.body.id;
Retrieve a product variant
Retrieve the variant you created to verify that the operation completed successfully.
response = productService.tenant(tenant).products.productId(tShirtId1).variants.variantId(tShirtBlackSmallVariant1).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.name, "The Emperor's New Clothes - Cuzco king Black");
assert.equal(response.body.options['t-shirt'].size, 'small');
assert.equal(response.body.options['t-shirt'].color, 'black');
response;
Retrieve all product variants
Because t-shirts can be black, white, blue, yellow, small, medium-large, and so on, it might be impossible to remember all of your variants, but you can always retrieve a list.
Create some more variants for this example. One variant from a previous exercise is still stored in the tShirtBlackSmallVariant1 variable, but in this exercise, you will create a new variant for extra-large, pink t-shirts.
response = productService.tenant(tenant).products.productId(tShirtId1).variants.post({
'code': 'emperors_new_big_clothes_cuzco_1000_pink',
'name': "The Emperor's New Big Clothes - Cuzco King Size Pink",
'options': {
't-shirt': {
'size': 'x-large',
'color': 'pink'
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
response;
Store the new product variant id in a variable to use it later:
tShirtPinkXLargeVariant2 = response.body.id;
You have now two variants of the same product and you can run one call that fetches them all.
response = productService.tenant(tenant).products.productId(tShirtId1).variants.get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.status, 200);
assert.deepEqual(response.body.map(function(variant) {return variant.code;}).sort(), ['emperors_new_clothes_cuzco_1_black', 'emperors_new_big_clothes_cuzco_1000_pink'].sort());
response;
Update basic attributes on all product variants
You can use product and variant data to give your customers full product information in a front-end application. In your front-end interface, you can display attributes returned from:
- a product
- variants created for a specific product
You can also display the basic attributes for a product along with all of its product variants. This approach has some advantages:
- You can share one basic attribute value among all product variants in the interface of your choice.
- If you need to update a common, basic attribute, you only need to perform the update once.
Consider this example. To change the description for all of your front-end product variants, you can change the description attribute, set in the product definition, only once. All of the product variants, a combination of the product and variant properties displayed in a front-end application, also receive the update.
When you update a single attribute, you can use the partial query parameter to perform a partial update. Find more information about partial updates in the Partial Update section.
response = productService.tenant(tenant).products.productId(tShirtId1).put({
'description' : "A t-shirt with The Emperor's New Clothes story motive. An everyday essential, this basic t-shirt is made from soft pima cotton. A regular fit, it has short sleeves and a classic round neckline."
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
},
query: {
'partial': true
}
});
assert.equal(response.status, 200);
response;
Share extended-product property among variants
Unique characteristics such as color and size help distinguish between product variants. However, you might need an attribute that has the same value for all product variants. A simplified example could be a tax value that applies to all items in your shop that are subject to taxation. If you define a tax attribute in a mixin schema, rather than in the variant definition, you can update an existing product and all its variants, and enjoy the flexibility of mixins that support product extensibility.
In this exercise, create a schema with an attribute that stores the tax code:
schemaTax = 'tax-version1.json';
response = schemaService.tenant(tenant).schema(schemaTax).get({
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
if(response.status == 404){
response = schemaService.tenant(tenant).schema(schemaTax).post({
'$schema': 'http://json-schema.org/draft-04/schema#',
'type': 'object',
'properties':{
'taxValue': {
'type':'number'
}
}
},{
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
});
assert.equal(response.status, 201);
}
Update an existing product and include the tax-related property in the payload. Use the product id from the tShirtId1 variable you created earlier in this tutorial.
response = productService.tenant(tenant).products.productId(tShirtId1).put({
'metadata': {
'mixins':{
'tax': 'https://api.'+possibleStagePrefix+'yaas.io/hybris/schema/v1/'+tenant+'/'+schemaTax
}
},
'mixins': {
'tax': {
'taxValue': 15
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
},
query: {
'partial': true
}
});
assert.equal(response.status, 200);
response;
Perform a full update on a variant
Perform a full update to modify multiple properties or all properties for a variant. You must include every property for the variant in the payload of any full update request.
In the example shown, the color attribute has a red
value, while the value for the name property describes a pink shirt. You must ensure data consistency, so you need to perform an update. Later in this tutorial, you will correct the inconsistency in this variant, but for now, run the example code shown:
response = productService.tenant(tenant).products.productId(tShirtId1).variants.variantId(tShirtPinkXLargeVariant2).put({
'code': 'emperors_new_big_clothes_cuzco_1000_pink',
'name': "The Emperor's New Big Clothes - Cuzco King Size Pink",
'options': {
't-shirt': {
'size': 'x-large',
'color': 'red'
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 200);
response;
Verify the latest update:
response = productService.tenant(tenant).products.productId(tShirtId1).variants.variantId(tShirtPinkXLargeVariant2).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.code, "emperors_new_big_clothes_cuzco_1000_pink");
assert.equal(response.body.name, "The Emperor's New Big Clothes - Cuzco King Size Pink");
assert.equal(response.body.options['t-shirt'].size, 'x-large');
assert.equal(response.body.options['t-shirt'].color, 'red');
response;
Perform a partial update of a variant
To correct the color inconsistency between the color and the product name values in the preceding example, you only need to change one property. Perform a partial update to make selected changes. Assuming the red
value for the color attribute is correct, you only need to modify the values of the name and code attributes. Use the partial query parameter as shown in this example:
response = productService.tenant(tenant).products.productId(tShirtId1).variants.variantId(tShirtPinkXLargeVariant2).put({
'code': 'emperors_new_big_clothes_cuzco_1000_red',
'name': "The Emperor's New Big Clothes - Cuzco King Size Red"
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
},
query: {
'partial': 'true'
}
});
assert.equal(response.status, 200);
response;
Verify that the partial update of the variant worked:
response = productService.tenant(tenant).products.productId(tShirtId1).variants.variantId(tShirtPinkXLargeVariant2).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.code, "emperors_new_big_clothes_cuzco_1000_red");
assert.equal(response.body.name, "The Emperor's New Big Clothes - Cuzco King Size Red");
assert.equal(response.body.options['t-shirt'].size, 'x-large');
assert.equal(response.body.options['t-shirt'].color, 'red');
response;
Delete a single variant
If you no longer need a specific product variant, you can remove it and leave the other variants intact. In this example, you will remove the variant for extra-large, pink t-shirts that you created previously. You need to know the variant id, which you can find in the tShirtPinkXLargeVariant2 variable.
After the method runs successfully, check the response status to make sure the product variant is removed.
response = productService.tenant(tenant).products.productId(tShirtId1).variants.variantId(tShirtPinkXLargeVariant2).delete({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
);
assert.equal(response.status, 204);
response;
Delete all product variants
You can remove all variants you no longer need in one call, as shown:
response = productService.tenant(tenant).products.productId(tShirtId1).variants.delete({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
);
assert.equal(response.status, 204);
response;
Verify that the variants are removed:
response = productService.tenant(tenant).products.productId(tShirtId1).variants.get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.body.length, 0);
response;
Remove a variant definition
You might want to keep a product, but you no longer need some of the properties defined in its variants. You might also want to prevent others from creating new variants with those unwanted properties. You can do so by performing a partial update to remove the variant definition that includes the unwanted properties. With partial update you only include those properties you want to change in your payload. In this example, provide an empty variant definition. The system removes any properties previously defined in the variants.
response = productService.tenant(tenant).products.productId(tShirtId1).put({
'metadata': {
'variants':{
'options':{
't-shirt': null
},
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
},
query: {
'partial': true
}
});
assert.equal(response.status, 200);
response;
Verify the outcome of the partial update:
response = productService.tenant(tenant).products.productId(tShirtId1).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.status, 200);
response;
As you can see, there are no longer any options defined for this variant.
Supported data types
You can prepare to create new product variants by creating schemas that declare additional options properties, and importing the schemas into a product definition. These variant options can hold different values, with a specified type for each property. You can use these data types when creating product variants:
- integer: A type for integral numbers
- number: A type for any numeric type, either integers or floating point numbers
- string: A type used to carry text messages
- boolean: A type for values to indicate whether something is true or false
The Product service has mechanisms that prevent the creation of variant options with unsupported data types. Run the examples shown to get more insight into the process of variant properties validation.
Create a schema with a property of the array type:
schema = 'tshirt_variant-version3.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':{
'colors': {
'type':'Array'
}
}
},{
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
});
assert.equal(response.status, 201);
}
Create a product:
response = productService.tenant(tenant).products.post({
'metadata': {
'variants':{
'options':{
't-shirt': 'https://api.'+possibleStagePrefix+'yaas.io/hybris/schema/v1/'+tenant+'/tshirt_variant-version3.json'
},
}
},
'code': 'emperors_new_clothes_cuzco_1',
'name': "The Emperor's New Clothes - Cuzco king",
'published': true,
'description': "A t-shirt with The Emperor's New Clothes story motive"
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 400);
response.body.message;
Normally, after you create a product, you can use the product id to create variants. In this case, however, you cannot create a product that imports a schema with option properties of a complex data type. You cannot have option properties of a complex data type within the options section of the product definition. You can create variants only from a product that contains variant options with the allowed data types: integers or floating point numbers, strings, and boolean.
Variant and product deletion
If you delete one variant or all variants, the associated product remains in the system. However, if you delete a product, the system automatically removes all of the variants created for that product's id identifier. To learn how to delete a product, see the Basics section.
Querying Variants
This tutorial describes how to retrieve one variant from a database of many. Suppose you run a shop that sells a multitude of products and variations on those products, called product variants. When your customers browse a product, they might be interested in retrieving a specific piece of information, such as price. When you can retrieve the requested data for a specified variant, without querying all of the products in your shop, you save time. Knowing the variant's id or code allows you to run a simple query and retrieve any variant you want, no matter which product it comes from.
The retrieval of non-public products requires authorization, while the retrieval of public products does not:
- Use an authorization token with the hybris.product_read_unpublished scope to retrieve any variant, published or not.
- Skip authorization with the hybris.product_read_unpublished scope to retrieve variants for published products, only.
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;
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/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 to a variable:
access_token = AccessToken.body.access_token;
Create an API client for the Product service
API.createClient('productService',
'/services/product/v2/api.raml');
Create an API client for the Schema repository service
API.createClient('schemaService', '/services/schema/v1/api.raml');
Cleanup
Before creating products and variants for 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;
Tutorial data
Before you start querying variants, prepare some data to use. It is best to have at least two products, along with two variants for each of the products.
Create variant schemas
schemaVariant1 = 'colorSize_variant-version1.json';
response = schemaService.tenant(tenant).schema(schemaVariant1).get({
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
if(response.status == 404){
response = schemaService.tenant(tenant).schema(schemaVariant1).post({
'$schema': 'http://json-schema.org/draft-04/schema#',
'type': 'object',
'properties':{
'size': {
'type':'string'
},
'colour': {
'type':'string'
}
}
},{
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
});
assert.equal(response.status, 201);
response.body ;
}
schemaVariant2 = 'fabric_variant-version1.json';
response = schemaService.tenant(tenant).schema(schemaVariant2).get({
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
if(response.status == 404){
response = schemaService.tenant(tenant).schema(schemaVariant2).post({
'$schema': 'http://json-schema.org/draft-04/schema#',
'type': 'object',
'properties':{
'fabric': {
'type':'string'
}
}
},{
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
);
assert.equal(response.status, 201);
}
Create products
Create two products that include variant schemas. You will use these products later, to create product variants with properties defined by the schema definitions.
Create a t-shirt product:
response = productService.tenant(tenant).products.post({
'metadata': {
'variants':{
'options':{
'colorSize': 'https://api.'+possibleStagePrefix+'yaas.io/hybris/schema/v1/'+tenant+'/colorSize_variant-version1.json',
'fabrics': 'https://api.'+possibleStagePrefix+'yaas.io/hybris/schema/v1/'+tenant+'/fabric_variant-version1.json'
},
}
},
'code': 'emperors_new_clothes_cuzco_1',
'name': "The Emperor's New Clothes - Cuzco king",
'published': true,
'description': "A t-shirt with The Emperor's New Clothes story motive"
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
response;
Store the product id in the tShirtProductId variable, so you can use it later in this tutorial.
tShirtProductId = response.body.id;
Create a kimono product:
response = productService.tenant(tenant).products.post({
'metadata': {
'variants':{
'options':{
'colorSize': 'https://api.'+possibleStagePrefix+'yaas.io/hybris/schema/v1/'+tenant+'/colorSize_variant-version1.json',
'fabrics': 'https://api.'+possibleStagePrefix+'yaas.io/hybris/schema/v1/'+tenant+'/fabric_variant-version1.json'
},
}
},
'code': 'kimono_kyoto_spring_1',
'name': "Charm of the Kyoto Spring",
'published': false,
'description': "A beautiful kimono, cherry blossom style, traditional design."
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
response;
Store the product id in the kimonoProductId variable, so you can use it later in this tutorial.
kimonoProductId = response.body.id;
Create two variants of the t-shirt product
Create a few t-shirt variants that you will use in the ensuing exercises. The id and code properties are stored in variables. You will need these properties to retrieve the variants.
Create a small, black, hemp fabric t-shirt product variant:
response = productService.tenant(tenant).products.productId(tShirtProductId).variants.post({
'code': 'hemp_tshirt_small_black',
'name': "Organic Hemp Fabric Small Black T-shirt",
'options': {
'colorSize': {
'size': 'small',
'colour': 'black'
},
'fabrics': {
'fabric': 'hemp'
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
vSmallBlackHempTshirtId = response.body.id;
vSmallBlackHempTshirtCode = 'hemp_tshirt_small_black';
response;
Create a large, pink cotton t-shirt product variant:
response = productService.tenant(tenant).products.productId(tShirtProductId).variants.post({
'code': 'cotton_shirt_large_pink',
'name': "Cotton Organic Large Pink T-shirt",
'options': {
'colorSize': {
'size': 'large',
'colour': 'pink'
},
'fabrics': {
'fabric': 'cotton'
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
vLargePinkCottonTshirtId = response.body.id;
vLargePinkCottonTshirtCode = 'cotton_shirt_large_pink';
response;
Create three variants of the kimono product
Create a few kimono variants that you will use in the ensuing examples. The id and code properties are stored in variables. You will need these properties to retrieve the variants.
Create a small, bright cherry, silk kimono product variant:
response = productService.tenant(tenant).products.productId(kimonoProductId).variants.post({
'code': 'silk_kimono_small_bright_cherry',
'name': "Small Bright Cherry Silk Kimono",
'options': {
'colorSize': {
'size': 'small',
'colour': 'bright cherry'
},
'fabrics': {
'fabric' : 'silk'
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
vSmallBrightCherrySilkKimonoId = response.body.id;
vSmallBrightCherrySilkKimonoCode = 'silk_kimono_small_bright_cherry';
response;
Create a small, dark cherry, dupion kimono product variant:
response = productService.tenant(tenant).products.productId(kimonoProductId).variants.post({
'code': 'dupion_kimono_small_dark_cherry',
'name': "Small Dark Cherry Dupion Kimono",
'options': {
'colorSize': {
'size': 'small',
'colour': 'dark cherry'
},
'fabrics': {
'fabric' : 'dupion'
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
vSmallDarkCherryDupionKimonoId = response.body.id;
vSmallDarkCherryDupionKimonoCode = 'dupion_kimono_small_dark_cherry';
response;
Create an extra large, cherry, pseudo-silk kimono product variant:
response = productService.tenant(tenant).products.productId(kimonoProductId).variants.post({
'code': 'pseudo_silk_large_cherry_kimono',
'name': "Extra large Cherry Pseudo Silk Kimono",
'options': {
'colorSize': {
'size': 'xlarge',
'colour': 'cherry'
},
'fabrics': {
'fabric' : 'pseudo silk'
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
vXLargeCherryPseudoSilkKimonoId = response.body.id;
vXLargeCherryPseudoSilkKimonoCode = 'pseudo_silk_large_cherry_kimono';
response;
Get a variant by id parameter
You can have many products, and each product can have many variants. Sometimes you want to retrieve variants of a specific product, but sometimes, you might need to retrieve a specific variant or a list of variants without browsing through all of your products. You can send a GET request to retrieve variants without browsing the products.
Check the next example and retrieve the variant by its id. Retrieving variants by id means:
- You need to know the variant id.
- You can retrieve one or more variants, each by its unique id.
Use the id to retrieve a cotton t-shirt:
response = productService.tenant(tenant).variants.get({
'id' : vLargePinkCottonTshirtId
}, {
headers: {
'Accept-Language': 'en',
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body[0].name, 'Cotton Organic Large Pink T-shirt');
response;
Get a variant by the code parameter
Use the variant's code property to retrieve a hemp t-shirt:
response = productService.tenant(tenant).variants.get({
'code' : vSmallBlackHempTshirtCode
}, {
headers: {
'Accept-Language': 'en',
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body[0].name, 'Organic Hemp Fabric Small Black T-shirt');
response;
Get multiple variants by id parameter
Use multiple id values to retrieve multiple variants. In this exercise, get the variants for a hemp t-shirt, dupion kimono, and silk kimono using the id parameter:
response = productService.tenant(tenant).variants.get({
'id' : 'in('+vSmallBlackHempTshirtId+', '+vSmallDarkCherryDupionKimonoId+', '+vSmallBrightCherrySilkKimonoId+')'
}, {
headers: {
'Accept-Language': 'en',
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.length, 3);
response;
Get multiple variants by the code parameter
Use multiple code values to retrieve multiple variants. For this exercise, retrieve a hemp t-shirt, dupion kimono, and silk kimono using the code parameter:
response = productService.tenant(tenant).variants.get({
'code' : 'in('+vSmallBlackHempTshirtCode+', '+vSmallDarkCherryDupionKimonoCode+', '+vSmallBrightCherrySilkKimonoCode+')'
}, {
headers: {
'Accept-Language': 'en',
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.length, 3);
response;
Get multiple variants by both id and code parameter
In this exercise, try to use multiple code and id values to retrieve multiple variants. Get variants using these properties for each:
- By id: hemp t-shirt, dupion kimono
- By code: silk kimono
The id query parameter returns one set of results, while the code query parameter returns another set. If you use both query parameters to retrive two sets, the response returns only the common variants in both sets.
If a query by id returns two variants, and a simultaneous query by code returns a different variant, the response returns an empty set, because there are no common elements in your two queries.
response = productService.tenant(tenant).variants.get({
'id' : 'in('+vSmallBlackHempTshirtId+', '+vSmallDarkCherryDupionKimonoId+')',
'code' : vSmallBrightCherrySilkKimonoCode
}, {
headers: {
'Accept-Language': 'en',
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.length, 0);
response;
Modify the preceding example to include the id of the variant for the silk kimono, as well as the code parameter you previously used. Here again, the service returns only the common variants in the two queries for the id and code parameters. This time, however, the intersection of the two queries is not an empty set. It includes one common element, a silk kimono.
response = productService.tenant(tenant).variants.get({
'id' : 'in('+vSmallBlackHempTshirtId+', '+vSmallDarkCherryDupionKimonoId+', '+vSmallBrightCherrySilkKimonoId+')',
'code' : vSmallBrightCherrySilkKimonoCode
}, {
headers: {
'Accept-Language': 'en',
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.length, 1);
response;
Get variants for published products only
Variants can be public or non-public, depending on the product they came from. You can use authorization to retrieve all kind of variants, as long as you know their code or id. However, you can skip authorization and still retrieve variants for public products.
The preceding examples used an authorization token with the hybris.product_read_unpublished scope to retrieve variants for both public and non-public products. In this section's example, retrieve variants without authorization. For comparison's sake, the t-shirt in this tutorial is a public product, while the kimono is non-public.
Retrieve several variants of the t-shirt and kimono without using any authorization:
response = productService.tenant(tenant).variants.get({
'id' : 'in('+vSmallBlackHempTshirtId+', '+vSmallDarkCherryDupionKimonoId+', '+vSmallBrightCherrySilkKimonoId+')'
}, {
headers: {
'Accept-Language': 'en',
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.length, 1);
response;
You tried to get one variant of a t-shirt, and two variants of the kimono without authorization. Because only the t-shirt variant comes from a public product, which does not require authorization, the service returned only one variant out of the three you requested. The remaining kimono items are variants of a non-public product, which you cannot retrieve without authorization and a proper scope.
Variants 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.
Let's go one step further. Your product can be defined by some characteristics like color, size, availability, by pretty much anything you want. Thinking of this, how would you make a difference between, for example, one t-shirt and another? You could use the options like color or size, but is the black t-shirt really a different product than another one, but white? Or it is the same product, coming with two product variants.
In such situation, you can create a product, and based on it make up multiple variants that have the same set of properties. You can define these properties in separate schemas and add the reference to them in the product definition.
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;
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/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 to a variable:
access_token = AccessToken.body.access_token;
Create API client for Product service
API.createClient('productService',
'/services/product/v2/api.raml');
Create API client for Schema repository service
API.createClient('schemaService', '/services/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 variants extended with inStock attribute
You want to create extended variants of a product. The process of creating an extended variant consists of a few steps:
- Preparing schemas
- Creating a variant-ready product
- Creating extended variants
Preparing schemas
Create a schema that will provide some differentiating options for the product items. You can add some options that constitute a variant, for example size or colour. These options do not really extend a variant, but they rather create a variant of a product.
schemaVariant1 = 'tshirt_variant-version1.json';
response = schemaService.tenant(tenant).schema(schemaVariant1).get({
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
if(response.status == 404){
response = schemaService.tenant(tenant).schema(schemaVariant1).post({
'$schema': 'http://json-schema.org/draft-04/schema#',
'type': 'object',
'properties':{
'size': {
'type':'string'
},
'colour': {
'type':'string'
}
}
},{
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
});
assert.equal(response.status, 201);
response.body ;
}
Prepare a schema that defines a property extending variants. Add the inStock property that will carry the information about availability of the variant item.
schemaInStock = 'inStock-version1.json';
response = schemaService.tenant(tenant).schema(schemaInStock).get({
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
if(response.status == 404){
response = schemaService.tenant(tenant).schema(schemaInStock).post({
'$schema': 'http://json-schema.org/draft-04/schema#',
'type': 'object',
'properties':{
'inStock': {
'type':'boolean'
}
}
},{
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
});
assert.equal(response.status, 201);
response.body ;
}
Creating a variant-ready product
Before you can create a variant, you need a product. Create a t-shirt, that will serve as a base product for variants. Use the schemas to create a product ready to be extended into variants.
response = productService.tenant(tenant).products.post({
'code': 'cottontshirt',
'name': 'Cotton T-shirt',
'published': true,
'description': 'Quality fair-trade t-shirt made of organic cotton.',
'metadata': {
'variants': {
'mixins': {
'tshirt_instock': 'https://api.'+possibleStagePrefix+'yaas.io/hybris/schema/v1/'+tenant+'/inStock-version1.json'
},
'options':{
't-shirt_options': 'https://api.'+possibleStagePrefix+'yaas.io/hybris/schema/v1/'+tenant+'/tshirt_variant-version1.json'
}
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
tShirtId1 = response.body.id;
response;
In the product definition above you have seen something interesting: one schema is located in the mixins section, another one in the options section. And they all belong to the variants section. To clarify:
- options: In this section you provide a reference to the schema that creates a variant. Options like color, size, or other, create many variants of a product.
- mixins: In this section you provide a reference to the schema that extends the variant itself. Your customers will be quite interested which variants of your product are available, and which are not. Black big t-shirt may be available, but red small one could be already sold out. The inStock property extends the variant with this piece of information.
Verify if the product is created:
response = productService.tenant(tenant).products.productId(tShirtId1).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.name, 'Cotton T-shirt');
response;
Create extended variants
Create extended variants now. Note that you create and extend a variant simultaneously in this procedure, by using both options and mixins sections filled with appropriate values.
Variant one: small black cotton t-shirt, available for sale:
response = productService.tenant(tenant).products.productId(tShirtId1).variants.post({
'code': 'cottonshirt_small_black',
'name': "Cotton Organic Small Black T-shirt",
'mixins': {
'tshirt_instock': {
'inStock': true
}
},
'options': {
't-shirt_options': {
'size': 'small',
'colour': 'black'
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
vSmallBlackTshirt = response.body.id;
response;
Variant two: extra large pink cotton t-shirt, sold out:
response = productService.tenant(tenant).products.productId(tShirtId1).variants.post({
'code': 'cottonshirt_xlarge_pink',
'name': "Cotton Organic Extra Large Pink T-shirt",
'mixins': {
'tshirt_instock': {
'inStock': false
}
},
'options': {
't-shirt_options': {
'size': 'x-large',
'colour': 'pink'
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
vXLargePinkTshirt = response.body.id;
response;
At this point, you have two extended variants of the same product. Retrieve those variants now:
response = productService.tenant(tenant).products.productId(tShirtId1).variants.get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.status, 200);
variants = response.body
assert.deepEqual(variants.map(function(variant) {return variant.code;}).sort(), ['cottonshirt_small_black', 'cottonshirt_xlarge_pink'].sort());
response;
The service can retrieve all variants, but it is responsibility of your front-end application to process those results and display to your customers those variants that meet their search criteria.
For example, you have extended your variants with inStock property to allow customers finding only those variants that are available for sale. You can now retrieve all variants based on a particular product. Once you get these results, your front-end application can filter them to display only those that are available, that are in stock.
variantsInStock = variants.filter(function(variant) {
return variant.mixins.tshirt_instock.inStock;
});
assert.deepEqual(variantsInStock.map(function(variant) {return variant.code;}).sort(), ['cottonshirt_small_black'].sort());
variantsInStock;
Summary
You can extend your variants with any option that you find suitable for your products. You can always prepare more schemas and add the reference to those schemas in the mixins section of the product definition.
Those custom properties extending your variants depend only on your needs. This is why service retrieves all extended variants, but the results filtering relays on your front-end application. Only you can decide how to extend variants by using the methods presented above, and which custom properties are meaningful to your customers and should be processed by your front-end application to match the search criteria used by your customers.
Default Variant
You can have many products that may have many variants. When your customers search for a particular product, they are really interested in certain product qualities like color, size, shape, or other. These qualities constitute variants, which your customers do search for. Each time they search for a product, a certain variant is shown to the customer in the storefront application. The customer still is able to switch to another variant by using tools provided in the storefront user interface.
However, how do you decide which variant displays to the customers as the first one, in other words as a default one? This tutorial answers this question.
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/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 hybris.product_publish 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/product/v2/api.raml');
Create API client for Schema repository service
API.createClient('schemaService', '/services/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;
Creating a default variant
Generate a few variants and set one of these variants as a default one.
Begin with creating a variant schema definition. Define size and colour attributes, which will be used to make variants.
schema = 'tshirt_variant-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':{
'size': {
'type':'string'
},
'colour': {
'type':'string'
}
}
},{
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
});
assert.equal(response.status, 201);
}
Make a product that will serve as a basis for the variants.
response = productService.tenant(tenant).products.post({
'metadata': {
'variants':{
'options':{
't-shirt': 'https://api.'+possibleStagePrefix+'yaas.io/hybris/schema/v1/'+tenant+'/tshirt_variant-version1.json'
},
}
},
'code': 'emperors_new_clothes_cuzco_1',
'name': "The Emperor's New Clothes - Cuzco king",
'published': true,
'description': "A t-shirt with The Emperor's New Clothes story motive"
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
response;
Store the product id in the tShirtId variable to be used in next steps of this tutorial.
tShirtId = response.body.id;
Create three variants, defined by color and size:
- pink XL
- pink XXL
- red XXXXL
First variant: pink t-shirt of XL size See the payload thoroughly. You will find a default property there. Use this property to set a certain variant as default. There can be only one default variant per product. You can of course set a default property for another variant, but it only means that another variant becomes a default one. There cannot be two default variants per product at a time.
response = productService.tenant(tenant).products.productId(tShirtId).variants.post({
'code': 'emperors_new_clothes_cuzco_1',
'name': "The Emperor's New Clothes - Cuzco ping king XL",
'default': true,
'options': {
't-shirt': {
'size': 'XL',
'colour': 'pink'
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
response;
Store the new product variant id in another variable:
tShirtPinkXLVariant1 = response.body.id;
Second variant: pink t-shirt of XXL size
response = productService.tenant(tenant).products.productId(tShirtId).variants.post({
'code': 'emperors_new_clothes_cuzco_2',
'name': "The Emperor's New Clothes - Cuzco pink king XXL",
'options': {
't-shirt': {
'size': 'XXL',
'colour': 'pink'
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
response;
Store the new product variant id in another variable:
tShirtPinkXLLVariant2 = response.body.id;
Third variant: red t-shirt of XXXXL size
response = productService.tenant(tenant).products.productId(tShirtId).variants.post({
'code': 'emperors_new_clothes_cuzco_3',
'name': "The Emperor's New Clothes - Cuzco red king XXXXL",
'options': {
't-shirt': {
'size': 'XXXXL',
'colour': 'red'
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
response;
Store the new product variant id in another variable:
tShirtRedXXXXLVariant3 = response.body.id;
Three variants exist now, and keep in mind the first variant is a default one. It is easy to see in the storefront application, as this will be the first variant that displays to the customer.
However, in this tutorial you can retrieve a variant and check the content of the default property. The default property can have two values: true
or false
. If the property is set to true
, then the variant is a default one. You do not need to worry about anything else. Any other previously default variant becomes non-default in the process.
Retrieve the variant and check its properties. A true
value of the default property clearly defines a variant as default one:
response = productService.tenant(tenant).products.productId(tShirtId).variants.variantId(tShirtPinkXLVariant1).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.options['t-shirt'].size, 'XL');
assert.equal(response.body.options['t-shirt'].colour, 'pink');
assert.equal(response.body.default, true);
response;
Update a variant to default variant
This example shows you what happens to the default variant if another variant becomes a default one. Set the third variant as default: red t-shirt of XXXXL size. The only thing you need is a partial update to change the value of the default property.
response = productService.tenant(tenant).products.productId(tShirtId).variants.variantId(tShirtRedXXXXLVariant3).put({
'default': true
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
},
query: {
'partial': 'true'
}
});
assert.equal(response.status, 200);
response;
You've got a new default variant now. Verify it:
response = productService.tenant(tenant).products.productId(tShirtId).variants.variantId(tShirtRedXXXXLVariant3).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.options['t-shirt'].size, 'XXXXL');
assert.equal(response.body.options['t-shirt'].colour, 'red');
assert.equal(response.body.default, true);
response;
Also, check the first variant, the one that has been set as default in the very beginning.
response = productService.tenant(tenant).products.productId(tShirtId).variants.variantId(tShirtPinkXLVariant1).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.default, false);
response;
The only property you are interested in this case is default. If the variant is no longer a default one, this property should hold the false
value. You did not change this value explicitly. But when you set the third variant as default, the service unset this property in the first variant automatically. This is the way to assure that there will never be more than one default variant per product.
Variants Media
You can add a taste to your product description by providing some attractive visual presentation. Moreover, you are not limited to only product in general, but you also can enhance any product variant with highly customized visualization, representing specific qualities of a variant that may appeal to your customers. Your customers can now see images prepared specifically for each product variant you have to offer.
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/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.schema_manage 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/product/v2/api.raml');
Create API client for Media service
API.createClient('mediaService',
'/services/media/v2/api.raml');
Create API client for Schema repository service
API.createClient('schemaService', '/services/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
Start with creating a product with variant. First step to do it is creating a schema for the variant.
schema = 'tshirt_variant-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':{
'size': {
'type':'string'
},
'colour': {
'type':'string'
}
}
},{
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
});
assert.equal(response.status, 201);
}
Having a schema, you can create a product with variant definition.
response = productService.tenant(tenant).products.post({
'metadata': {
'variants':{
'options':{
't-shirt': 'https://api.'+possibleStagePrefix+'yaas.io/hybris/schema/v1/'+tenant+'/tshirt_variant-version1.json'
},
}
},
'code': 'emperors_new_clothes_cuzco_1',
'name': "The Emperor's New Clothes - Cuzco king",
'published': true,
'description': "A t-shirt with The Emperor's New Clothes story motive"
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
response;
Store the product id in the tShirtId1 variable to use it later in the tutorial:
tShirtId1 = response.body.id;
Finally, create a product variant. It has not only general attributes as defined in the product definition, but also attributes that provide differentiating factor to each variant like size and colour. To successfully create a product variant, keep two things in mind:
- Set up the size and colour attributes in the variant schema
- Include the metadata section in the product definition. This will allow you to create variants by providing values for the properties declared in the variant schema
Create new t-shirt small and black:
response = productService.tenant(tenant).products.productId(tShirtId1).variants.post({
'code': 'emperors_new_clothes_cuzco_1_black',
'name': "The Emperor's New Clothes - Cuzco king Black",
'options': {
't-shirt': {
'size': 'small',
'colour': 'black'
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
response;
You have a brand new variant now. You can always create more product variants later, on the need-to-have basis. As you can see, the name and code are given new values for this particular variant. There is no description attribute, since it has already been defined in the product and cannot be overwritten. If you need to present it to your customers, then your business-level front-end application will be responsible to merge the appropriate attributes from the product and appropriate attributes from the product variant.
Store the new product variant id in another variable:
tShirtBlackSmallVariant1 = response.body.id;
Create media for product's variant
Having a product variant is one thing. It may have all the necessary attributes, providing all the necessary information. But you most certainly like to show some visuals to your customers. Begin with preparing a custom image for each variant you have created.
Create first media item with position
You can add several images to the product variant. One of them must have the first position. Begin with scenario where a media item is added to a product variant that does not have any media yet. In this scenario, define the position property of the media item. The whole process involves three steps as presented in the next subsections.
Step one
Create the upload link, which will be used later to actually upload the media item. To do so, you need:
response = productService.tenant(tenant).products.productId(tShirtId1).variants.variantId(tShirtBlackSmallVariant1).media.post({
'contentType' : 'image/gif',
'position' : 2,
'tags' : ['one']
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
mediaId = response.body.id;
uploadLink = response.body.uploadLink;
response;
Step two
Having the upload link, use it to actually add the media item. To do so, use the PUT method on the uploadLink.
var status = jQuery.ajax({
url: uploadLink,
type: 'PUT',
contentType: 'image/gif',
data: atob('R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='),
processData: false,
async: false
}).status;
assert.equal(status, 200);
status;
Step three
In the final step, confirm the upload and receive the link to the created medium item. Do not send anything in the payload now. All the necessary information is already in the request URL and the headers. Check the following example:
response = productService.tenant(tenant).products.productId(tShirtId1).variants.variantId(tShirtBlackSmallVariant1).media.mediaId(mediaId).commit.post({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
}
)
assert.equal(response.status, 202);
link = response.body.link;
response;
true
attribute.Retrieve created variant with media
Compared to creating a variant with media, the process of retrieving such variant is pretty simple. All the necessary details are included in the url path parameters: id of the product and id of the product variant:
response = productService.tenant(tenant).products.productId(tShirtId1).variants.variantId(tShirtBlackSmallVariant1).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
response;
Other possible operations
You can perform other operations on the variants with media. You can find more information on those operations in the Media tutorial section.
Removing Variant Definition
Each product in YaaS has a set of basic attributes. Variants are different versions of the same product, with additional characteristics such as size and color. To create variants, you must add at least one variant option definition to a product.
Option properties make variants unique from the base product. When you create a product, you include a definition of the option properties that allow you to create product variants. You can create multiple variants, based on the product containing the variant definition(s).
Because at least one variant definition is required to create variants, the service does not allow you to remove the last variant definition from a product if any variant using that definition still exists.
Setup
url = document.referrer
parser = document.createElement('a')
parser.href = url
possibleStagePrefix = ''
if (parser.hostname.indexOf('stage')>0)
possibleStagePrefix = 'stage.'
Assertion
Define help variables for this tutorial:
assert = chai.assert;
expect = chai.expect;
Get access token
To perform any operation with a specific service, you need an access token. Create an API Client for the OAuth2 service:
API.createClient('oAuth2Service',
'/services/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.schema_manage 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/product/v2/api.raml');
Create an API client for the Schema repository service
API.createClient('schemaService', '/services/schema/v1/api.raml');
Cleanup
Before creating new products and variants 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;
Tutorial data
Create a few items to use in tutorial examples that show you how to delete variants definitions.
Create variant schemas
Create variant schemas for use in the examples:
schema = 'tshirt_variant-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':{
'size': {
'type':'string'
},
'colour': {
'type':'string'
}
}
},{
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
);
assert.equal(response.status, 201);
}
schema = 'tshirt_variant-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':{
'fabric': {
'type':'string'
}
}
},{
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
);
assert.equal(response.status, 201);
}
Create a product with variant definitions
Create a product that includes variant schemas. Use this product later to create associated variants with properties defined in the schemas.
response = productService.tenant(tenant).products.post({
'metadata': {
'variants':{
'options':{
't-shirt1': 'https://api.'+possibleStagePrefix+'yaas.io/hybris/schema/v1/'+tenant+'/tshirt_variant-version1.json',
't-shirt2': 'https://api.'+possibleStagePrefix+'yaas.io/hybris/schema/v1/'+tenant+'/tshirt_variant-version2.json'
},
}
},
'code': 'emperors_new_clothes_cuzco_1',
'name': "The Emperor's New Clothes - Cuzco king",
'published': true,
'description': "A t-shirt with The Emperor's New Clothes story motive"
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
response;
Store the product id in the tShirtProductId1 variable, so you can use it later in this tutorial.
tShirtProductId1 = response.body.id;
Create a variant using two variant definitions
If the product includes two variant definitions, then you can create a variant that has properties defined in both definitions. In fact, your products can have even more definitions, depending on your needs. From a product with multiple definitions, you can create many variants.
Create a variant that uses one definition for the color and size of a t-shirt, along with another definition that identifies the t-shirt fabric.
response = productService.tenant(tenant).products.productId(tShirtProductId1).variants.post({
'code': 'emperors_new_clothes_cuzco_1_black',
'name': "The Emperor's New Clothes - Cuzco king Black",
'options': {
't-shirt1': {
'size': 'small',
'colour': 'black'
},
't-shirt2': {
'fabric': 'hemp'
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
response;
Store the variant id in the tShirtProductVariantId1 variable, so you can use it later in this tutorial.
tShirtProductVariantId1 = response.body.id;
Retrieve the variant to verify that it includes all the data you added:
response = productService.tenant(tenant).products.productId(tShirtProductId1).variants.variantId(tShirtProductVariantId1).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.name, "The Emperor's New Clothes - Cuzco king Black");
assert.equal(response.body.options['t-shirt1'].size, 'small');
assert.equal(response.body.options['t-shirt1'].colour, 'black');
assert.equal(response.body.options['t-shirt2'].fabric, 'hemp');
response;
Remove a variant definition with a partial update
You have a product that includes two variant definitions. One of the definitions defines the fabric property, available for all variants that stem from the same product definition. When you no longer need certain information in your product variants, you can remove the associated variant definition from the product. When you remove a definition from a product, all properties that stem from this definition disappear from the associated variants.
Remove the t-shirt2 definition from a product. To do so, perform a partial update and include the t-shirt2 option property, set to null
, in the payload.
response = productService.tenant(tenant).products.productId(tShirtProductId1).put({
'metadata': {
'variants' : {
'options': {
't-shirt2': null
}
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
},
query: {
'partial': 'true'
}
});
assert.equal(response.status, 200);
response;
The information defined in the removed definition disappears from the associated variants, but those variants still exist. That means you can no longer retrieve any information about the product's fabric in any of the affected variants, but you still can see the size and color information.
Verify the update was successful by retrieving the variant and checking the response content.
response = productService.tenant(tenant).products.productId(tShirtProductId1).variants.variantId(tShirtProductVariantId1).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.status, 200);
assert.equal(response.body.name, "The Emperor's New Clothes - Cuzco king Black");
assert.equal(response.body.options['t-shirt1'].size, 'small');
assert.equal(response.body.options['t-shirt1'].colour, 'black');
assert.equal(response.body.options['t-shirt2'], null);
response;
Verify the product, as well. Check the response content to verify that it has only one variant definition, as expected.
response = productService.tenant(tenant).products.productId(tShirtProductId1).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
response;
Remove the last variant definition with a full update
In the previous section, you removed one of two variant definitions from a product. Now, you remove the remaining definition. You can do this with a full or partial update. For the purpose of this example, do a full update. With a full update, you send a payload that defines the product anew. The updated payload does not include a variant definition.
What happens to the variant when you remove the associated variant definition from the product? In theory, this full update replaces the old product with a new product that does not include a variant definition. The service, however, does not allow you to remove all variant definitions because a variant is still present in the system, and variants cannot exist without an associated product that defines the variant options. Therefore, the remaining variant definition remains in the system and associated with the product variants.
response = productService.tenant(tenant).products.productId(tShirtProductId1).put({
'code': 'emperors_new_big_clothes_cuzco_1000_pink',
'name': "The Emperor's New Big Clothes - Cuzco King Size Pink"
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 400);
response.body.message;
Remove the last variant definition with a partial update
Perform a partial update, including only the options property, with a null
value, in the payload. A partial update is acceptable in this scenario, because you are ready to delete the last of all existing variant definitions.
As mentioned in the previous example, the Product service does not allow you to delete all variant definitions for a product if any associated variants still exist in the system.
response = productService.tenant(tenant).products.productId(tShirtProductId1).put({
'metadata': {
'variants' : {
'options' : null
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
},
query: {
'partial': 'true'
}
});
assert.equal(response.status, 400);
response.body.message;
The next section shows you how to remove the last variant definition from a product.
Remove the last variant definition
To remove the last variant definition from a product, first remove all variants that use this definition. Begin with removing existing variants that are defined in a particular product. You have just one variant in this example, but the same procedure applies for multiple variants.
response = productService.tenant(tenant).products.productId(tShirtProductId1).variants.delete({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
);
assert.equal(response.status, 204);
response;
Verify the variants are removed:
response = productService.tenant(tenant).products.productId(tShirtProductId1).variants.get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Accept-Language': 'en'
}
}
)
assert.equal(response.body.length, 0);
response;
Now, remove the variant definition. This time, because there are no longer any existing variants, you can remove the variant definition. You can also perform a full update to remove the last variant definition, but for the purpose of this exercise, run a partial update.
response = productService.tenant(tenant).products.productId(tShirtProductId1).put({
'options': null
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
},
query: {
'partial': 'true'
}
});
assert.equal(response.status, 200);
response;
Retrieve the product and check the response content. Any variant definition that previously existed is gone.
response = productService.tenant(tenant).products.productId(tShirtProductId1).get({}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
}
)
assert.equal(response.status, 200);
response;
Searching
Sometimes other services may requiring one single response from the Product service about multiple products or variants. For example, a service responsible for checkout might need to retrieve information about all products in a cart to properly support sale process. A cart can have multiple products and in that case, information about multiple products is needed. Theoretically, a service can make several calls, each one requesting information about particular product. Unfortunately, this could lead to efficiency issues. Instead of asking for each product separately, it may be more efficient to simply send over one single request that states what is needed, and let the Product service handle this request with one response.
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;
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/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/product/v2/api.raml');
Create API client for Schema service
API.createClient('schemaService', '/services/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
You are going to retrieve information about multiple items in one call, but you need some data to work with. Create a few products and variants to use them in the tutorial examples that present how to retrieve them all.
Create example products
The first 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;
Assign yrn of the returned product to a variable.
productYRN1 = response.body.yrn;
The second product uses mixins. Start with creating schema and use it to create mixin-based product next:
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 second product:
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.'+possibleStagePrefix+'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;
Assign yrn of the returned product to a variable.
productYRN2 = response.body.yrn;
The third 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.'+possibleStagePrefix+'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;
Assign yrn of the returned product to a variable.
productYRN3 = response.body.yrn;
Create example variant
Assume your customer has enough apples and decided to put a t-shirt into the cart. Creating variant is a multiple-step operation.
First, create a variant schema definition, that defines size and colour attributes.
schema = 'tshirt_variant-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':{
'size': {
'type':'string'
},
'colour': {
'type':'string'
}
}
},{
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json'
}
});
assert.equal(response.status, 201);
}
Create a product to build your variants on. Possible variant options are included in the product definition, in the metadata section. The t-shirt property allows you to use the variant properties defined in the schema, created in previous step.
response = productService.tenant(tenant).products.post({
'metadata': {
'variants':{
'options':{
't-shirt': 'https://api.'+possibleStagePrefix+'yaas.io/hybris/schema/v1/'+tenant+'/tshirt_variant-version1.json'
},
}
},
'code': 'emperors_new_clothes_cuzco_1',
'name': "The Emperor's New Clothes - Cuzco king",
'published': true,
'description': "A t-shirt with The Emperor's New Clothes story motive"
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
response;
Assign yrn of the returned product to a variable.
productYRN4 = response.body.yrn;
Additionally, keep the id of this product. Use it in the next step to create a product variant.
productId4 = response.body.id;
The new variant is black and small t-shirt:
response = productService.tenant(tenant).products.productId(productId4).variants.post({
'code': 'emperors_new_clothes_cuzco_1_black',
'name': "The Emperor's New Clothes - Cuzco king Black",
'options': {
't-shirt': {
'size': 'small',
'colour': 'black'
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
assert.equal(response.status, 201);
response;
Store the new product variant yrn in another variable.
variantYRN1 = response.body.yrn;
Search for all specified products and variants
At this point of the tutorial, you have four products, and one variant, too. It is just an tutorial example, but you can have thousands of products present, while you still are able to search only for selected few, as presented below.
Use yrn as the property that uniquely decides which products you are after. You can add other properties for which you want to retrieve the current information per each returned product or variant.
You must always send yrn, but other properties in the payload are optional:
- You can restrict returned fields separately for products and variants with fields property
- You can request the product data be returned within variant, by using variant.expand property set to product.
response = productService.tenant(tenant).search.post({
"yrns" : [productYRN1,
productYRN2,
productYRN3,
productYRN4,
variantYRN1],
"params" : {
"product" : {
"fields":["name", "description"]
},
"variant" : {
"fields":["options"],
"expand":"product"
}
}
}, {
headers: {
'Authorization': 'Bearer ' + access_token,
'Content-Type' : 'application/json',
'Content-Language': 'en'
}
});
/* Assertions check the correctness of the request response */
/* Status code 200 - success */
assert.equal(response.status, 200);
/* We check the number of objects returned per call. There should be 5 items in the returned array. */
assert.equal(response.body.length, 5);
/* Now we check if the correct items have been returned: first 4 elements are products, while the 5th element is a our variant, based on the 4th product. */
assert.equal(response.body[0].yrn, productYRN1);
assert.equal(response.body[0].name, 'Lobo');
assert.equal(response.body[1].yrn, productYRN2);
assert.equal(response.body[1].name, 'Jubilee apple');
assert.equal(response.body[2].yrn, productYRN3);
assert.equal(response.body[2].name, 'Spartan');
assert.equal(response.body[3].yrn, productYRN4);
assert.equal(response.body[3].name, "The Emperor's New Clothes - Cuzco king");
assert.equal(response.body[4].yrn, variantYRN1);
assert.equal(response.body[4].options['t-shirt'].colour, 'black');
assert.equal(response.body[4].options['t-shirt'].size, 'small');
assert.equal(response.body[4].product.name, "The Emperor's New Clothes - Cuzco king");
assert.equal(response.body[4].product.description, "A t-shirt with The Emperor's New Clothes story motive");
response;
The response should include an array of objects, where each object is a single product or variant, represented only by those properties you have requested by using projection, both for products and variants.
Product Service v2 Media Migration
The Product v2 migration service is included in the Product Content package. With the Product v2 Migration service you can migrate the ordered media collections connected with your products, from Product Service v1 to Product Service v2. The reason why you may want to migrate your media to Product service v2 is that you would no longer need to manually manage the media's position in the media collection.
If you want to keep your media's order from Product Service v1, you need to run this tool just once. Make sure you do it after you have finished using Product Service v1 and before you start using Product Service v2.
Migration service reorders your media to the format supported by Product Service v2, by taking into consideration media's position attribute and the main flag. Note that the main flag has the higher priority than any position value.
Preserving product media order during migration
The Product v2 migration service has one endpoint: /{tenant}/migration
. The endpoint supports two methods:
- POST: This method triggers asynchronous migration process
- GET: This method gives you the status of that process
To perform migration you need to use hybris.product_migrate
scope.
The migration process can have the following statuses:
- Started: Migration process gets this status after it has been triggered.
- Completed: Migration process gets this status after the migration has been completed and all media for all products in a tenant have been reordered successfully.
- Failed: Migration process gets this status after the migration from v1 to v2 has failed
Trigger media migration
You can trigger migration for a particular tenant by performing a POST request on the https://api.beta.yaas.io/hybris/product-v2-migration/v1/{tenant}/migration
endpoint. The process may take a while, depending on the number of products with media to be reordered.
You must provide a proper scope that enables users to perform these 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, see Authorization. For more information about the scopes in the Product service, see the Scopes in the Product Service section.
Response
Of course, there is a response to the above-mentioned request. The list below outlines the content of such response:
- Status code:
202
- Message:
Accepted
Status code:409
- Message:
Conflict
- Message:
- Status code:
403
- Message:
Wrong tenant
- Message:
Check migration status
You can check the status of the migration by performing a GET request on the https://api.beta.yaas.io/hybris/product-v2-migration/v1/{tenant}/migration
endpoint.
You must provide a proper scope that enables users to perform these 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, see Authorization. For more information about the scopes in the Product service, see the Scopes in the Product Service section.
Response
The list below outlines the content of such response:
- Status code:
200
- Message:
OK
- Body
{ "status": "Completed", "metadata": { "createdAt": "2016-02-25T14:00:03.942+0000", "modifiedAt": "2016-02-25T14:00:05.731+0000", "version": 2 }, "finishedAt": "2016-02-25T14:00:04.419Z", "id": "56cf08e30f6e8f2d9fb216a6" }
The response body includes the status of the migration:
- If the status is
Started
, the process is still running and you need to wait until all the media are reordered. - If the status is
Completed
, your media have been successfully migrated and you can start using Product Service v2. - If the status is
Failed
, your migration has been stopped due to an error. Please try again later.
Glossary
Term | Description |
---|---|
code | Product Code, a distinct product item. |
mixin | Customizable definition of additional properties for the product. One product can use many mixins, and one mixin can be used by many products. |
If you find any information that is unclear or incorrect, please let us know so that we can improve the Dev Portal content.
Use our private help channel. Receive updates over email and contact our specialists directly.
If you need more information about this topic, visit hybris Experts to post your own question and interact with our community and experts.