diff --git a/framework/bigcommerce/api/endpoints/catalog/products/get-products.ts b/framework/bigcommerce/api/endpoints/catalog/products/get-products.ts index e0cc27912..8471767aa 100644 --- a/framework/bigcommerce/api/endpoints/catalog/products/get-products.ts +++ b/framework/bigcommerce/api/endpoints/catalog/products/get-products.ts @@ -1,6 +1,5 @@ import { Product } from '@commerce/types/product' import { ProductsEndpoint } from '.' -import getAllProducts from '../../../../product/get-all-products' const SORT: { [key: string]: string | undefined } = { latest: 'id', @@ -15,6 +14,7 @@ const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({ res, body: { search, category, brand, sort }, config, + commerce, }) => { // Use a dummy base as we only care about the relative path const url = new URL('/v3/catalog/products', 'http://a') @@ -47,18 +47,18 @@ const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({ url.pathname + url.search ) - const entityIds = data.map((p) => p.id) - const found = entityIds.length > 0 + const ids = data.map((p) => String(p.id)) + const found = ids.length > 0 // We want the GraphQL version of each product - const graphqlData = await getAllProducts({ - variables: { first: LIMIT, entityIds }, + const graphqlData = await commerce.getAllProducts({ + variables: { first: LIMIT, ids }, config, }) // Put the products in an object that we can use to get them by id const productsById = graphqlData.products.reduce<{ - [k: number]: Product + [k: string]: Product }>((prods, p) => { prods[Number(p.id)] = p return prods @@ -68,7 +68,7 @@ const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({ // Populate the products array with the graphql products, in the order // assigned by the list of entity ids - entityIds.forEach((id) => { + ids.forEach((id) => { const product = productsById[id] if (product) products.push(product) }) diff --git a/framework/bigcommerce/api/index.ts b/framework/bigcommerce/api/index.ts index 542439cbb..c81107f5e 100644 --- a/framework/bigcommerce/api/index.ts +++ b/framework/bigcommerce/api/index.ts @@ -22,6 +22,7 @@ import getPage from './operations/get-page' import getSiteInfo from './operations/get-site-info' import getCustomerWishlist from './operations/get-customer-wishlist' import getAllProductPaths from './operations/get-all-product-paths' +import getAllProducts from './operations/get-all-products' export interface BigcommerceConfig extends CommerceAPIConfig { // Indicates if the returned metadata with translations should be applied to the @@ -124,6 +125,7 @@ export const provider = { getSiteInfo, getCustomerWishlist, getAllProductPaths, + getAllProducts, }, } diff --git a/framework/bigcommerce/api/operations/get-all-products.ts b/framework/bigcommerce/api/operations/get-all-products.ts new file mode 100644 index 000000000..ef9696c73 --- /dev/null +++ b/framework/bigcommerce/api/operations/get-all-products.ts @@ -0,0 +1,160 @@ +import type { + OperationContext, + OperationOptions, +} from '@commerce/api/operations' +import type { + GetAllProductsQuery, + GetAllProductsQueryVariables, +} from '../../schema' +import type { GetAllProductsOperation } from '../../types/product' +import type { RecursivePartial, RecursiveRequired } from '../utils/types' +import filterEdges from '../utils/filter-edges' +import setProductLocaleMeta from '../utils/set-product-locale-meta' +import { productConnectionFragment } from '../fragments/product' +import { BigcommerceConfig, Provider } from '..' +import { normalizeProduct } from '../../lib/normalize' + +export const getAllProductsQuery = /* GraphQL */ ` + query getAllProducts( + $hasLocale: Boolean = false + $locale: String = "null" + $entityIds: [Int!] + $first: Int = 10 + $products: Boolean = false + $featuredProducts: Boolean = false + $bestSellingProducts: Boolean = false + $newestProducts: Boolean = false + ) { + site { + products(first: $first, entityIds: $entityIds) @include(if: $products) { + ...productConnnection + } + featuredProducts(first: $first) @include(if: $featuredProducts) { + ...productConnnection + } + bestSellingProducts(first: $first) @include(if: $bestSellingProducts) { + ...productConnnection + } + newestProducts(first: $first) @include(if: $newestProducts) { + ...productConnnection + } + } + } + + ${productConnectionFragment} +` + +export type ProductEdge = NonNullable< + NonNullable[0] +> + +export type ProductNode = ProductEdge['node'] + +export type GetAllProductsResult< + T extends Record = { + products: ProductEdge[] + } +> = T + +const FIELDS = [ + 'products', + 'featuredProducts', + 'bestSellingProducts', + 'newestProducts', +] + +export type ProductTypes = + | 'products' + | 'featuredProducts' + | 'bestSellingProducts' + | 'newestProducts' + +export type ProductVariables = { field?: ProductTypes } & Omit< + GetAllProductsQueryVariables, + ProductTypes | 'hasLocale' +> + +function getProductsType( + relevance?: GetAllProductsOperation['variables']['relevance'] +): ProductTypes { + switch (relevance) { + case 'featured': + return 'featuredProducts' + case 'best_selling': + return 'bestSellingProducts' + case 'newest': + return 'newestProducts' + default: + return 'products' + } +} + +export default function getAllProductsOperation({ + commerce, +}: OperationContext) { + async function getAllProducts(opts?: { + variables?: T['variables'] + config?: BigcommerceConfig + preview?: boolean + }): Promise + + async function getAllProducts( + opts: { + variables?: T['variables'] + config?: BigcommerceConfig + preview?: boolean + } & OperationOptions + ): Promise + + async function getAllProducts({ + query = getAllProductsQuery, + variables: vars = {}, + config, + }: { + query?: string + variables?: T['variables'] + config?: BigcommerceConfig + preview?: boolean + } = {}): Promise { + config = commerce.getConfig(config) + + const locale = config.locale + const field = getProductsType(vars.relevance) + const variables: GetAllProductsQueryVariables = { + locale, + hasLocale: !!locale, + } + + if (!FIELDS.includes(field)) { + throw new Error( + `The field variable has to match one of ${FIELDS.join(', ')}` + ) + } + + variables[field] = true + + if (vars.first) variables.first = vars.first + if (vars.ids) variables.entityIds = vars.ids.map((id) => Number(id)) + + // RecursivePartial forces the method to check for every prop in the data, which is + // required in case there's a custom `query` + const { data } = await config.fetch>( + query, + { variables } + ) + const edges = data.site?.[field]?.edges + const products = filterEdges(edges as RecursiveRequired) + + if (locale && config.applyLocale) { + products.forEach((product: RecursivePartial) => { + if (product.node) setProductLocaleMeta(product.node) + }) + } + + return { + products: products.map(({ node }) => normalizeProduct(node as any)), + } + } + + return getAllProducts +} diff --git a/framework/bigcommerce/api/operations/get-customer-wishlist.ts b/framework/bigcommerce/api/operations/get-customer-wishlist.ts index f4036ee51..fc9487ffe 100644 --- a/framework/bigcommerce/api/operations/get-customer-wishlist.ts +++ b/framework/bigcommerce/api/operations/get-customer-wishlist.ts @@ -8,7 +8,7 @@ import type { } from '../../types/wishlist' import type { RecursivePartial, RecursiveRequired } from '../utils/types' import { BigcommerceConfig, Provider } from '..' -import getAllProducts, { ProductEdge } from '../../product/get-all-products' +import getAllProducts, { ProductEdge } from './get-all-products' export default function getCustomerWishlistOperation({ commerce, @@ -47,13 +47,13 @@ export default function getCustomerWishlistOperation({ const wishlist = data[0] if (includeProducts && wishlist?.items?.length) { - const entityIds = wishlist.items - ?.map((item) => item?.product_id) - .filter((id): id is number => !!id) + const ids = wishlist.items + ?.map((item) => (item?.product_id ? String(item?.product_id) : null)) + .filter((id): id is string => !!id) - if (entityIds?.length) { - const graphqlData = await getAllProducts({ - variables: { first: 100, entityIds }, + if (ids?.length) { + const graphqlData = await commerce.getAllProducts({ + variables: { first: 100, ids }, config, }) // Put the products in an object that we can use to get them by id diff --git a/framework/bigcommerce/api/utils/set-product-locale-meta.ts b/framework/bigcommerce/api/utils/set-product-locale-meta.ts index 974a197bd..767286477 100644 --- a/framework/bigcommerce/api/utils/set-product-locale-meta.ts +++ b/framework/bigcommerce/api/utils/set-product-locale-meta.ts @@ -1,4 +1,4 @@ -import type { ProductNode } from '../../product/get-all-products' +import type { ProductNode } from '../operations/get-all-products' import type { RecursivePartial } from './types' export default function setProductLocaleMeta( diff --git a/framework/bigcommerce/product/get-all-products.ts b/framework/bigcommerce/product/get-all-products.ts deleted file mode 100644 index 4c563bc62..000000000 --- a/framework/bigcommerce/product/get-all-products.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { - GetAllProductsQuery, - GetAllProductsQueryVariables, -} from '../schema' -import type { Product } from '@commerce/types' -import type { RecursivePartial, RecursiveRequired } from '../api/utils/types' -import filterEdges from '../api/utils/filter-edges' -import setProductLocaleMeta from '../api/utils/set-product-locale-meta' -import { productConnectionFragment } from '../api/fragments/product' -import { BigcommerceConfig, getConfig } from '../api' -import { normalizeProduct } from '../lib/normalize' - -export const getAllProductsQuery = /* GraphQL */ ` - query getAllProducts( - $hasLocale: Boolean = false - $locale: String = "null" - $entityIds: [Int!] - $first: Int = 10 - $products: Boolean = false - $featuredProducts: Boolean = false - $bestSellingProducts: Boolean = false - $newestProducts: Boolean = false - ) { - site { - products(first: $first, entityIds: $entityIds) @include(if: $products) { - ...productConnnection - } - featuredProducts(first: $first) @include(if: $featuredProducts) { - ...productConnnection - } - bestSellingProducts(first: $first) @include(if: $bestSellingProducts) { - ...productConnnection - } - newestProducts(first: $first) @include(if: $newestProducts) { - ...productConnnection - } - } - } - - ${productConnectionFragment} -` - -export type ProductEdge = NonNullable< - NonNullable[0] -> - -export type ProductNode = ProductEdge['node'] - -export type GetAllProductsResult< - T extends Record = { - products: ProductEdge[] - } -> = T - -const FIELDS = [ - 'products', - 'featuredProducts', - 'bestSellingProducts', - 'newestProducts', -] - -export type ProductTypes = - | 'products' - | 'featuredProducts' - | 'bestSellingProducts' - | 'newestProducts' - -export type ProductVariables = { field?: ProductTypes } & Omit< - GetAllProductsQueryVariables, - ProductTypes | 'hasLocale' -> - -async function getAllProducts(opts?: { - variables?: ProductVariables - config?: BigcommerceConfig - preview?: boolean -}): Promise<{ products: Product[] }> - -async function getAllProducts< - T extends Record, - V = any ->(opts: { - query: string - variables?: V - config?: BigcommerceConfig - preview?: boolean -}): Promise> - -async function getAllProducts({ - query = getAllProductsQuery, - variables: { field = 'products', ...vars } = {}, - config, -}: { - query?: string - variables?: ProductVariables - config?: BigcommerceConfig - preview?: boolean - // TODO: fix the product type here -} = {}): Promise<{ products: Product[] | any[] }> { - config = getConfig(config) - - const locale = vars.locale || config.locale - const variables: GetAllProductsQueryVariables = { - ...vars, - locale, - hasLocale: !!locale, - } - - if (!FIELDS.includes(field)) { - throw new Error( - `The field variable has to match one of ${FIELDS.join(', ')}` - ) - } - - variables[field] = true - - // RecursivePartial forces the method to check for every prop in the data, which is - // required in case there's a custom `query` - const { data } = await config.fetch>( - query, - { variables } - ) - const edges = data.site?.[field]?.edges - const products = filterEdges(edges as RecursiveRequired) - - if (locale && config.applyLocale) { - products.forEach((product: RecursivePartial) => { - if (product.node) setProductLocaleMeta(product.node) - }) - } - - return { products: products.map(({ node }) => normalizeProduct(node as any)) } -} - -export default getAllProducts diff --git a/framework/bigcommerce/product/index.ts b/framework/bigcommerce/product/index.ts index b290c189f..d5b1cefac 100644 --- a/framework/bigcommerce/product/index.ts +++ b/framework/bigcommerce/product/index.ts @@ -1,4 +1,4 @@ export { default as usePrice } from './use-price' export { default as useSearch } from './use-search' export { default as getProduct } from './get-product' -export { default as getAllProducts } from './get-all-products' +export { default as getAllProducts } from '../api/operations/get-all-products' diff --git a/framework/bigcommerce/types/wishlist.ts b/framework/bigcommerce/types/wishlist.ts index 510299eac..1e148b88c 100644 --- a/framework/bigcommerce/types/wishlist.ts +++ b/framework/bigcommerce/types/wishlist.ts @@ -1,6 +1,6 @@ import * as Core from '@commerce/types/wishlist' import { definitions } from '../api/definitions/wishlist' -import type { ProductEdge } from '../product/get-all-products' +import type { ProductEdge } from '../api/operations/get-all-products' export * from '@commerce/types/wishlist' diff --git a/framework/commerce/api/operations.ts b/framework/commerce/api/operations.ts index eebff74aa..c11ebf965 100644 --- a/framework/commerce/api/operations.ts +++ b/framework/commerce/api/operations.ts @@ -3,7 +3,10 @@ import type { LoginOperation } from '../types/login' import type { GetAllPagesOperation, GetPageOperation } from '../types/page' import type { GetSiteInfoOperation } from '../types/site' import type { GetCustomerWishlistOperation } from '../types/wishlist' -import type { GetAllProductPathsOperation } from '../types/product' +import type { + GetAllProductPathsOperation, + GetAllProductsOperation, +} from '../types/product' import type { APIProvider, CommerceAPI } from '.' const noop = () => { @@ -109,6 +112,22 @@ export type Operations

= { } & OperationOptions ): Promise } + + getAllProducts: { + (opts: { + variables?: T['variables'] + config?: P['config'] + preview?: boolean + }): Promise + + ( + opts: { + variables?: T['variables'] + config?: P['config'] + preview?: boolean + } & OperationOptions + ): Promise + } } export type APIOperations

= { diff --git a/framework/commerce/types/product.ts b/framework/commerce/types/product.ts index 22a045f48..ed7e0e85b 100644 --- a/framework/commerce/types/product.ts +++ b/framework/commerce/types/product.ts @@ -72,3 +72,12 @@ export type GetAllProductPathsOperation< data: { products: Pick[] } variables: { first?: number } } + +export type GetAllProductsOperation = { + data: { products: T['product'][] } + variables: { + relevance?: 'featured' | 'best_selling' | 'newest' + ids?: string[] + first?: number + } +} diff --git a/pages/index.tsx b/pages/index.tsx index d74a649fb..b33ed585b 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -6,7 +6,6 @@ import { ProductCard } from '@components/product' import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next' import { getConfig } from '@framework/api' -import getAllProducts from '@framework/product/get-all-products' export async function getStaticProps({ preview, @@ -14,7 +13,7 @@ export async function getStaticProps({ }: GetStaticPropsContext) { const config = getConfig({ locale }) - const { products } = await getAllProducts({ + const { products } = await commerce.getAllProducts({ variables: { first: 12 }, config, preview,