This commit is contained in:
Dom Sip 2022-04-19 18:50:09 +01:00
parent 02b477bdea
commit 3afc174b15
8 changed files with 153 additions and 144 deletions

View File

@ -1,33 +1,31 @@
import { normalizeSearchProducts } from '../../../utils/normalise-product'; import { normalizeSearchProducts } from '../../../utils/normalise-product'
import { ProductsEndpoint } from '.' import { ProductsEndpoint } from '.'
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({ const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
req, req,
res, res,
body: { search, categoryId, brandId, sort }, body: { search, categoryId, brandId, sort },
config config,
}) => { }) => {
const { sdk } = config; const { sdk } = config
// 'clothing' is our main category default, and a manually set category has priority // 'clothing' is our main category default, and a manually set category has priority
const searchTerm = categoryId ? categoryId as string : search || 'clothing'; const searchTerm = categoryId ? (categoryId as string) : search || 'clothing'
const searchClient = await sdk.getSearchClient(); const searchClient = await sdk.getSearchClient()
// use SDK search API for initial products // use SDK search API for initial products
const searchResults = await searchClient.productSearch({ const searchResults = await searchClient.productSearch({
parameters: { parameters: {
q: searchTerm, q: searchTerm,
limit: 20 limit: 20,
} },
}); })
let products = []; let products = []
let found = false; let found = false
if (searchResults.total) { if (searchResults.total) {
found = true; found = true
products = normalizeSearchProducts(searchResults.hits) as any[]; products = normalizeSearchProducts(searchResults.hits) as any[]
} }
res.status(200).json({ data: { products, found } }) res.status(200).json({ data: { products, found } })
} }

View File

@ -11,7 +11,6 @@ import getAllProductPaths from './operations/get-all-product-paths'
import getAllProducts from './operations/get-all-products' import getAllProducts from './operations/get-all-products'
import getProduct from './operations/get-product' import getProduct from './operations/get-product'
export interface SFCCConfig extends CommerceAPIConfig { export interface SFCCConfig extends CommerceAPIConfig {
sdk: Sdk sdk: Sdk
} }
@ -22,8 +21,7 @@ const config: SFCCConfig = {
customerCookie: '', customerCookie: '',
cartCookieMaxAge: 2592000, cartCookieMaxAge: 2592000,
fetch: createFetcher(() => getCommerceApi().getConfig()), fetch: createFetcher(() => getCommerceApi().getConfig()),
sdk // SalesForce Cloud Commerce API SDK sdk, // SalesForce Cloud Commerce API SDK
} }
const operations = { const operations = {
@ -39,7 +37,9 @@ const operations = {
export const provider = { config, operations } export const provider = { config, operations }
export type Provider = typeof provider export type Provider = typeof provider
export type SFCCProviderAPI<P extends Provider = Provider> = CommerceAPI<P | any> export type SFCCProviderAPI<P extends Provider = Provider> = CommerceAPI<
P | any
>
export function getCommerceApi<P extends Provider>( export function getCommerceApi<P extends Provider>(
customProvider: P = provider as any customProvider: P = provider as any

View File

@ -1,13 +1,15 @@
import { Product } from '@vercel/commerce/types/product' import { Product } from '@vercel/commerce/types/product'
import { OperationContext } from '@vercel/commerce/api/operations' import { OperationContext } from '@vercel/commerce/api/operations'
import { normalizeSearchProducts } from '../utils/normalise-product'; import { normalizeSearchProducts } from '../utils/normalise-product'
import { SFCCConfig } from '..' import { SFCCConfig } from '..'
export type GetAllProductPathsResult = { export type GetAllProductPathsResult = {
products: Array<{ path: string }> products: Array<{ path: string }>
} }
export default function getAllProductPathsOperation({ commerce }: OperationContext<any>) { export default function getAllProductPathsOperation({
commerce,
}: OperationContext<any>) {
async function getAllProductPaths({ async function getAllProductPaths({
query, query,
config, config,
@ -17,23 +19,22 @@ export default function getAllProductPathsOperation({ commerce }: OperationConte
config?: SFCCConfig config?: SFCCConfig
variables?: any variables?: any
} = {}): Promise<GetAllProductPathsResult> { } = {}): Promise<GetAllProductPathsResult> {
// TODO: support locale // TODO: support locale
const { sdk, locale } = commerce.getConfig(config) as SFCCConfig const { sdk, locale } = commerce.getConfig(config) as SFCCConfig
const searchClient = await sdk.getSearchClient() const searchClient = await sdk.getSearchClient()
// use SDK search API for initial products same as getAllProductsOperation // use SDK search API for initial products same as getAllProductsOperation
const searchResults = await searchClient.productSearch({ const searchResults = await searchClient.productSearch({
parameters: { q: "dress", limit: variables?.first }, parameters: { q: 'dress', limit: variables?.first },
}); })
let products = [] as Product[]; let products = [] as Product[]
if (searchResults.total) { if (searchResults.total) {
products = normalizeSearchProducts(searchResults.hits) products = normalizeSearchProducts(searchResults.hits)
} else { } else {
// TODO: handle this better? // TODO: handle this better?
console.log("No results for search"); console.log('No results for search')
} }
return { return {

View File

@ -2,7 +2,7 @@ import { Product } from '@vercel/commerce/types/product'
import { GetAllProductsOperation } from '@vercel/commerce/types/product' import { GetAllProductsOperation } from '@vercel/commerce/types/product'
import type { OperationContext } from '@vercel/commerce/api/operations' import type { OperationContext } from '@vercel/commerce/api/operations'
import type { SFCCConfig } from '../index' import type { SFCCConfig } from '../index'
import { normalizeSearchProducts } from '../utils/normalise-product'; import { normalizeSearchProducts } from '../utils/normalise-product'
export default function getAllProductsOperation({ export default function getAllProductsOperation({
commerce, commerce,
@ -17,26 +17,25 @@ export default function getAllProductsOperation({
config?: Partial<SFCCConfig> config?: Partial<SFCCConfig>
preview?: boolean preview?: boolean
} = {}): Promise<{ products: Product[] | any[] }> { } = {}): Promise<{ products: Product[] | any[] }> {
// TODO: support locale // TODO: support locale
const { sdk, locale } = commerce.getConfig(config) as SFCCConfig const { sdk, locale } = commerce.getConfig(config) as SFCCConfig
const searchClient = await sdk.getSearchClient() const searchClient = await sdk.getSearchClient()
// use SDK search API for initial products // use SDK search API for initial products
const searchResults = await searchClient.productSearch({ const searchResults = await searchClient.productSearch({
parameters: { q: "dress", limit: variables?.first }, parameters: { q: 'dress', limit: variables?.first },
}); })
let products = [] as Product[]; let products = [] as Product[]
if (searchResults.total) { if (searchResults.total) {
products = normalizeSearchProducts(searchResults.hits) products = normalizeSearchProducts(searchResults.hits)
} else { } else {
// TODO: handle this better? // TODO: handle this better?
console.log("No results for search"); console.log('No results for search')
} }
return { return {
products: products products: products,
} }
} }
return getAllProducts return getAllProducts

View File

@ -1,8 +1,7 @@
import { GetProductOperation, Product } from '@vercel/commerce/types/product' import { GetProductOperation, Product } from '@vercel/commerce/types/product'
import type { SFCCConfig } from '../index' import type { SFCCConfig } from '../index'
import type { OperationContext } from '@vercel/commerce/api/operations' import type { OperationContext } from '@vercel/commerce/api/operations'
import { normalizeProduct } from '../utils/normalise-product'; import { normalizeProduct } from '../utils/normalise-product'
export default function getProductOperation({ export default function getProductOperation({
commerce, commerce,
@ -17,11 +16,12 @@ export default function getProductOperation({
config?: Partial<SFCCConfig> config?: Partial<SFCCConfig>
preview?: boolean preview?: boolean
} = {}): Promise<Product | {} | any> { } = {}): Promise<Product | {} | any> {
// TODO: support locale // TODO: support locale
const { sdk, locale } = commerce.getConfig(config) as SFCCConfig const { sdk, locale } = commerce.getConfig(config) as SFCCConfig
const shopperProductsClient = await sdk.getshopperProductsClient() const shopperProductsClient = await sdk.getshopperProductsClient()
const product = await shopperProductsClient.getProduct({parameters: {id: variables?.slug as string}}); const product = await shopperProductsClient.getProduct({
parameters: { id: variables?.slug as string },
})
const normalizedProduct = normalizeProduct(product) const normalizedProduct = normalizeProduct(product)
return { return {

View File

@ -1,9 +1,9 @@
import { ClientConfig, Customer } from "commerce-sdk"; import { ClientConfig, Customer } from 'commerce-sdk'
// client configuration parameters // client configuration parameters
export const clientConfig: ClientConfig = { export const clientConfig: ClientConfig = {
headers: { headers: {
authorization: `` authorization: ``,
}, },
parameters: { parameters: {
clientId: process.env.SFCC_CLIENT_ID || '', clientId: process.env.SFCC_CLIENT_ID || '',
@ -11,7 +11,7 @@ export const clientConfig: ClientConfig = {
shortCode: process.env.SFCC_SHORT_CODE || '', shortCode: process.env.SFCC_SHORT_CODE || '',
siteId: process.env.SFCC_SITE_ID || '', siteId: process.env.SFCC_SITE_ID || '',
}, },
}; }
/** /**
* Get the shopper or guest JWT/access token, along with a refresh token, using client credentials * Get the shopper or guest JWT/access token, along with a refresh token, using client credentials
@ -19,25 +19,24 @@ export const clientConfig: ClientConfig = {
* @returns guest user authorization token * @returns guest user authorization token
*/ */
export async function getGuestUserAuthToken(): Promise<Customer.ShopperLogin.TokenResponse> { export async function getGuestUserAuthToken(): Promise<Customer.ShopperLogin.TokenResponse> {
const credentials = `${process.env.SFCC_CLIENT_ID}:${process.env.SFCC_CLIENT_SECRET}`; const credentials = `${process.env.SFCC_CLIENT_ID}:${process.env.SFCC_CLIENT_SECRET}`
const base64data = Buffer.from(credentials).toString("base64"); const base64data = Buffer.from(credentials).toString('base64')
const headers = { Authorization: `Basic ${base64data}` }; const headers = { Authorization: `Basic ${base64data}` }
const client = new Customer.ShopperLogin(clientConfig); const client = new Customer.ShopperLogin(clientConfig)
return await client.getAccessToken({ return await client.getAccessToken({
headers, headers,
body: { body: {
grant_type: "client_credentials", grant_type: 'client_credentials',
}, },
}); })
} }
export const getConfigAuth = async () => { export const getConfigAuth = async () => {
const shopperToken = await getGuestUserAuthToken(); const shopperToken = await getGuestUserAuthToken()
const configAuth = { const configAuth = {
...clientConfig, ...clientConfig,
headers: {"authorization":`Bearer ${shopperToken.access_token}`} headers: { authorization: `Bearer ${shopperToken.access_token}` },
}; }
return configAuth; return configAuth
} }

View File

@ -1,46 +1,58 @@
import { Product as SFCCProduct, Search } from "commerce-sdk"; import { Product as SFCCProduct, Search } from 'commerce-sdk'
import type { Product, ProductImage, ProductOption, ProductVariant } from '@vercel/commerce/types/product' import type {
Product,
ProductImage,
ProductOption,
ProductVariant,
} from '@vercel/commerce/types/product'
const normaliseOptions = (options: SFCCProduct.ShopperProducts.Product["variationAttributes"]): Product["options"] => { const normaliseOptions = (
options: SFCCProduct.ShopperProducts.Product['variationAttributes']
): Product['options'] => {
if (!Array.isArray(options)) return [] if (!Array.isArray(options)) return []
return options.map(option => { return options.map((option) => {
return { return {
id: option.id, id: option.id,
displayName: option.name as string, displayName: option.name as string,
values: option.values!.map(value => ({label: value.name})) values: option.values!.map((value) => ({ label: value.name })),
} as ProductOption } as ProductOption
}); })
} }
const normaliseVariants = (variants: SFCCProduct.ShopperProducts.Product["variants"]): Product["variants"] => { const normaliseVariants = (
variants: SFCCProduct.ShopperProducts.Product['variants']
): Product['variants'] => {
if (!Array.isArray(variants)) return [] if (!Array.isArray(variants)) return []
return variants.map(variant => { return variants.map((variant) => {
const options = [] as ProductOption[]
const options = [] as ProductOption[];
if (variant.variationValues) { if (variant.variationValues) {
for (const [key, value] of Object.entries(variant.variationValues)) { for (const [key, value] of Object.entries(variant.variationValues)) {
const variantOptionObject = { const variantOptionObject = {
id: `${variant.productId}-${key}`, id: `${variant.productId}-${key}`,
displayName: key, displayName: key,
values: [{ values: [
{
label: value, label: value,
}] },
],
} }
options.push(variantOptionObject); options.push(variantOptionObject)
} }
} }
return { return {
id: variant.productId, id: variant.productId,
options options,
} as ProductVariant; } as ProductVariant
}); })
} }
export function normalizeProduct(product: SFCCProduct.ShopperProducts.Product): Product { export function normalizeProduct(
product: SFCCProduct.ShopperProducts.Product
): Product {
return { return {
id: product.id, id: product.id,
// TODO: use `name-ID` as a virtual slug (for search 1:1) // TODO: use `name-ID` as a virtual slug (for search 1:1)
@ -49,36 +61,36 @@ export function normalizeProduct(product: SFCCProduct.ShopperProducts.Product):
description: product.longDescription!, description: product.longDescription!,
price: { price: {
value: product.price!, value: product.price!,
currencyCode: product.currency currencyCode: product.currency,
}, },
images: product.imageGroups![0].images.map(image => ({ images: product.imageGroups![0].images.map((image) => ({
url: image.disBaseLink, url: image.disBaseLink,
altText: image.title altText: image.title,
})) as ProductImage[], })) as ProductImage[],
variants: normaliseVariants(product.variants), variants: normaliseVariants(product.variants),
options: normaliseOptions(product.variationAttributes), options: normaliseOptions(product.variationAttributes),
}; }
} }
export function normalizeSearchProducts(products: Search.ShopperSearch.ProductSearchHit[]): Product[] { export function normalizeSearchProducts(
products: Search.ShopperSearch.ProductSearchHit[]
return products.map(product => ({ ): Product[] {
return products.map((product) => ({
id: product.productId, id: product.productId,
slug: product.productId, // use product ID as a slug slug: product.productId, // use product ID as a slug
name: product.productName!, name: product.productName!,
description: '', description: '',
price: { price: {
value: product.price!, value: product.price!,
currencyCode: product.currency currencyCode: product.currency,
}, },
images: [ images: [
{ {
url: product.image!.link, url: product.image!.link,
altText: product.productName altText: product.productName,
} as ProductImage } as ProductImage,
], ],
variants: normaliseVariants(product.variants), variants: normaliseVariants(product.variants),
options: normaliseOptions(product.variationAttributes), options: normaliseOptions(product.variationAttributes),
})); }))
} }

View File

@ -1,19 +1,19 @@
import { Product, Search } from "commerce-sdk"; import { Product, Search } from 'commerce-sdk'
import { getConfigAuth } from "./get-auth-token"; import { getConfigAuth } from './get-auth-token'
const getSearchClient = async () => { const getSearchClient = async () => {
const configAuth = await getConfigAuth(); const configAuth = await getConfigAuth()
return new Search.ShopperSearch(configAuth); return new Search.ShopperSearch(configAuth)
} }
const getshopperProductsClient = async () => { const getshopperProductsClient = async () => {
const configAuth = await getConfigAuth(); const configAuth = await getConfigAuth()
return new Product.ShopperProducts(configAuth) return new Product.ShopperProducts(configAuth)
} }
export const sdk = { export const sdk = {
getshopperProductsClient, getshopperProductsClient,
getSearchClient getSearchClient,
} }
export type Sdk = typeof sdk export type Sdk = typeof sdk
export default sdk export default sdk