diff --git a/framework/spree/api/operations/get-all-product-paths.ts b/framework/spree/api/operations/get-all-product-paths.ts index e2368a833..b96f7008a 100644 --- a/framework/spree/api/operations/get-all-product-paths.ts +++ b/framework/spree/api/operations/get-all-product-paths.ts @@ -6,6 +6,8 @@ export type GetAllProductPathsResult = { export default function getAllProductPathsOperation() { function getAllProductPaths(): Promise { + console.log('getAllProductPaths called.') + return Promise.resolve({ // products: data.products.map(({ path }) => ({ path })), // TODO: Return Storefront [{ path: '/long-sleeve-shirt' }, ...] from Spree products. Paths using product IDs are fine too. diff --git a/framework/spree/api/operations/get-all-products.ts b/framework/spree/api/operations/get-all-products.ts index 2e74eb8b5..3572ee2d2 100644 --- a/framework/spree/api/operations/get-all-products.ts +++ b/framework/spree/api/operations/get-all-products.ts @@ -1,33 +1,38 @@ -import type { - Product, - ProductOption, - ProductOptionValues, - ProductPrice, - ProductVariant, -} from '@commerce/types/product' +import type { Product } 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 { RelationType } from '@spree/storefront-api-v2-sdk/types/interfaces/Relationships' import type { SpreeApiConfig, SpreeApiProvider } from '../index' import type { SpreeSdkVariables } from 'framework/spree/types' -import { findIncluded, findIncludedOfType } from 'framework/spree/utils/jsonApi' -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' +import normalizeProduct from 'framework/spree/utils/normalizeProduct' export default function getAllProductsOperation({ commerce, }: OperationContext) { + async function getAllProducts(opts?: { + variables?: T['variables'] + config?: Partial + preview?: boolean + }): Promise + + async function getAllProducts( + opts: { + variables?: T['variables'] + config?: Partial + preview?: boolean + } & OperationOptions + ): Promise + async function getAllProducts({ variables: getAllProductsVariables = {}, config: userConfig, }: { variables?: T['variables'] config?: Partial - } = {}): Promise<{ products: Product[] | any[] }> { + } = {}): Promise<{ products: Product[] }> { console.info( 'getAllProducts called. Configuration: ', 'getAllProductsVariables: ', @@ -56,89 +61,7 @@ export default function getAllProductsOperation({ ) const normalizedProducts: Product[] = spreeSuccessResponse.data.map( - (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, - } - } + (spreeProduct) => normalizeProduct(spreeSuccessResponse, spreeProduct) ) return { products: normalizedProducts } diff --git a/framework/spree/api/operations/get-product.ts b/framework/spree/api/operations/get-product.ts index de0804592..87c0ff284 100644 --- a/framework/spree/api/operations/get-product.ts +++ b/framework/spree/api/operations/get-product.ts @@ -1,26 +1,76 @@ -import type { LocalConfig } from '../index' -import { Product } from '@commerce/types/product' -import { GetProductOperation } from '@commerce/types/product' -import data from '../../../local/data.json' -import type { OperationContext } from '@commerce/api/operations' +import type { SpreeApiConfig, SpreeApiProvider } from '../index' +import type { GetProductOperation } from '@commerce/types/product' +import type { + OperationContext, + 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({ commerce, -}: OperationContext) { +}: OperationContext) { + async function getProduct(opts: { + variables: T['variables'] + config?: Partial + preview?: boolean + }): Promise + + async function getProduct( + opts: { + variables: T['variables'] + config?: Partial + preview?: boolean + } & OperationOptions + ): Promise + async function getProduct({ query = '', - variables, - config, + variables: getProductVariables, + config: userConfig, }: { query?: string variables?: T['variables'] - config?: Partial + config?: Partial preview?: boolean - } = {}): Promise { + }): Promise { + 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( + '__UNUSED__', + { variables } + ) + return { - product: data.products.find(({ slug }) => slug === variables!.slug), - // TODO: Return Spree product. - // product: {}, + product: normalizeProduct( + spreeSuccessResponse, + spreeSuccessResponse.data + ), } } diff --git a/framework/spree/errors/MissingSlugVariableError.ts b/framework/spree/errors/MissingSlugVariableError.ts new file mode 100644 index 000000000..09b9d2e20 --- /dev/null +++ b/framework/spree/errors/MissingSlugVariableError.ts @@ -0,0 +1 @@ +export default class MissingSlugVariableError extends Error {} diff --git a/framework/spree/utils/expandOptions.ts b/framework/spree/utils/expandOptions.ts index 75f4089e8..455354608 100644 --- a/framework/spree/utils/expandOptions.ts +++ b/framework/spree/utils/expandOptions.ts @@ -2,8 +2,10 @@ import type { ProductOption, ProductOptionValues, } from '@commerce/types/product' -import type { JsonApiDocument } from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi' -import type { IProducts } from '@spree/storefront-api-v2-sdk/types/interfaces/Product' +import type { + JsonApiDocument, + JsonApiResponse, +} from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi' import type { RelationType } from '@spree/storefront-api-v2-sdk/types/interfaces/Relationships' import SpreeResponseContentError from '../errors/SpreeResponseContentError' import { findIncluded } from './jsonApi' @@ -12,7 +14,7 @@ const isColorProductOption = (productOption: ProductOption) => productOption.displayName === 'Color' const expandOptions = ( - spreeSuccessResponse: IProducts, + spreeSuccessResponse: JsonApiResponse, spreeOptionValue: JsonApiDocument, accumulatedOptions: ProductOption[] ): ProductOption[] => { diff --git a/framework/spree/utils/normalizeProduct.ts b/framework/spree/utils/normalizeProduct.ts new file mode 100644 index 000000000..0953d85a5 --- /dev/null +++ b/framework/spree/utils/normalizeProduct.ts @@ -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