mirror of
https://github.com/vercel/commerce.git
synced 2025-07-03 19:51:22 +00:00
Fetch single product during build time
This commit is contained in:
parent
24e635b048
commit
a7a75e7f69
@ -6,6 +6,8 @@ export type GetAllProductPathsResult = {
|
|||||||
|
|
||||||
export default function getAllProductPathsOperation() {
|
export default function getAllProductPathsOperation() {
|
||||||
function getAllProductPaths(): Promise<GetAllProductPathsResult> {
|
function getAllProductPaths(): Promise<GetAllProductPathsResult> {
|
||||||
|
console.log('getAllProductPaths called.')
|
||||||
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
// products: data.products.map(({ path }) => ({ path })),
|
// products: data.products.map(({ path }) => ({ path })),
|
||||||
// TODO: Return Storefront [{ path: '/long-sleeve-shirt' }, ...] from Spree products. Paths using product IDs are fine too.
|
// TODO: Return Storefront [{ path: '/long-sleeve-shirt' }, ...] from Spree products. Paths using product IDs are fine too.
|
||||||
|
@ -1,33 +1,38 @@
|
|||||||
import type {
|
import type { Product } from '@commerce/types/product'
|
||||||
Product,
|
|
||||||
ProductOption,
|
|
||||||
ProductOptionValues,
|
|
||||||
ProductPrice,
|
|
||||||
ProductVariant,
|
|
||||||
} from '@commerce/types/product'
|
|
||||||
import type { GetAllProductsOperation } from '@commerce/types/product'
|
import type { GetAllProductsOperation } from '@commerce/types/product'
|
||||||
import type { OperationContext } from '@commerce/api/operations'
|
import type {
|
||||||
|
OperationContext,
|
||||||
|
OperationOptions,
|
||||||
|
} from '@commerce/api/operations'
|
||||||
import type { IProducts } from '@spree/storefront-api-v2-sdk/types/interfaces/Product'
|
import type { IProducts } from '@spree/storefront-api-v2-sdk/types/interfaces/Product'
|
||||||
import { RelationType } from '@spree/storefront-api-v2-sdk/types/interfaces/Relationships'
|
|
||||||
import type { SpreeApiConfig, SpreeApiProvider } from '../index'
|
import type { SpreeApiConfig, SpreeApiProvider } from '../index'
|
||||||
import type { SpreeSdkVariables } from 'framework/spree/types'
|
import type { SpreeSdkVariables } from 'framework/spree/types'
|
||||||
import { findIncluded, findIncludedOfType } from 'framework/spree/utils/jsonApi'
|
import normalizeProduct from 'framework/spree/utils/normalizeProduct'
|
||||||
import getMediaGallery from 'framework/spree/utils/getMediaGallery'
|
|
||||||
import createGetAbsoluteImageUrl from 'framework/spree/utils/createGetAbsoluteImageUrl'
|
|
||||||
import { requireConfigValue } from 'framework/spree/isomorphicConfig'
|
|
||||||
import SpreeResponseContentError from 'framework/spree/errors/SpreeResponseContentError'
|
|
||||||
import expandOptions from 'framework/spree/utils/expandOptions'
|
|
||||||
|
|
||||||
export default function getAllProductsOperation({
|
export default function getAllProductsOperation({
|
||||||
commerce,
|
commerce,
|
||||||
}: OperationContext<SpreeApiProvider>) {
|
}: OperationContext<SpreeApiProvider>) {
|
||||||
|
async function getAllProducts<T extends GetAllProductsOperation>(opts?: {
|
||||||
|
variables?: T['variables']
|
||||||
|
config?: Partial<SpreeApiConfig>
|
||||||
|
preview?: boolean
|
||||||
|
}): Promise<T['data']>
|
||||||
|
|
||||||
|
async function getAllProducts<T extends GetAllProductsOperation>(
|
||||||
|
opts: {
|
||||||
|
variables?: T['variables']
|
||||||
|
config?: Partial<SpreeApiConfig>
|
||||||
|
preview?: boolean
|
||||||
|
} & OperationOptions
|
||||||
|
): Promise<T['data']>
|
||||||
|
|
||||||
async function getAllProducts<T extends GetAllProductsOperation>({
|
async function getAllProducts<T extends GetAllProductsOperation>({
|
||||||
variables: getAllProductsVariables = {},
|
variables: getAllProductsVariables = {},
|
||||||
config: userConfig,
|
config: userConfig,
|
||||||
}: {
|
}: {
|
||||||
variables?: T['variables']
|
variables?: T['variables']
|
||||||
config?: Partial<SpreeApiConfig>
|
config?: Partial<SpreeApiConfig>
|
||||||
} = {}): Promise<{ products: Product[] | any[] }> {
|
} = {}): Promise<{ products: Product[] }> {
|
||||||
console.info(
|
console.info(
|
||||||
'getAllProducts called. Configuration: ',
|
'getAllProducts called. Configuration: ',
|
||||||
'getAllProductsVariables: ',
|
'getAllProductsVariables: ',
|
||||||
@ -56,89 +61,7 @@ export default function getAllProductsOperation({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const normalizedProducts: Product[] = spreeSuccessResponse.data.map(
|
const normalizedProducts: Product[] = spreeSuccessResponse.data.map(
|
||||||
(spreeProduct) => {
|
(spreeProduct) => normalizeProduct(spreeSuccessResponse, spreeProduct)
|
||||||
const spreeImageRecords = findIncludedOfType(
|
|
||||||
spreeSuccessResponse,
|
|
||||||
spreeProduct,
|
|
||||||
'images'
|
|
||||||
)
|
|
||||||
|
|
||||||
const images = getMediaGallery(
|
|
||||||
spreeImageRecords,
|
|
||||||
createGetAbsoluteImageUrl(requireConfigValue('spreeImageHost'))
|
|
||||||
)
|
|
||||||
|
|
||||||
const price: ProductPrice = {
|
|
||||||
value: parseFloat(spreeProduct.attributes.price),
|
|
||||||
currencyCode: spreeProduct.attributes.currency,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add sku to product object equal to master SKU from Spree.
|
|
||||||
// Currently, the Spree API doesn't return it.
|
|
||||||
|
|
||||||
const hasNonMasterVariants =
|
|
||||||
(spreeProduct.relationships.variants.data as RelationType[]).length >
|
|
||||||
0
|
|
||||||
|
|
||||||
let variants: ProductVariant[]
|
|
||||||
let options: ProductOption[] = []
|
|
||||||
|
|
||||||
if (hasNonMasterVariants) {
|
|
||||||
const spreeVariantRecords = findIncludedOfType(
|
|
||||||
spreeSuccessResponse,
|
|
||||||
spreeProduct,
|
|
||||||
'variants'
|
|
||||||
)
|
|
||||||
|
|
||||||
variants = spreeVariantRecords.map((spreeVariantRecord) => {
|
|
||||||
const spreeOptionValues = findIncludedOfType(
|
|
||||||
spreeSuccessResponse,
|
|
||||||
spreeVariantRecord,
|
|
||||||
'option_values'
|
|
||||||
)
|
|
||||||
|
|
||||||
let variantOptions: ProductOption[] = []
|
|
||||||
|
|
||||||
// Only include options which are used by variants.
|
|
||||||
|
|
||||||
spreeOptionValues.forEach((spreeOptionValue) => {
|
|
||||||
variantOptions = expandOptions(
|
|
||||||
spreeSuccessResponse,
|
|
||||||
spreeOptionValue,
|
|
||||||
variantOptions
|
|
||||||
)
|
|
||||||
|
|
||||||
options = expandOptions(
|
|
||||||
spreeSuccessResponse,
|
|
||||||
spreeOptionValue,
|
|
||||||
options
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: spreeVariantRecord.id,
|
|
||||||
options: variantOptions,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
variants = []
|
|
||||||
}
|
|
||||||
|
|
||||||
const slug = spreeProduct.attributes.slug
|
|
||||||
const path = `/${spreeProduct.attributes.slug}`
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: spreeProduct.id,
|
|
||||||
name: spreeProduct.attributes.name,
|
|
||||||
description: spreeProduct.attributes.description,
|
|
||||||
images,
|
|
||||||
variants,
|
|
||||||
options,
|
|
||||||
price,
|
|
||||||
slug,
|
|
||||||
path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return { products: normalizedProducts }
|
return { products: normalizedProducts }
|
||||||
|
@ -1,26 +1,76 @@
|
|||||||
import type { LocalConfig } from '../index'
|
import type { SpreeApiConfig, SpreeApiProvider } from '../index'
|
||||||
import { Product } from '@commerce/types/product'
|
import type { GetProductOperation } from '@commerce/types/product'
|
||||||
import { GetProductOperation } from '@commerce/types/product'
|
import type {
|
||||||
import data from '../../../local/data.json'
|
OperationContext,
|
||||||
import type { OperationContext } from '@commerce/api/operations'
|
OperationOptions,
|
||||||
|
} from '@commerce/api/operations'
|
||||||
|
import type { IProduct } from '@spree/storefront-api-v2-sdk/types/interfaces/Product'
|
||||||
|
import type { SpreeSdkVariables } from 'framework/spree/types'
|
||||||
|
import MissingSlugVariableError from 'framework/spree/errors/MissingSlugVariableError'
|
||||||
|
import normalizeProduct from 'framework/spree/utils/normalizeProduct'
|
||||||
|
|
||||||
export default function getProductOperation({
|
export default function getProductOperation({
|
||||||
commerce,
|
commerce,
|
||||||
}: OperationContext<any>) {
|
}: OperationContext<SpreeApiProvider>) {
|
||||||
|
async function getProduct<T extends GetProductOperation>(opts: {
|
||||||
|
variables: T['variables']
|
||||||
|
config?: Partial<SpreeApiConfig>
|
||||||
|
preview?: boolean
|
||||||
|
}): Promise<T['data']>
|
||||||
|
|
||||||
|
async function getProduct<T extends GetProductOperation>(
|
||||||
|
opts: {
|
||||||
|
variables: T['variables']
|
||||||
|
config?: Partial<SpreeApiConfig>
|
||||||
|
preview?: boolean
|
||||||
|
} & OperationOptions
|
||||||
|
): Promise<T['data']>
|
||||||
|
|
||||||
async function getProduct<T extends GetProductOperation>({
|
async function getProduct<T extends GetProductOperation>({
|
||||||
query = '',
|
query = '',
|
||||||
variables,
|
variables: getProductVariables,
|
||||||
config,
|
config: userConfig,
|
||||||
}: {
|
}: {
|
||||||
query?: string
|
query?: string
|
||||||
variables?: T['variables']
|
variables?: T['variables']
|
||||||
config?: Partial<LocalConfig>
|
config?: Partial<SpreeApiConfig>
|
||||||
preview?: boolean
|
preview?: boolean
|
||||||
} = {}): Promise<Product | {} | any> {
|
}): Promise<T['data']> {
|
||||||
|
console.log(
|
||||||
|
'getProduct called. Configuration: ',
|
||||||
|
'getProductVariables: ',
|
||||||
|
getProductVariables,
|
||||||
|
'config: ',
|
||||||
|
userConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!getProductVariables?.slug) {
|
||||||
|
throw new MissingSlugVariableError()
|
||||||
|
}
|
||||||
|
|
||||||
|
const variables: SpreeSdkVariables = {
|
||||||
|
methodPath: 'products.show',
|
||||||
|
arguments: [
|
||||||
|
getProductVariables.slug,
|
||||||
|
{
|
||||||
|
include: 'variants,images,option_types,variants.option_values',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = commerce.getConfig(userConfig)
|
||||||
|
const { fetch: apiFetch } = config // TODO: Send config.locale to Spree.
|
||||||
|
|
||||||
|
const { data: spreeSuccessResponse } = await apiFetch<IProduct>(
|
||||||
|
'__UNUSED__',
|
||||||
|
{ variables }
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
product: data.products.find(({ slug }) => slug === variables!.slug),
|
product: normalizeProduct(
|
||||||
// TODO: Return Spree product.
|
spreeSuccessResponse,
|
||||||
// product: {},
|
spreeSuccessResponse.data
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
1
framework/spree/errors/MissingSlugVariableError.ts
Normal file
1
framework/spree/errors/MissingSlugVariableError.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default class MissingSlugVariableError extends Error {}
|
@ -2,8 +2,10 @@ import type {
|
|||||||
ProductOption,
|
ProductOption,
|
||||||
ProductOptionValues,
|
ProductOptionValues,
|
||||||
} from '@commerce/types/product'
|
} from '@commerce/types/product'
|
||||||
import type { JsonApiDocument } from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi'
|
import type {
|
||||||
import type { IProducts } from '@spree/storefront-api-v2-sdk/types/interfaces/Product'
|
JsonApiDocument,
|
||||||
|
JsonApiResponse,
|
||||||
|
} from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi'
|
||||||
import type { RelationType } from '@spree/storefront-api-v2-sdk/types/interfaces/Relationships'
|
import type { RelationType } from '@spree/storefront-api-v2-sdk/types/interfaces/Relationships'
|
||||||
import SpreeResponseContentError from '../errors/SpreeResponseContentError'
|
import SpreeResponseContentError from '../errors/SpreeResponseContentError'
|
||||||
import { findIncluded } from './jsonApi'
|
import { findIncluded } from './jsonApi'
|
||||||
@ -12,7 +14,7 @@ const isColorProductOption = (productOption: ProductOption) =>
|
|||||||
productOption.displayName === 'Color'
|
productOption.displayName === 'Color'
|
||||||
|
|
||||||
const expandOptions = (
|
const expandOptions = (
|
||||||
spreeSuccessResponse: IProducts,
|
spreeSuccessResponse: JsonApiResponse,
|
||||||
spreeOptionValue: JsonApiDocument,
|
spreeOptionValue: JsonApiDocument,
|
||||||
accumulatedOptions: ProductOption[]
|
accumulatedOptions: ProductOption[]
|
||||||
): ProductOption[] => {
|
): ProductOption[] => {
|
||||||
|
100
framework/spree/utils/normalizeProduct.ts
Normal file
100
framework/spree/utils/normalizeProduct.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import type {
|
||||||
|
ProductOption,
|
||||||
|
ProductPrice,
|
||||||
|
ProductVariant,
|
||||||
|
} from '@commerce/types/product'
|
||||||
|
import type {
|
||||||
|
JsonApiListResponse,
|
||||||
|
JsonApiResponse,
|
||||||
|
} from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi'
|
||||||
|
import type { ProductAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Product'
|
||||||
|
import type { RelationType } from '@spree/storefront-api-v2-sdk/types/interfaces/Relationships'
|
||||||
|
import { requireConfigValue } from '../isomorphicConfig'
|
||||||
|
import createGetAbsoluteImageUrl from './createGetAbsoluteImageUrl'
|
||||||
|
import expandOptions from './expandOptions'
|
||||||
|
import getMediaGallery from './getMediaGallery'
|
||||||
|
import { findIncludedOfType } from './jsonApi'
|
||||||
|
|
||||||
|
const normalizeProduct = (
|
||||||
|
spreeSuccessResponse: JsonApiResponse | JsonApiListResponse,
|
||||||
|
spreeProduct: ProductAttr
|
||||||
|
) => {
|
||||||
|
const spreeImageRecords = findIncludedOfType(
|
||||||
|
spreeSuccessResponse,
|
||||||
|
spreeProduct,
|
||||||
|
'images'
|
||||||
|
)
|
||||||
|
|
||||||
|
const images = getMediaGallery(
|
||||||
|
spreeImageRecords,
|
||||||
|
createGetAbsoluteImageUrl(requireConfigValue('spreeImageHost'))
|
||||||
|
)
|
||||||
|
|
||||||
|
const price: ProductPrice = {
|
||||||
|
value: parseFloat(spreeProduct.attributes.price),
|
||||||
|
currencyCode: spreeProduct.attributes.currency,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add sku to product object equal to master SKU from Spree.
|
||||||
|
// Currently, the Spree API doesn't return it.
|
||||||
|
|
||||||
|
const hasNonMasterVariants =
|
||||||
|
(spreeProduct.relationships.variants.data as RelationType[]).length > 0
|
||||||
|
|
||||||
|
let variants: ProductVariant[]
|
||||||
|
let options: ProductOption[] = []
|
||||||
|
|
||||||
|
if (hasNonMasterVariants) {
|
||||||
|
const spreeVariantRecords = findIncludedOfType(
|
||||||
|
spreeSuccessResponse,
|
||||||
|
spreeProduct,
|
||||||
|
'variants'
|
||||||
|
)
|
||||||
|
|
||||||
|
variants = spreeVariantRecords.map((spreeVariantRecord) => {
|
||||||
|
const spreeOptionValues = findIncludedOfType(
|
||||||
|
spreeSuccessResponse,
|
||||||
|
spreeVariantRecord,
|
||||||
|
'option_values'
|
||||||
|
)
|
||||||
|
|
||||||
|
let variantOptions: ProductOption[] = []
|
||||||
|
|
||||||
|
// Only include options which are used by variants.
|
||||||
|
|
||||||
|
spreeOptionValues.forEach((spreeOptionValue) => {
|
||||||
|
variantOptions = expandOptions(
|
||||||
|
spreeSuccessResponse,
|
||||||
|
spreeOptionValue,
|
||||||
|
variantOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
options = expandOptions(spreeSuccessResponse, spreeOptionValue, options)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: spreeVariantRecord.id,
|
||||||
|
options: variantOptions,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
variants = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const slug = spreeProduct.attributes.slug
|
||||||
|
const path = `/${spreeProduct.attributes.slug}`
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: spreeProduct.id,
|
||||||
|
name: spreeProduct.attributes.name,
|
||||||
|
description: spreeProduct.attributes.description,
|
||||||
|
images,
|
||||||
|
variants,
|
||||||
|
options,
|
||||||
|
price,
|
||||||
|
slug,
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default normalizeProduct
|
Loading…
x
Reference in New Issue
Block a user