Vue Storefront is now Alokai! Learn More
Generated API

Generated API

Overview

Alokai takes advantage of OpenAPI to create a TypeScript API client for SAP Commerce Cloud. This client helps us quickly set up Express endpoints for the Alokai Storefront.

When you first set up a SAP Commerce Cloud instance - out of the box - its API starts with a standard set of controllers to handle fundamental e-commerce features like "add to cart", "log in" and others. The default TypeScript API client for our integration for SAP Commerce Cloud - out of the box - knows how to query only that initial set of methods.

If you customized your SAP Commerce Cloud instance so that...:

  1. its default API methods have different parameters
  2. it has more API controllers thanks to Java extensions you have written
  3. it runs a more recent version of SAP Commerce Cloud and the specification of the initial API changed (and the Alokai SAP CC integration wasn't updated for that version yet)

... you'll run into a situation where the Alokai integration will query your SAP CC API as if it were the default one, while it is vastly different. This means that certain methods may be missing from IDE auto-completions when using them with the Alokai SDK, reducing the developer experience. In other cases, calling SDK methods may fail due to e.g. differing parameter names, even though static type checking found no errors.

Generating your own TypeScript API client lets you bring the Alokai integration up to date with your customized SAP CC instance, and make your custom methods show up in the SDK IDE auto-completions.

Generating your own TypeScript API client

To generate your own TypeScript API client, you will need to open the terminal and run the OpenAPI Generator CLI, which is a npm package (npx @openapitools/openapi-generator-cli generate [required arguments described in further sections]). Please follow the steps below.

1. Prerequisites

1. Java 11

Ensure the Java 11 runtime is installed in your system, as OpenAPI Generator CLI depends on it. On MacOS, it can be done with the command brew install openjdk@11. For alternative means of installation, see here.

2. Package versions

  • Ensure your @vsf-enterprise/sapcc-api version is at least 8.0.6 so that createProxyApi is available.
  • Ensure your @vue-storefront/middleware version is at least 4.3.1 so that extendApiMethods supports createProxyApi.

2. Find your SAPCC instance's OpenAPI schema URI

If you've already completed the quick start guide, you already know the contents of the SAPCC_API_URI environment variable. This makes it easy to find out your SAP Commerce Cloud instance's OpenAPI schema URI, as the formula for it is simply:

[your SAPCC_API_URI here]/api-docs

Keep in mind that in most cases, SAPCC_API_URI is not a simple hostname, but that it also has a /occ/v2 suffix

For example, the final URL can be similar to:
https://api.c1jvi8hu9a-vsfspzooa1-d1-public.model-t.cc.commerce.ondemand.com/occ/v2/api-docs

To verify that the URL is correct, you can open it in your browser and ensure that it shows a block of unformatted JSON starting with {"openapi":.

In case of problems, please refer to Swagger specification about Schema and Resource Listing, as SAP Commerce Cloud uses Swagger to implement OpenAPI.

3. Running the OpenAPI Generator CLI

Having completed the two previous steps, you can now run the OpenAPI Generator CLI to generate the TypeScript API client.

Please run the below command in the directory where your middleware configuration is (apps/storefront-middleware if your project is generated from an Alokai CLI/cloud bootstrapper).

Remember to pass the OpenAPI schema URI from the first step of this guide to the --input-spec parameter.

Changing the generated class name

In the below command, there's the parameter --openapi-normalizer SET_TAGS_FOR_ALL_OPERATIONS=sapcc
This will generate a class named SapccApi. If you are generating a TypeScript API client for a different API than the SAP Commerce Cloud OCC API (for example, the SAP Commerce Cloud Assisted Service Module, which has a separate OpenAPI URI), you can instead set --openapi-normalizer to SET_TAGS_FOR_ALL_OPERATIONS=sapccAsm, which will generate a class named SapccAsmApi

npx @openapitools/openapi-generator-cli generate --input-spec [your SAPCC instance's OpenAPI schema URI here] --generator-name typescript-axios --output custom-sapcc-openapi --skip-validate-spec --openapi-normalizer SET_TAGS_FOR_ALL_OPERATIONS=sapcc --additional-properties=useSingleRequestParameter=true,supportsES6=true

Using --openapi-normalizer SET_TAGS_FOR_ALL_OPERATIONS=[...] is crucial if you want to seamlessly replace our client with your own. Building API we expect to have a single class with methods representing all endpoints. It is possible to generate it in a different way but you will not be able to use it with our generator.

Running the command will create a custom-sapcc-openapi/ directory next to the package.json file closest to your current folder (not your current folder itself). Move the newly created custom-sapcc-openapi/ directory to the directory where the SAP Commerce Cloud extension is (apps/storefront-middleware/integrations/sapcc/custom-sapcc-openapi). The most important file inside is api.ts, which contains the SapccApi class definition. That class can call all the HTTP methods exposed by your SAPCC API, like addCartEntry etc.

Saving this command in a package.json script

Adding this command as a package.json script in your project's code repository will help you re-generate the TypeScript API client with a fresh OpenAPI spec if needed in the future.

This could be useful if e.g. a new endpoint is added to your SAP Commerce Cloud instance's API (which can happen after a SAP Commerce Cloud update, or when you install a custom OCC API extension).

Additional features

We preserve the interfaces of methods that are exposed as API endpoints which means that you should expect exactly the same input and output as described in the OpenAPI schema (Swagger). However we add some additional features that work on top of the generated API client. We created a proxy builder function that takes the generated SDK class and adds some additional features to it. The features that we add are:

  • logger - we add a logger to the client that logs all errors. This way, we can easily debug the client and see what is going on under the hood. You can read more about the logger in the Logger Docs section.
  • injection of common parameters - some parameters repeats in all endpoints makes it cumbersome to add them to each request. Example of such parameters are baseSiteId, userId or fields. Even though these parameters are required in the OpenAPI schema, we mark them as optional and inject them into each function call.

Connecting Your Custom API Endpoints to the Alokai Storefront

Once the API client is generated, you can use it in the middleware configuration to build and expose the final integration endpoints. For example, you can follow the instructions below to create a proxy class for a custom SAP CC instance. This assumes you moved the generated OpenAPI Generator CLI custom-sapcc-openapi directory with its contents to apps/storefront-middleware/integrations/sapcc/custom-sapcc-openapi.

1. Adding types

Open the file apps/storefront-middleware/integrations/sapcc/types.ts and add the following lines:

  import { WithoutDefaults } from "@vsf-enterprise/sapcc-api";
+ import { SapccApi } from "./custom-sapcc-openapi";
 
  export type { Endpoints as UnifiedEndpoints } from "@vsf-enterprise/unified-api-sapcc";
  export type { Endpoints as CommerceEndpoints } from "@vsf-enterprise/sapcc-api";
+ export type CustomSapccEndpoints = WithoutDefaults<SapccApi>;

2. Adding the extension

Create the file apps/storefront-middleware/integrations/sapcc/extensions/custom-sapcc.ts and put the following code inside:

import { ApiClientExtension } from "@vue-storefront/middleware";
import { createProxyApi } from "@vsf-enterprise/sapcc-api";
import { CustomSapccEndpoints } from "../types";
import { SapccApi } from "../custom-sapcc-openapi";

export const mySapccExtension: ApiClientExtension = {
  name: "customsapcc",
  isNamespaced: true,
  extendApiMethods(context) {
    return {
      ...createProxyApi(SapccApi, context.client),
    }
  },
};

Then export the extension from the apps/storefront-middleware/integrations/sapcc/extensions/index.ts file:

  export * from "./unified";
  export * from "./cdn";
+ export * from "./custom-sapcc"

The createProxyApi is our utility function that takes the generated SDK class and adds some additional features to it but you can also write your own function that will fit your needs. The main goal is to have a function that will return an object with enumerable properties that are functions that will be exposed as API endpoints.

The optional axiosInstance argument allows providing a custom Axios instance for sending HTTP requests from the new generated API client. This is useful if you have needs related to analytics or custom authorization, which you could implement using a custom Axios instance with the right Axios interceptors (pre- and post- request hooks). Thus, you can optionally create a custom Axios client for passing to createProxyApi's second argument like below.

  import axios from "axios";

  const myClient = axios.create(/* add options, interceptors here */);

  // ...
   extendApiMethods(context) {
     return {
+      ...createProxyApi(SapccApi, myClient),
     }
   },

3. Registering the extension

To use the extension, you need to add it to the list of extensions in the middleware config in apps/storefront-middleware/integrations/sapcc/config.ts:

- import { cdnExtension, unifiedApiExtension } from './extensions'; 
+ import { mySapccExtension, cdnExtension, unifiedApiExtension } from "./extensions"
  export const config = {
    location: '@vsf-enterprise/sapcc-api/server',
  
    configuration: {
      OAuth: {
        uri: SAPCC_OAUTH_URI,
        clientId: SAPCC_OAUTH_CLIENT_ID,
        clientSecret: SAPCC_OAUTH_CLIENT_SECRET,
        tokenEndpoint: SAPCC_OAUTH_TOKEN_ENDPOINT,
        tokenRevokeEndpoint: SAPCC_OAUTH_TOKEN_REVOKE_ENDPOINT,
  
        cookieOptions: {
          'vsf-sap-token': {
            secure: NODE_ENV !== 'development'
          }
        }
      },
  
      api: {
        uri: SAPCC_API_URI,
        baseSiteId: 'apparel-uk',
        catalogId: 'apparelProductCatalog',
        catalogVersion: 'Online',
        defaultLanguage: 'en',
        defaultCurrency: 'USD'
      }
    },
  
    extensions: (extensions: ApiClientExtension[]) => [
      ...extensions,
      unifiedApiExtension,
      cdnExtension,
      ...(IS_MULTISTORE_ENABLED === 'true' ? [multistoreExtensionFactory()] : []),
      checkoutExtensionFactory(),
+     mySapccExtension
    ]
  } satisfies Integration<Config>;

To use the newly enabled extension with the Alokai SDK, pass it to your SDK configuration in the frontend app as such (apps/storefront-unified-nextjs/sdk/config.ts):

  import type {
    CommerceEndpoints,
    UnifiedCmsEndpoints,
    UnifiedEndpoints,
    CheckoutEndpoints,
+   CustomSapccEndpoints
  } from 'storefront-middleware/types';
  import { initSDK, middlewareModule } from "@vue-storefront/sdk";
  
  export const sdk = initSDK({
    commerce: buildModule(middlewareModule<CommerceEndpoints>, {
      apiUrl: `${config.middlewareUrl}/commerce`,
      cdnCacheBustingId: config.cdnCacheBustingId,
      defaultRequestConfig: {
        headers: getRequestHeaders(),
      },
+   }),
+   customsapcc: buildModule(middlewareModule<CustomSapccEndpoints>, {
+     apiUrl: `${config.middlewareUrl}/commerce/customsapcc`, // "customsapcc" suffix because mySapccExtension has `isNamespaced: true` and has `name: 'mysapcc'`
+   }),
+ });

Then you can use your new extension through the SDK in your frontend the standard way:

useSdk().customsapcc.someMethod()

Code example of createProxyApi function

export const createProxyApi = <T extends object>(
  OpenApiClass: {
    new (...params: ConstructorParameters<typeof SapccApi>): T;
  },
  axiosInstance?: AxiosInstance
) => {
  /*
   * For more info about JavaScript Proxies, see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy
   *
   * This Proxy is meant to be a "behaves-like-an-object" adapter for the *CLASS* `new SapccApi()`
   * `apiClientFactory` at the bottom of this file wasn't made to accept classes in the `{ api: }` param (`{ api: new SapccApi() }`)
   * So we need to wrap the SapccApi class with an object, which this proxy does.
   *
   */
  return new Proxy(new OpenApiClass(undefined, undefined, axiosInstance), {
    /*
     * This method describes what happens when a function tries to list the keys of our proxy object. e.g. `Object.keys(proxy)`
     * In the case of this implementation, it will list all the methods of the `SapccApi` class instance. e.g. `Object.keys(proxy) // ['createCart', 'getProduct', ...]
     * @example `Object.keys(proxy) // lists all methods in `new SapccApi()`
     *
     * `ownKeys` is required because otherwise `Object.entries` call in the below link in the middleware package always returns an empty array (see @example below)
     * @see {@link https://github.com/vuestorefront/vue-storefront/blob/main/packages/middleware/src/apiClientFactory/applyContextToApi.ts#L33}
     * @example `Object.entries(new SapccApi()) // returns []
     * This is because Object.keys only lists *own* *properties* of an *object*, but we passed a *class* which doesn't have *own* properties - it has only *inherited* properties in __proto__.
     */
    ownKeys(target) {
      /*
       * "Damn why didn't he use `Object.keys` to list class methods? Would be easier?"
       * Object.keys(new SapccApi())                            -> [], wrong
       * Object.getOwnPropertyNames(new SapccApi())             -> [], wrong
       * Object.keys((new SapccApi()).__proto__)                -> [], wrong
       * Object.getOwnPropertyNames((new SapccApi()).__proto__) -> actually gets class methods
       */
      return Object.getOwnPropertyNames(
        // @ts-expect-error Hidden property
        // eslint-disable-next-line no-proto
        target.__proto__
      );
    },
    // https://stackoverflow.com/questions/65339985/why-isnt-ownkeys-proxy-trap-working-with-object-keys
    getOwnPropertyDescriptor(_target, property) {
      return {
        // we do not want to iterate over the constructor
        enumerable: property !== "constructor",
        configurable: true,
      };
    },
    /*
     * This is the function that defines what happens
     * when the expression `proxy['prop']` is evaluated.
     *
     * Normally we'd do just write `return (new SapccApi())[prop]`
     *
     * All the quirks are described in the comments in the implementation below.
     *
     * @see {@link https://github.com/vuestorefront/vue-storefront/blob/main/packages/middleware/src/apiClientFactory/applyContextToApi.ts#L40}
     */
    get(openApiClientInstance, methodName: string) {
      if (methodName === "then") {
        return Promise.resolve(openApiClientInstance[methodName]);
      }

      if (!(methodName in openApiClientInstance)) {
        throw new Error(
          `The API client tried to find a method named ${`methodName`} but no such method exists within the integration`
        );
      }

      /*
       * Normally we'd just `return target[prop]`,
       * but we need to wrap that in a function that reorders the arguments.
       * `@vue-storefront/middleware` expects to receive methods in the format:
       * `someApiMethod(middlewareContext, methodProps)`
       * (see {@link https://github.com/vuestorefront/vue-storefront/blob/main/packages/middleware/src/apiClientFactory/applyContextToApi.ts#L40}):
       * but all of `new SapccApi` methods expect the format:
       * `someApiMethod(methodProps, options?)`
       * So in real life like:
       * `createCart({ baseSiteId: 'apparel-uk'}, { headers: {...}})`
       *
       * This also gives you access to the context, so you can grab the `apiUrl` from the middleware configuration and pass it to the method. By default it just queries our SAP CC instance.
       */
      return async (
        context: Context["$sapcc"],
        // most of the time this is actually just [object], not object[] so you can do args[0]
        ...args: object[]
      ) => {
        /* Calling e.g. proxy.createCart() without the .bind will result in `this.configuration is undefined` errors
         * this meant that the function's lexical context "forgot" to which class it belongs,
         * so we use .bind() to reconnect it to the SapccApi class instance.
         */
        const apiFunction = openApiClientInstance[methodName].bind(
          openApiClientInstance
        ) as GenericOpenapiMethod;

        const { logger } = context.config;

        /*
         * The regular props that the method accepts in query or body (baseSiteId, userId, and others, depending on what the method is, like creatCart has different args etc)
         * Always append baseSiteId from the config - we don't let the user provide it on a per-method basis
         * The BaseProps type is included in the union because of Alokai SDK Methods
         */
        const props: Record<string, unknown> & BaseProps = {
          baseSiteId: context.config.api.baseSiteId, // this is required for SapccAPI
          baseSite: context.config.api.baseSiteId, // this is required for SapccAsmAPI
          userId: getUserIdFromRequest({ req: context.req, props: args[0] }),
          fields: "FULL",
          ...args[0],
        };

        const axiosRequestOptions = createRequestOptionsWithJsonHeader({
          context,
          props,
          tokenMode: getTokenModeByMethodName(methodName),
        });

        try {
          const response = await apiFunction(props, axiosRequestOptions);
          /**
           * We have to remove the request object because of the circular reference error.:
           * Starting Object: The circular structure starts with an object constructed using the ClientRequest constructor.
           * Circular Path:
           * This ClientRequest object has a property named socket which is an instance of TLSSocket.
           * The TLSSocket instance has a property _httpMessage that references back to the ClientRequest object.
           * Cycle: This creates a circular reference where ClientRequest references TLSSocket, which in turn references back to ClientRequest.
           */
          response.request = null;

          return response;
        } catch (error) {
          if (logger) {
            logger.error(`api/${methodName}`, error.message);
          }

          throw error;
        }
      };
    },
  });
};