Moved getAllProducts

This commit is contained in:
Luis Alvarez 2021-05-24 17:22:46 -05:00
parent c2a5904e01
commit be5d47bac3
11 changed files with 209 additions and 155 deletions

View File

@ -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)
})

View File

@ -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,
},
}

View File

@ -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<GetAllProductsQuery['site']['products']['edges']>[0]
>
export type ProductNode = ProductEdge['node']
export type GetAllProductsResult<
T extends Record<keyof GetAllProductsResult, any[]> = {
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<Provider>) {
async function getAllProducts<T extends GetAllProductsOperation>(opts?: {
variables?: T['variables']
config?: BigcommerceConfig
preview?: boolean
}): Promise<T['data']>
async function getAllProducts<T extends GetAllProductsOperation>(
opts: {
variables?: T['variables']
config?: BigcommerceConfig
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getAllProducts<T extends GetAllProductsOperation>({
query = getAllProductsQuery,
variables: vars = {},
config,
}: {
query?: string
variables?: T['variables']
config?: BigcommerceConfig
preview?: boolean
} = {}): Promise<T['data']> {
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<RecursivePartial<GetAllProductsQuery>>(
query,
{ variables }
)
const edges = data.site?.[field]?.edges
const products = filterEdges(edges as RecursiveRequired<typeof edges>)
if (locale && config.applyLocale) {
products.forEach((product: RecursivePartial<ProductEdge>) => {
if (product.node) setProductLocaleMeta(product.node)
})
}
return {
products: products.map(({ node }) => normalizeProduct(node as any)),
}
}
return getAllProducts
}

View File

@ -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

View File

@ -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(

View File

@ -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<GetAllProductsQuery['site']['products']['edges']>[0]
>
export type ProductNode = ProductEdge['node']
export type GetAllProductsResult<
T extends Record<keyof GetAllProductsResult, any[]> = {
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<keyof GetAllProductsResult, any[]>,
V = any
>(opts: {
query: string
variables?: V
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetAllProductsResult<T>>
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<RecursivePartial<GetAllProductsQuery>>(
query,
{ variables }
)
const edges = data.site?.[field]?.edges
const products = filterEdges(edges as RecursiveRequired<typeof edges>)
if (locale && config.applyLocale) {
products.forEach((product: RecursivePartial<ProductEdge>) => {
if (product.node) setProductLocaleMeta(product.node)
})
}
return { products: products.map(({ node }) => normalizeProduct(node as any)) }
}
export default getAllProducts

View File

@ -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'

View File

@ -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'

View File

@ -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<P extends APIProvider> = {
} & OperationOptions
): Promise<T['data']>
}
getAllProducts: {
<T extends GetAllProductsOperation>(opts: {
variables?: T['variables']
config?: P['config']
preview?: boolean
}): Promise<T['data']>
<T extends GetAllProductsOperation>(
opts: {
variables?: T['variables']
config?: P['config']
preview?: boolean
} & OperationOptions
): Promise<T['data']>
}
}
export type APIOperations<P extends APIProvider> = {

View File

@ -72,3 +72,12 @@ export type GetAllProductPathsOperation<
data: { products: Pick<T['product'], 'path'>[] }
variables: { first?: number }
}
export type GetAllProductsOperation<T extends ProductTypes = ProductTypes> = {
data: { products: T['product'][] }
variables: {
relevance?: 'featured' | 'best_selling' | 'newest'
ids?: string[]
first?: number
}
}

View File

@ -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,