Extending a Module
In most cases, a module exports the base set of communication methods and utilities.
However, developers might require additional functionality that the module doesn't provide.
To address this issue without requesting new features from the module's author, each module can be customized to meet the specific needs of developers.
Quick start
To extend the SDK module, add either an object or a function to the buildModule
function parameters.
import { UnifiedEndpoints } from "storefront-middleware/types";
// Extension as an object
const sdk = initSDK({
commerce: buildModule(
middlewareModule<UnifiedEndpoints>,
{
apiUrl: "http://localhost:8181/commerce",
},
{
extend: {
customMethod: () => {
console.log("Custom method");
},
},
}
),
});
await sdk.commerce.customMethod(); // Logs "Custom method"
// Extension as a function
const sdk = initSDK({
commerce: buildModule(
middlewareModule<UnifiedEndpoints>,
{
apiUrl: "http://localhost:8181/commerce",
},
(extensionParams, { methods, context }) => ({
extend: {
customMethod: () => {
console.log("Extension params: ", extensionParams);
},
},
}),
{ customParam: "customValue" }
),
});
await sdk.commerce.customMethod(); // Logs "Extension params: { customParam: 'customValue' }"
Extensions initialized as functions can be configured with the extensionParams
and have access to the module internals - methods
and context
.
Extension properties
An example extension might look like this:
import { Extension } from "@vue-storefront/sdk";
export const sapccExtension = {
interceptors: [],
utils: {},
extend: {},
override: {},
subscribers: {},
};
Let's review each of the extension properties to understand their roles and responsibilities.
interceptors
before
interceptors
before
interceptors allow you to define a list of interceptors that can modify the input parameters of an SDK method.
These interceptors will run before your method call and will modify the input parameters before they enter the SDK method.
before
interceptors should not change the return type of the parameter!
The idea of before
interceptors is to modify the input parameters values only.
It should not be used to change the contract, that may break the typing and cause unforeseen issues.
import { UnifiedEndpoints } from "storefront-middleware/types";
export const sapccExtension = {
interceptors: [
{
before: {
getProducts: (
args: Parameters<UnifiedEndpoints["getProducts"]>
): Parameters<UnifiedEndpoints["getProducts"]> => {
console.log(`Interceptor modifies the input of getProducts method.`);
return [
{
id: 2,
},
];
},
},
},
],
};
after
interceptors
after
interceptors allow you to define a list of interceptors that can modify the output of an SDK method.
These interceptors run after your method call and modify the output result.
after
interceptors should not change the return type of the parameter!
The idea of after
interceptors is to modify the output value only.
It should not be used to change the contract, that may break the typing and cause unforeseen issues.
import { UnifiedEndpoints } from "storefront-middleware/types";
export const sapccExtension = {
interceptors: [
{
after: {
getProducts: (
res: ReturnType<UnifiedEndpoints["getProducts"]>
): ReturnType<UnifiedEndpoints["getProducts"]> => {
console.log(`Interceptor modifies the output of getProducts method.`);
return [{ id: res[0].id, name: "Hello world" }];
},
},
},
],
};
around
interceptors
around
interceptors allow you to define a list of interceptors that can modify the input and output of an SDK method and have access to the original method.
These interceptors run after all before
interceptors and before all after
interceptors.
around
interceptors should not change the return type of the parameter!
it is up to the developer to call the original method, if it's not called, the SDK method won't be executed.
import { UnifiedEndpoints } from "storefront-middleware/types";
type GetProductsFn = UnifiedEndpoints["getProducts"];
export const sapccExtension = {
interceptors: [
{
around: {
getProducts: (
next: GetProductsFn,
...args: Parameters<GetProductsFn>
): ReturnType<GetProductsFn> => {
// Do something before the method call
// ...
// Call the original method, if it's not called, the SDK method won't be executed
const result = next(...args);
// Do something after the method call with the result
result.myCustomProperty = "Hello world";
return result;
},
},
},
],
};
Execution order of interceptors
Assume that you have the following interceptors defined for the getProducts
method:
const extension = {
interceptors: [
{
before: {
getProducts: (args: any) => {
return ["modified-args"];
},
},
after: {
getProducts: (result: any) => {
return `${result}-modified-result`;
},
},
around: {
getProducts: [
(next: any, arg1: any, arg2: any) => {
const result = next(arg1, arg2);
return result + "-around1";
},
(next: any, arg1: any, arg2: any) => {
const result = next(arg1, arg2);
return result + "-around2";
},
(next: any, arg1: any, arg2: any) => {
const result = next(arg1, arg2);
return result + "-around3";
},
],
},
},
],
};
The execution order of interceptors will be as follows:
- all
before
interceptors in the order they are definedaround
interceptor 1 up to next() callaround
interceptor 2 up to next() callaround
interceptor 3 up to next() callgetProducts
method
around
interceptor 3 after next() call
around
interceptor 2 after next() call
around
interceptor 1 after next() call
- all
after
interceptors in the order they are defined
getProducts
method will be called only once.
utils
The utils
property allows you to define methods that can be used to extend the module's functionalities. Utils should not depend on any other components
Why to use utils
methods?
Imagine you're creating an integration with a payment provider.
To initialize the payment, you need to pass a specific config, that might be hard to create for someone who is not familiar with the payment provider.
In this case, you can create a utils
method that will create the config for you.
Such method won't be asynchronous and won't be affected by the interceptors.
export const sapccExtension = {
utils: {
buildConfig: (config: any) => {
return {
...config,
paymentServiceProvider: "SAPCC-Payments",
};
},
},
};
Example of using utils
method:
// Using utils methods
const sapccPaymentConfig = sdk.sapcc.utils.buildConfig(baseConfig);
extend
extend
can be used to create a new method that is not covered by the module.
These methods are affected by interceptors
Like the built-in SDK methods, methods in extend
are impacted by your interceptors.
Let's assume you want to add a custom authenticate
method that calls an external service to authenticate the user and want to use a fallback if the service is not available.
You can add the authenticate
method to the extend
property and reuse the existing login
method as a fallback.
import { UnifiedEndpoints } from "storefront-middleware/types";
const sdk = initSDK({
auth: buildModule(
middlewareModule<UnifiedEndpoints>,
{
apiUrl: "http://localhost:8181/auth",
},
(_extensionParams, { methods }) => ({
extend: {
authenticate: async (username: string, password: string) => {
try {
const response = await fetch(
"http://external-service/authenticate",
{
method: "POST",
body: JSON.stringify({ username, password }),
}
);
if (response.ok) {
return response.json();
}
} catch (e) {
console.error("External service is not available. Using fallback.");
}
return await methods.login(username, password);
},
},
})
),
});
You can also access the module context
. Each module author can define its own context. middlewareModule
provides access to module options and requestSender
through its context
.
import { UnifiedEndpoints } from "storefront-middleware/types";
const sdk = initSDK({
auth: buildModule(
middlewareModule<UnifiedEndpoints>,
{
apiUrl: "http://localhost:8181/commerce",
},
(_extensionParams, { methods, context }) => ({
extend: {
authenticate: async (username: string, password: string) => {
try {
console.log("Base url: ": context.options.apiUrl);
const response = await context.requestSender("/authenticate", {
username,
password,
});
return response;
} catch (e) {
console.error("Auth service is not available. Using fallback.");
}
return await methods.login(username, password);
},
},
})
),
});
As a rule of thumb, it's recommended to add options and client to the context object as it allows for easy implementation of custom methods.
:::
warning the context
object is optional and might not be available in all modules. Please, check the module's documentation to see if it's available and what properties it contains. You can also check type-hinting in your IDE to see what properties are available.
:::
override
While extend
allows you to create a new method, override
allows you to change the behavior of the existing method.
These methods are affected by interceptors
Like the built-in SDK methods, methods in override
are impacted by your interceptors.
export const sapccExtension = {
override: {
getProducts: (params: any) => {
// Override the getProducts method
},
},
};
It might be useful when you want to change the behavior of the existing method, like adding a new parameter or changing the default behavior.
subscribers
Subscribers are functions that are called when a specific event is emitted.
Events that can be emitted are:
*_before
- run the function before EACH method of EACH module,*_after
- run the function after EACH method of EACH module,<module>_before
- run the function before EACH method of the specific module,<module>_after
- run the function after EACH method of the specific module,<module>_<method>_before
- run the function before the specific method of the specific module,<module>_<method>_after
- run the function after the specific method of the specific module.
It implements the publish-subscribe pattern.
It's a great place to add some custom logic, like logging or analytics.
export const sapccExtension = {
subscribers: {
sapcc_before: () => {
console.log(`Before each SAPCC method do something`);
},
sapcc_after: () => {
console.log(`After each SAPCC method do something`);
},
},
};