Vue Storefront is now Alokai! Learn More
Extending a Module

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 defined
    • around interceptor 1 up to next() call
      • around interceptor 2 up to next() call
        • around interceptor 3 up to next() call
          • getProducts 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`);
    },
  },
};