Fetch single product during build time

This commit is contained in:
tniezg 2021-07-28 14:05:46 +02:00
parent 24e635b048
commit a7a75e7f69
6 changed files with 193 additions and 115 deletions

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
export default class MissingSlugVariableError extends Error {}

View File

@ -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[] => {

View 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