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 '.'
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
req,
res,
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
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
const searchResults = await searchClient.productSearch({
parameters: {
q: searchTerm,
limit: 20
}
});
let products = [];
let found = false;
parameters: {
q: searchTerm,
limit: 20,
},
})
let products = []
let found = false
if (searchResults.total) {
found = true;
products = normalizeSearchProducts(searchResults.hits) as any[];
found = true
products = normalizeSearchProducts(searchResults.hits) as any[]
}
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 getProduct from './operations/get-product'
export interface SFCCConfig extends CommerceAPIConfig {
sdk: Sdk
}
@ -22,8 +21,7 @@ const config: SFCCConfig = {
customerCookie: '',
cartCookieMaxAge: 2592000,
fetch: createFetcher(() => getCommerceApi().getConfig()),
sdk // SalesForce Cloud Commerce API SDK
sdk, // SalesForce Cloud Commerce API SDK
}
const operations = {
@ -39,7 +37,9 @@ const operations = {
export const provider = { config, operations }
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>(
customProvider: P = provider as any

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import { ClientConfig, Customer } from "commerce-sdk";
import { ClientConfig, Customer } from 'commerce-sdk'
// client configuration parameters
export const clientConfig: ClientConfig = {
headers: {
authorization: ``
authorization: ``,
},
parameters: {
clientId: process.env.SFCC_CLIENT_ID || '',
@ -11,7 +11,7 @@ export const clientConfig: ClientConfig = {
shortCode: process.env.SFCC_SHORT_CODE || '',
siteId: process.env.SFCC_SITE_ID || '',
},
};
}
/**
* 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
*/
export async function getGuestUserAuthToken(): Promise<Customer.ShopperLogin.TokenResponse> {
const credentials = `${process.env.SFCC_CLIENT_ID}:${process.env.SFCC_CLIENT_SECRET}`;
const base64data = Buffer.from(credentials).toString("base64");
const headers = { Authorization: `Basic ${base64data}` };
const client = new Customer.ShopperLogin(clientConfig);
const credentials = `${process.env.SFCC_CLIENT_ID}:${process.env.SFCC_CLIENT_SECRET}`
const base64data = Buffer.from(credentials).toString('base64')
const headers = { Authorization: `Basic ${base64data}` }
const client = new Customer.ShopperLogin(clientConfig)
return await client.getAccessToken({
headers,
body: {
grant_type: "client_credentials",
grant_type: 'client_credentials',
},
});
})
}
export const getConfigAuth = async () => {
const shopperToken = await getGuestUserAuthToken();
const shopperToken = await getGuestUserAuthToken()
const configAuth = {
...clientConfig,
headers: {"authorization":`Bearer ${shopperToken.access_token}`}
};
return configAuth;
headers: { authorization: `Bearer ${shopperToken.access_token}` },
}
return configAuth
}

View File

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

View File

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