From 7d00f3a7c23bb7fa7d846706b403b870d645e9e4 Mon Sep 17 00:00:00 2001 From: Joel Uong Date: Wed, 1 May 2024 10:59:51 -0400 Subject: [PATCH] add custom API helper --- src/static/helperTemplates/index.ts.hbs | 1 + src/static/helpers/customApi.ts | 149 ++++++++++++++++++++++++ templates/ClientInstance.ts.hbs | 1 + templates/index.ts.hbs | 5 +- templates/operations.ts.hbs | 10 +- 5 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 src/static/helpers/customApi.ts diff --git a/src/static/helperTemplates/index.ts.hbs b/src/static/helperTemplates/index.ts.hbs index 67b07730..e56c41d2 100644 --- a/src/static/helperTemplates/index.ts.hbs +++ b/src/static/helperTemplates/index.ts.hbs @@ -7,3 +7,4 @@ export * as slasHelpers from "./slas"; export * as helpers from "./shopperCustomer"; +export * as customApi from "./customApi"; diff --git a/src/static/helpers/customApi.ts b/src/static/helpers/customApi.ts new file mode 100644 index 00000000..46e73160 --- /dev/null +++ b/src/static/helpers/customApi.ts @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2022, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import { BodyInit, RequestInit } from "node-fetch"; +import { ClientConfig, Response, StaticClient } from "@commerce-apps/core"; +import { PathParameters } from "@commerce-apps/core/dist/base/resource"; +import type { OperationOptions } from "retry"; + +// TODO: move into config file +const CUSTOM_API_DEFAULT_BASE_URI = + "https://{shortCode}.api.commercecloud.salesforce.com/custom/{apiName}/{apiVersion}"; + +// Helper method to find Content Type header +// returns true if it exists, false otherwise +const contentTypeHeaderExists = ( + headers: Record | undefined +) => { + let foundHeader = false; + if (headers) { + foundHeader = Boolean( + Object.keys(headers).find((key) => key.toLowerCase() === "content-type") + ); + } + return foundHeader; +}; + +/* eslint-disable tsdoc/syntax */ +/** + * A helper function designed to make calls to a custom API endpoint + * For more information about custom APIs, please refer to the [API documentation](https://developer.salesforce.com/docs/commerce/commerce-api/guide/custom-apis.html?q=custom+API) + * @param options - An object containing any custom settings you want to apply to the request + * @param options.method? - The request HTTP operation. 'GET' is the default if no method is provided. + * @param options.parameters? - Query parameters that are added to the request + * @param options.headers? - Headers that are added to the request. Authorization header should be in this parameter or in the clientConfig.headers + * @param options.body? - Body that is used for the request + * @param clientConfig - Client Configuration object used by the SDK with properties that can affect the fetch call + * @param clientConfig.parameters - Path parameters used for custom API endpoints. The required properties are: apiName, endpointPath, organizationId, and shortCode. An error will be thrown if these are not provided. + * @param clientConfig.headers? - Additional headers that are added to the request. Authorization header should be in this argument or in the options?.headers. options?.headers will override any duplicate properties. + * @param clientConfig.fetchOptions? - fetchOptions that are passed onto the fetch request + * @param clientConfig.throwOnBadResponse? - flag that when set true will throw a response error if the fetch request fails + * @param rawResponse? - Flag to return the raw response from the fetch call. True for raw response object, false for the data from the response + * @returns Raw response or data from response based on rawResponse argument from fetch call + */ +export const callCustomEndpoint = async (args: { + options: { + method?: string; + parameters?: { + [key: string]: string | number | boolean | string[] | number[]; + }; + customApiPathParameters?: { + apiName?: string; + apiVersion?: string; + endpointPath?: string; + organizationId?: string; + shortCode?: string; + }; + headers?: { + authorization?: string; + } & { [key: string]: string }; + body?: BodyInit | unknown; + retrySettings?: OperationOptions; + fetchOptions?: RequestInit; + }; + clientConfig: ClientConfig; + rawResponse?: boolean; +}): Promise => { + const { options, clientConfig, rawResponse } = args; + + const requiredArgs = [ + "apiName", + "endpointPath", + "organizationId", + "shortCode", + ]; + + const pathParams: Record = { + ...clientConfig.parameters, + ...options?.customApiPathParameters, + }; + + requiredArgs.forEach((arg) => { + if (!pathParams[arg]) { + throw new Error( + `Missing required property needed in options.customApiPathParameters or clientConfig.parameters: ${arg}` + ); + } + }); + + if (!pathParams.apiVersion) { + pathParams.apiVersion = "v1"; + } + + let clientConfigCopy = clientConfig; + if (!clientConfig.baseUri) { + clientConfigCopy = { + ...clientConfig, + baseUri: CUSTOM_API_DEFAULT_BASE_URI, + }; + } + + // Use siteId from clientConfig if it is not defined in options and is available in clientConfig + const useSiteId = Boolean( + !options.parameters?.siteId && clientConfig.parameters?.siteId + ); + + const contentTypeExists = + contentTypeHeaderExists(options.headers) || + contentTypeHeaderExists(clientConfigCopy.headers); + + let optionsCopy = options; + + if (!contentTypeExists || useSiteId) { + optionsCopy = { + ...options, + headers: { + ...options.headers, + // If Content-Type header does not exist, we default to "Content-Type": "application/json" + ...(!contentTypeExists && { "Content-Type": "application/json" }), + }, + parameters: { + ...options.parameters, + ...(useSiteId && { siteId: clientConfig.parameters?.siteId as string }), + }, + }; + } + + const sdkOptions = { + client: { clientConfig: clientConfigCopy }, + path: "/organizations/{organizationId}/{endpointPath}", + pathParameters: pathParams as PathParameters, + queryParameters: optionsCopy.parameters, + headers: optionsCopy.headers, + rawResponse, + retrySettings: (optionsCopy || {})?.retrySettings, + fetchOptions: optionsCopy.fetchOptions, + body: optionsCopy?.body, + }; + + const operation = options.method?.toLowerCase() as + | "delete" + | "get" + | "patch" + | "post" + | "put"; + return StaticClient.runFetch(operation, sdkOptions); +}; diff --git a/templates/ClientInstance.ts.hbs b/templates/ClientInstance.ts.hbs index 0d0c3380..2a59730a 100644 --- a/templates/ClientInstance.ts.hbs +++ b/templates/ClientInstance.ts.hbs @@ -10,6 +10,7 @@ import { } from "@commerce-apps/core"; import type { OperationOptions } from "retry"; import type { RequestInit } from "node-fetch"; +import { PathParameters } from "@commerce-apps/core/dist/base/resource"; // eslint-disable-next-line @typescript-eslint/no-namespace export namespace {{name.upperCamelCase}} { diff --git a/templates/index.ts.hbs b/templates/index.ts.hbs index ebe7ea37..ce5dc2a8 100644 --- a/templates/index.ts.hbs +++ b/templates/index.ts.hbs @@ -7,11 +7,12 @@ export { {{/each}} } -import { slasHelpers, helpers } from "./helpers" +import { slasHelpers, helpers, customApi } from "./helpers" export { slasHelpers, - helpers + helpers, + customApi } export { ClientConfig, CommonParameters, sdkLogger } from "@commerce-apps/core" diff --git a/templates/operations.ts.hbs b/templates/operations.ts.hbs index d4da8e51..a122a547 100644 --- a/templates/operations.ts.hbs +++ b/templates/operations.ts.hbs @@ -132,7 +132,7 @@ {{#each ../parameters}} "{{{name}}}": optionParams["{{{name}}}"] {{! if it is a common path parameter, fallback to client config for undefined }} - {{#if (isCommonPathParameter name)}} !== undefined ? optionParams["{{{name}}}"] : configParams["{{{name}}}"]{{/if}} + {{#if (isCommonPathParameter name)}} !== undefined ? optionParams["{{{name}}}"] : configParams?.["{{{name}}}"]{{/if}} {{#unless @last}},{{/unless}} {{/each}} }; @@ -142,8 +142,8 @@ {{#each request.queryParameters}} if (optionParams["{{{name}}}"] !== undefined) { queryParams["{{{name}}}"] = optionParams["{{{name}}}"]; - } else if (configParams["{{{name}}}"] !== undefined) { - queryParams["{{{name}}}"] = configParams["{{{name}}}"]; + } else if (configParams?.["{{{name}}}"] !== undefined) { + queryParams["{{{name}}}"] = configParams?.["{{{name}}}"]; } {{#if (is required "true")}} else { @@ -171,12 +171,12 @@ client: this, rawResponse: rawResponse, path: "{{../path}}", - pathParameters: pathParams, + pathParameters: pathParams as PathParameters, queryParameters: queryParams, retrySettings: (options || {}).retrySettings, headers: headers{{#or (is method "patch") (is method "post") (is method "put")}}, body: (options || {}).body{{/or}}, - ...(options.fetchOptions && { fetchOptions: options.fetchOptions }) + ...(options?.fetchOptions && { fetchOptions: options.fetchOptions }) }); }