Elastic Path Commerce Development

Resource Repositories

Resource Repositories

A repository class provides information through either Commerce Engine or another repository to a resource via a consistent set of CRUD (create, read, update, delete) methods. Repositories translate between commerce domain objects and API representations, abstracting the complexity of Commerce Engine's databases and business logic from the API layer.

How Repositories Work

Repositories typically implement either one of the interfaces in the com.elasticpath.repository package, and one or more of their methods. A resource may have multiple repositories implemented.

CRUD Repositories

CRUD repositories implement the Repository interface, and at least one of the following methods:

  • submit(): Creates a new entity or updates an existing entity. If the entity does not exist, it is created. If it does, it is either updated based on the information submitted, or its identifier is returned.
    • E.g. a) Create a new shipping address for a customer's profile. b) Prompting a user to double-check their actions if they've added two of the same item to the cart. See Using submit() for more information.
  • findOne(): Finds (reads) a single entity.
    • E.g. Find a specific shipping address for a customer's profile.
  • findAll(): Finds (reads) all entities of a specified type.
    • E.g. Find all shipping addresses for a customer's profile.
  • delete(): Deletes an entity.
    • E.g. Delete a shipping address from a customer's profile.
  • update(): Updates an entity.
    • E.g. Delete a shipping address from a customer's profile.

Using submit()

The submit() method is available for use in scenarios where you need to do any of the following upon a form submission:

  • Consistently create a new entity.
    • E.g. Create a new shipping address for a customer's profile.
  • Provide users with messaging.
    • E.g. Prompting a user to double-check their actions if they've added two of the same item to the cart.
  • Update the status of an existing entity.
    • E.g. Incrementing the amount of an item that currently exists in the cart when the user adds that item to the cart a second time.

Links Repositories

Links repositories are used to link an identifier of one resource type with zero or more identifiers of another resource type. They implement the LinksRepository interface and at least one of the following methods:
  • getElements(): Returns all identifiers of the linked resource type.
    • E.g. Return all identifiers for items on a wishlist.
  • deleteAll(): Deletes all elements of the linked resource type.
    • E.g. Delete all items from a wishlist (but not the wishlist itself).

Alias Repositories

Alias repositories resolve a resource identifier from an alias (pointer). Aliases are typically human-readable, for example the default cart alias. Aliases can also differentiate between functionally identical but conceptually different resources: for example, the idea of a holiday cart, which may require differentiation from a standard cart for business reasons. Alias repositories implement the the AliasRepository interface and the following method:

  • resolve(): Returns the linked identifier for the alias identifier.
    • E.g. Resolve a default cart identifier to a shopping cart identifier.

Pagination Repositories

Pagination repositories supply additional pagination information for entities and decipher links among and between paginated results. They implement the PaginationRepository interface and all of the following methods:

  • getPaginationInfo(): Returns the pagination entity containing pagination information for the resource.
    • E.g. Get the paginated results for a searched keyword.
  • getElements(): Returns linked paginated items for the resource.
    • E.g. Get the item identifier from a keyword search result.
  • getPagingLinks(): Returns links for navigating between results pages.
    • E.g. Get paging links between keyword search results pages.

Selector Repositories

Selector repositories help in implementing the selector resource pattern. For example, a resource for selecting a billing address or a resource for selecting shipping address are implemented using the selector resource pattern. Selector repositories implement the SelectorRepository interface and all of the following methods:

  • getChoices(): Returns the choices for the selector.
    • E.g. Get all the addresses for a customer as choices for a billing address.
  • getChoice(): Returns a particular choice.
    • E.g. Get a particular address for a customer.
  • selectChoice(): Select a particular choice.
    • E.g. Select an address as a billing address.

Extending Repositories

To add functionality or fields to an entity, or to change its default behaviour, you must extend a repository. To extend a repository, either override its existing methods or implement new methods.

  1. Extend a repository in the ep-commerce/extensions/cortex/repositories/src/main/java module in the following format:
    @Component(property = {Constants.SERVICE_RANKING + ":Integer=101"}, service = Repository.class)
    public class repository<E extends MyResourceEntity, I extends MyResourceIdentifier>
             extends repositry<MyResourceEntity, MyResourceIdentifier> {
    Where MyResourceEntity is any default Elastic Path Commerce entity and MyResourceIdentifier is any default Elastic Path Commerce identifier.
Note: For more information about the SERVICE_RANKING property , see the OSGi javadoc. If more than one service implements the specified class, the one with the highest ranking is returned. The default ranking is 0. Any class with service ranking more than the default value overrides the default class.

Implementing Additional CRUD Functionality

A repository may not implement all CRUD methods by default. To implement additional CRUD functionality, implement the desired CRUD method in an existing repository.

For example, the ability for a user to create more than one wishlist per profile is not implemented by default. To implement this functionality, extend the out of the box repository by adding a create() method to it.

Changing the Default Behaviour of a Repository's CRUD Method

To change the default behaviour or business logic of an repository, override a repository's CRUD method with the desired functionality.

Adding Fields to a Repository

Sometimes, when extending a Commerce Engine domain object like addresses, you may need to pass extra fields to the repository and resource. To add fields to an entity that the repository returns, override the appropriate CRUD methods in a repository class to include the extra fields.

An example of this is examined in detail in the Extend a Helix Resource tutorial.

Creating New Repositories for New Resources

When creating a custom resource, you will have to write the repositories from scratch. Your repository class should do the following:
  • Annotate the repository class with org.osgi.service.component.annotations.Component, to ensure it is exposed as an OSGi service and injectable.
  • Implement the Repository or LinksRepository interface.
  • Implement at least one CRUD method provided by the interfaces.
  • Use either public or protected to ensure method extensibility.

Implementing CRUD Methods

Repositories implement an interface's methods based on a resource's intended functionality. To implement a CRUD method, you will typically invoke one or more Commerce Engine services or legacy repository methods using the ReactiveAdapter class. ReactiveAdapter helps to integrate CE Services and legacy repositories into reactive Rx execution chains.

To use the ReactiveAdapter methods, inject an instance of ReactiveAdapter into your repository implementation, then use one of its methods to call a Commerce Engine service or legacy repository:

@Inject
CarsRepositoryImpl(
  @Named("reactiveAdapter")
  final ReactiveAdapter reactiveAdapter) {
    this.reactiveAdapter = reactiveAdapter;
 }
          
...
        

Calling Commerce Engine Services

To initiate a non-blocking Commerce Engine service call with automatic null and exception handling, use one of the following ReactiveAdapter methods:
  • fromService() – returns a RxJava Observable which wraps the return value of a Commerce Engine service.
  • fromServiceAsSingle() – returns a RxJava Single which wraps the return value of a Commerce Engine Service.
  • fromServiceAsCompletable() - returns whether or not an operation was successful.

An example of returning an Observable from a Commerce Engine service is below:

Observable o = reactiveAdapter.fromService(() -> ceService.myMethod())
          

ReactiveAdapter methods handle InvalidBusinessStateException (HTTP 409) and EpValidationException (HTTP 400) exceptions automatically, and return appropriate structured error messages.

Custom Exception Handling when Calling Commerce Engine Services

If needed, you can still do custom exception handling:

Completable c = reactiveAdapter.fromServiceAsCompletable(() -> {
    try {
      return customerService.update(customer);
    } catch (UserIdExistException error) {
      throw exceptionTransformer.getResourceOperationFailure(error);
    }
  });
}
          

Or:

Completable c = reactiveAdapter.fromServiceAsCompletable(() -> customerService.update(customer))
    .onErrorResumeNext(throwable -> {
      try {
        throw throwable;
      } catch (NullPointerException npe) {
      return Completable.error(ResourceOperationFailure.notFound());
    }
  });
}
          

Calling Legacy Repositories

You may have to call a legacy resource's repository from your resource. Legacy repository methods return an ExecutionResult. ReactiveAdapter provides methods for returning this data to your repository as a RxJava type, depending on the type of data wrapped in the ExecutionResult.

If the legacy repository call returns an ExcecutionResult containing an Iterable as data use ReactiveAdapter.<String, List<String>>fromRepository():

Observable o = reactiveAdapter.<String, List<String>>fromRepository(() -> repository.getShoppingItems())
          

If the legacy repository call returns an ExecutionResult containing a non-Iterable, non-null Object use ReactiveAdapter.<String, List<String>>fromRepositoryAsSingle():

Single s = reactiveAdapter.<String, List<String>>fromRepositoryAsSingle(() -> repository.getDefaultCard())
          

If the legacy repository call returns an ExecutionResult containing a null value as data use ReactiveAdapter.<String, List<String>>fromRepositoryAsCompletable():

Completable c = reactiveAdapter.<String, List<String>>fromRepositoryAsCompletable(() -> repository.updateCustomer())
          

All these methods are non-blocking. Typically the legacy repository method does the exception handling.

Injecting a Repository into a Prototype

In order to inject a Repository (implementing any one of the Repository, LinksRepository, AliasRepository, PaginationRepository, SelectorRepository interfaces) into a prototype class, use the @Inject annotation in combination with the @ResourceRepository annotation in the method signature:

@Inject
public MyPrototype(@ResourceRepository Repository<MyEntity, MyIdentifier> repository) 
{ ... }
      

Make sure to type the generic type parameter of the Repository classes to the appropriate ResourceEntity and ResourceIdentifier.