Vendure is built to be highly configurable and extensible. Two methods of providing this extensibility are strategies and configurable operations.
A strategy is named after the Strategy Pattern, and is a way of providing a pluggable implementation of a particular feature. Vendure makes heavy use of this pattern to delegate the implementation of key points of extensibility to the developer.
Examples of strategies include:
OrderCodeStrategy - determines how order codes are generatedStockLocationStrategy - determines which stock locations are used to fulfill an orderActiveOrderStrategy - determines how the active order in the Shop API is selectedAssetStorageStrategy - determines where uploaded assets are storedGuestCheckoutStrategy - defines rules relating to guest checkoutsOrderItemPriceCalculationStrategy - determines how items are priced when added to the orderTaxLineCalculationStrategy - determines how tax is calculated for an order lineAs an example, let's take the . This strategy determines how codes are generated when new orders are created. By default, Vendure will use the built-in which generates a random 16-character string.
OrderCodeStrategyDefaultOrderCodeStrategyWhat if you need to change this behavior? For instance, you might have an existing back-office system that is responsible for generating order codes, which you need to integrate with. Here's how you would do this:
All strategies can make use of existing services by using the init() method. This is because all strategies
extend the underlying InjectableStrategy interface. In
this example we are assuming that we already created an OrderCodeService which contains all the specific logic for
connecting to our backend service which generates the order codes.
We then need to pass this custom strategy to our config:
Strategies can use two optional lifecycle methods:
init(injector: Injector) - called during the bootstrap phase when the server or worker is starting up.
This is where you can inject any services which you need to use in the strategy. You can also perform any other setup logic
needed, such as instantiating a connection to an external service.destroy() - called during the shutdown of the server or worker. This is where you can perform any cleanup logic, such as
closing connections to external services.Sometimes you might want to pass some configuration options to a strategy.
For example, imagine you want to create a custom StockLocationStrategy which
selects a location within a given proximity to the customer's address. You might want to pass the maximum distance to the strategy
in your config:
This config will be passed to the strategy's constructor:
A plugin can change the result of a configured strategy and keep the strategy itself. For this, the plugin adds a
strategy layer. A layer holds the next strategy in the chain (this.inner) and has the same interface.
Write a layer as a class that extends StrategyLayer:
Then add the layer in the configuration function of the plugin with
composeStrategy():
Vendure applies these rules:
priority sets the position of the layer in the chain. A layer with a lower
number gets the call first, as with the priorityValue of promotions. The order of the plugins array has no effect.
If two layers have the same priority, Vendure logs a warning and uses the plugin order for them.composeStrategy() does not add it again, and does not change its options or its priority.with() to give options, and read them from this.options. Vendure calls init() on the base strategy and then on each
layer from the inside out, and waits for each call. It calls destroy() in the opposite order. A layer does not call
init() or destroy() on this.inner.A layer that uses implements must define each method of the strategy interface. When the interface gets a new
method, TypeScript reports an error and you decide what the layer does.
If a layer changes only some methods, extend StrategyLayer.passThrough() and do not use implements. Vendure sends each
method that the layer does not define to the inner strategy. TypeScript checks the signature of each method that the
layer defines:
If the inner strategy does not have an optional method, the layer does not have it either.
A plugin that replaces the whole strategy removes the layers that the project and other plugins added. Vendure logs
a warning that names the dropped layers. To change only the base strategy and keep the layers, use
replaceBase():
Vendure merges the project config before the configuration functions of the plugins run. Thus a project can record
changes for a layer that a plugin adds later:
If the project config adds a layer itself, for example composeStrategy(MinimumPriceLayer.with({ minimum: 50 }, { priority: 250 }), ...),
the options and the priority of the project win. The plugin cannot add the layer again.
When a strategy has layers, the config option holds a different object than the base strategy. Thus instanceof and
constructor.name on the config option do not see the base strategy. Use
getBaseStrategy():
Do not keep a reference to a composed strategy in the configuration function. A later change to the chain gives a
new object.
orderOptions.activeOrderStrategy accepts a list, and Vendure calls the entries in list order. To add an entry, use
addStrategyToList() and do not assign a new array:
Vendure sorts the list by priority after all configuration functions have run. An entry with a lower number gets the
call first. Entries that the project gives as a plain array, and the default entry, have the priority 0. Use a negative
priority to call an entry before them. A project removes an entry, also the default entry, with removeStrategyFromList():
Configurable operations are similar to strategies in that they allow certain aspects of the system to be customized. However, the main difference is that they can also be configured via the Dashboard. This allows the store owner to make changes to the behavior of the system without having to restart the server.
So they are typically used to supply some custom logic that needs to accept configurable arguments which can change at runtime.
Vendure uses the following configurable operations:
CollectionFilter - determines which products are included in a collectionPaymentMethodHandler - determines how payments are processedPromotionCondition - determines whether a promotion is applicablePromotionAction - determines what happens when a promotion is appliedShippingEligibilityChecker - determines whether a shipping method is availableShippingCalculator - determines how shipping costs are calculatedWhereas strategies are typically used to provide a single implementation of a particular feature, configurable operations are used to provide a set of implementations which can be selected from at runtime.
For example, Vendure ships with a set of default CollectionFilters:
When setting up a Collection, you can choose from these available default filters:
When one is selected, the UI will allow you to configure the arguments for that filter:
Let's take a look at a simplified implementation of the variantNameCollectionFilter:
Here are the important parts:
code property which is a unique string identifier.description property which is a localizable, human-readable description of the operation.args property which defines the arguments which can be configured via the Dashboard. If the operation has no arguments,
then this would be an empty object.apply() method
is used to apply the filter to the query builder.The args property is an object which defines the arguments which can be configured via the Dashboard. Each property of the args
object is a key-value pair, where the key is the name of the argument, and the value is an object which defines the type of the argument
and any additional configuration.
As an example let's look at the dummyPaymentMethodHandler, a test payment method which we ship with the Vendure Framework:
The following properties are used to configure the argument:
The following types are available: string, int, float, boolean, datetime, ID.
A human-readable label for the argument. This is used in the Dashboard.
A human-readable description for the argument. This is used in the Dashboard as a tooltip.
boolean
Whether the argument is required. If true, then the Dashboard will not allow the user to save the configuration
unless a value has been provided for this argument.
any (depends on the type)
The default value for the argument. If not provided, then the argument will be undefined by default.
boolean
Whether the argument is a list of values. If true, then the Dashboard will allow the user to add multiple values
for this argument. Defaults to false.
Allows you to specify the UI component that will be used to render the argument in the Dashboard, by specifying
a component property, and optional properties to configure that component.
A full description of the available UI components can be found in the Custom Fields guide.
boolean
Since v3.8.0, setting secret: true on a string argument encrypts its value in the database and redacts it in API
responses for users who lack the ReadSecret permission. Use it for values such as the API key of a payment
method handler. List arguments cannot be secret. This requires an encryption key to be configured; see the
Secret Fields guide for setup and details.
Configurable operations are instantiated before being passed to your config, so the mechanism for injecting dependencies
is similar to that of strategies: namely you use an optional init() method to inject dependencies into the operation instance.
The main difference is that the injected dependency cannot then be stored as a class property, since you are not defining a class when you define a configurable operation. Instead, you can store the dependency as a closure variable.
Here’s an example of a ShippingCalculator that injects a service which has been defined in a plugin:
Architecture reviews, custom plugin work, migrations, ongoing support. Get a hand from the team that builds Vendure.
Talk to the team