diff --git a/framework/spree/.env.template b/framework/spree/.env.template index 8831ec06c..42de48b17 100644 --- a/framework/spree/.env.template +++ b/framework/spree/.env.template @@ -4,10 +4,7 @@ COMMERCE_PROVIDER=spree {# public (available in the web browser) #} NEXT_PUBLIC_SPREE_API_HOST=http://localhost:3000 - -{# private #} NEXT_PUBLIC_SPREE_DEFAULT_LOCALE=en-us NEXT_PUBLIC_SPREE_CART_COOKIE_NAME=spree_cart - -{# # TODO: #} -{# # COMMERCE_IMAGE_HOST #} +NEXT_PUBLIC_SPREE_IMAGE_HOST=http://localhost:3000 +NEXT_PUBLIC_SPREE_ALLOWED_IMAGE_DOMAIN=localhost diff --git a/framework/spree/api/index.ts b/framework/spree/api/index.ts index 13461dcb7..c8bac3bf1 100644 --- a/framework/spree/api/index.ts +++ b/framework/spree/api/index.ts @@ -1,4 +1,4 @@ -import type { APIProvider, CommerceAPI, CommerceAPIConfig } from '@commerce/api' +import type { APIProvider, CommerceAPIConfig } from '@commerce/api' import { getCommerceApi as commerceApi } from '@commerce/api' import createApiFetch from './utils/create-api-fetch' diff --git a/framework/spree/api/operations/get-all-products.ts b/framework/spree/api/operations/get-all-products.ts index d19280f4c..1791b5ac8 100644 --- a/framework/spree/api/operations/get-all-products.ts +++ b/framework/spree/api/operations/get-all-products.ts @@ -1,78 +1,142 @@ -import { Product } from '@commerce/types/product' -import { GetAllProductsOperation } from '@commerce/types/product' +import type { + Product, + ProductOption, + ProductOptionValues, + ProductPrice, + ProductVariant, +} from '@commerce/types/product' +import type { GetAllProductsOperation } from '@commerce/types/product' import type { OperationContext } from '@commerce/api/operations' -import type { LocalConfig, Provider, SpreeApiProvider } from '../index' import type { IProducts } from '@spree/storefront-api-v2-sdk/types/interfaces/Product' -// import data from '../../../local/data.json' +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' export default function getAllProductsOperation({ commerce, }: OperationContext) { async function getAllProducts({ - query = 'products.list', - variables = { first: 10 }, + variables: getAllProductsVariables = {}, config: userConfig, }: { - query?: string variables?: T['variables'] - config?: Partial + config?: Partial } = {}): Promise<{ products: Product[] | any[] }> { - const config = commerce.getConfig(userConfig) - const { fetch: apiFetch /*, locale*/ } = config - const first = variables.first // How many products to fetch. - - console.log( - 'sdfuasdufahsdf variables = ', - variables, - 'query = ', - query, - 'config = ', - config + console.info( + 'getAllProducts called. Configuration: ', + 'getAllProductsVariables: ', + getAllProductsVariables, + 'config: ', + userConfig ) - console.log('sdfasdg') - - const { data } = await apiFetch( - query, - { variables } - // { - // ...(locale && {}), - // } - ) - - console.log('asuidfhasdf', data) - - // return { - // products: data.products.edges.map(({ node }) => - // normalizeProduct(node as ShopifyProduct) - // ), - // } - - const normalizedProducts: Product[] = data.data.map((spreeProduct) => { - return { - id: spreeProduct.id, - name: spreeProduct.attributes.name, - description: spreeProduct.attributes.description, - images: [], - variants: [], - options: [], - price: { - value: 10, - currencyCode: 'USD', - retailPrice: 8, - salePrice: 7, - listPrice: 6, - extendedSalePrice: 2, - extendedListPrice: 1, + const first = getAllProductsVariables.first + const variables: SpreeSdkVariables = { + methodPath: 'products.list', + arguments: [ + { + include: 'variants,images,option_types,variants.option_values', + per_page: first, }, - } - }) - - return { - // products: data.products, - // TODO: Return Spree products. - products: normalizedProducts, + ], } + + const config = commerce.getConfig(userConfig) + const { fetch: apiFetch } = config // TODO: Send config.locale to Spree. + + const { data: spreeSuccessResponse } = await apiFetch( + '__UNUSED__', + { variables } + ) + + 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 = [] + } + + return { + id: spreeProduct.id, + name: spreeProduct.attributes.name, + description: spreeProduct.attributes.description, + images, + variants, + options, + price, + } + } + ) + + return { products: normalizedProducts } } return getAllProducts diff --git a/framework/spree/api/utils/create-api-fetch.ts b/framework/spree/api/utils/create-api-fetch.ts index 162e5c1d5..68652aca2 100644 --- a/framework/spree/api/utils/create-api-fetch.ts +++ b/framework/spree/api/utils/create-api-fetch.ts @@ -1,7 +1,3 @@ -// import { FetcherError } from '@commerce/utils/errors' -// import type { GraphQLFetcher } from '@commerce/api' -// import type { BigcommerceConfig } from '../index' - import { GraphQLFetcher, GraphQLFetcherResult } from '@commerce/api' import { SpreeApiConfig } from '..' import { errors, makeClient } from '@spree/storefront-api-v2-sdk' @@ -9,81 +5,54 @@ import { requireConfigValue } from 'framework/spree/isomorphicConfig' import convertSpreeErrorToGraphQlError from 'framework/spree/utils/convertSpreeErrorToGraphQlError' import type { ResultResponse } from '@spree/storefront-api-v2-sdk/types/interfaces/ResultResponse' import type { - JsonApiDocument, JsonApiListResponse, + JsonApiResponse, } from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi' -// import fetch from './fetch' - -// const fetchGraphqlApi: (getConfig: () => BigcommerceConfig) => GraphQLFetcher = -// (getConfig) => -// async (query: string, { variables, preview } = {}, fetchOptions) => { -// // log.warn(query) -// const config = getConfig() -// const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), { -// ...fetchOptions, -// method: 'POST', -// headers: { -// Authorization: `Bearer ${config.apiToken}`, -// ...fetchOptions?.headers, -// 'Content-Type': 'application/json', -// }, -// body: JSON.stringify({ -// query, -// variables, -// }), -// }) - -// const json = await res.json() -// if (json.errors) { -// throw new FetcherError({ -// errors: json.errors ?? [{ message: 'Failed to fetch Bigcommerce API' }], -// status: res.status, -// }) -// } - -// return { data: json.data, res } -// } - -// export default fetchGraphqlApi +import getSpreeSdkMethodFromEndpointPath from 'framework/spree/utils/getSpreeSdkMethodFromEndpointPath' +import { SpreeSdkVariables } from 'framework/spree/types' +import SpreeSdkMethodFromEndpointPathError from 'framework/spree/errors/SpreeSdkMethodFromEndpointPathError' const createApiFetch: ( getConfig: () => SpreeApiConfig -) => GraphQLFetcher< - GraphQLFetcherResult -> = (getConfig) => { +) => GraphQLFetcher, SpreeSdkVariables> = ( + getConfig +) => { const client = makeClient({ host: requireConfigValue('spreeApiHost') }) - // FIXME: Allow Spree SDK to use fetch instead of axios. - return async (query, queryData = {}, fetchOptions = {}) => { - const url = query - console.log('ydsfgasgdfagsdf', url) + return async (url, queryData = {}, fetchOptions = {}) => { + console.log( + 'apiFetch called. query = ', + url, + 'url = ', + queryData, + 'fetchOptions = ', + fetchOptions + ) + const { variables } = queryData - let prev = null // FIXME: - const clientEndpointMethod = url - .split('.') - .reduce((clientNode: any, pathPart) => { - prev = clientNode - //FIXME: use actual type instead of any. - // TODO: Fix clientNode type - return clientNode[pathPart] - }, client) - .bind(prev) - console.log('aisdfuiuashdf', clientEndpointMethod) + if (!variables) { + throw new SpreeSdkMethodFromEndpointPathError( + `Required SpreeSdkVariables not provided.` + ) + } - const storeResponse: ResultResponse = - await clientEndpointMethod() // FIXME: Not the best to use variables here as it's type is any. - // await clientEndpointMethod(...variables.args) // FIXME: Not the best to use variables here as it's type is any. - - console.log('87868767868', storeResponse) + const storeResponse: ResultResponse = + await getSpreeSdkMethodFromEndpointPath( + client, + variables.methodPath + )(...variables.arguments) if (storeResponse.success()) { return { data: storeResponse.success(), - res: storeResponse as any, //FIXME: MUST BE FETCH RESPONSE + res: storeResponse as any, //FIXME: MUST BE fetch() RESPONSE instead of axios. } } + // FIXME: Allow Spree SDK to use fetch instead of axios + // (https://github.com/spree/spree-storefront-api-v2-js-sdk/issues/189) + const storeResponseError = storeResponse.fail() if (storeResponseError instanceof errors.SpreeError) { @@ -91,64 +60,7 @@ const createApiFetch: ( } throw storeResponseError - // throw getError( - // [ - // { - // message: `${err} \n Most likely related to an unexpected output. e.g the store might be protected with password or not available.`, - // }, - // ], - // 500 - // ) - // console.log('jsdkfhjasdf', getConfig()) - // // await - // return { - // data: [], - // res: , - // } } } export default createApiFetch - -// LOCAL - -// fetch( -// query: string, -// queryData?: CommerceAPIFetchOptions, -// fetchOptions?: RequestInit -// ): Promise> - -// import { FetcherError } from '@commerce/utils/errors' -// import type { GraphQLFetcher } from '@commerce/api' -// import type { LocalConfig } from '../index' -// import fetch from './fetch' - -// const fetchGraphqlApi: (getConfig: () => LocalConfig) => GraphQLFetcher = -// (getConfig) => -// async (query: string, { variables, preview } = {}, fetchOptions) => { -// const config = getConfig() -// const res = await fetch(config.commerceUrl, { -// ...fetchOptions, -// method: 'POST', -// headers: { -// ...fetchOptions?.headers, -// 'Content-Type': 'application/json', -// }, -// body: JSON.stringify({ -// query, -// variables, -// }), -// }) - -// const json = await res.json() -// if (json.errors) { -// throw new FetcherError({ -// errors: json.errors ?? [{ message: 'Failed to fetch for API' }], -// status: res.status, -// }) -// } - -// return { data: json.data, res } -// } - -// export default fetchGraphqlApi diff --git a/framework/spree/errors/SpreeResponseContentError.ts b/framework/spree/errors/SpreeResponseContentError.ts new file mode 100644 index 000000000..19c10cf2e --- /dev/null +++ b/framework/spree/errors/SpreeResponseContentError.ts @@ -0,0 +1 @@ +export default class SpreeResponseContentError extends Error {} diff --git a/framework/spree/errors/SpreeSdkMethodFromEndpointPathError.ts b/framework/spree/errors/SpreeSdkMethodFromEndpointPathError.ts new file mode 100644 index 000000000..bf15aada0 --- /dev/null +++ b/framework/spree/errors/SpreeSdkMethodFromEndpointPathError.ts @@ -0,0 +1 @@ +export default class SpreeSdkMethodFromEndpointPathError extends Error {} diff --git a/framework/spree/fetcher.ts b/framework/spree/fetcher.ts index 36e79c7a2..48834cf32 100644 --- a/framework/spree/fetcher.ts +++ b/framework/spree/fetcher.ts @@ -3,11 +3,12 @@ import convertSpreeErrorToGraphQlError from './utils/convertSpreeErrorToGraphQlE import { makeClient } from '@spree/storefront-api-v2-sdk' import type { ResultResponse } from '@spree/storefront-api-v2-sdk/types/interfaces/ResultResponse' import type { - JsonApiDocument, JsonApiListResponse, + JsonApiResponse, } from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi' import { errors } from '@spree/storefront-api-v2-sdk' import { requireConfigValue } from './isomorphicConfig' +import getSpreeSdkMethodFromEndpointPath from './utils/getSpreeSdkMethodFromEndpointPath' // import { handleFetchResponse } from './utils' const client = makeClient({ host: requireConfigValue('spreeApiHost') }) @@ -31,32 +32,10 @@ const fetcher: Fetcher = async (requestOptions) => { `Fetching products using options: ${JSON.stringify(requestOptions)}.` ) - // const storeResponse = await fetch(url, { - // method, - // body: JSON.stringify({ query, variables: vars }), - // headers: { - // 'X-Shopify-Storefront-Access-Token': API_TOKEN, - // 'Content-Type': 'application/json', TODO: Probably not needed. Check! - // }, - // }) - - // const storeResponse.json() - - // if (storeResponse.ok) { - // return - // } - // TODO: Not best to use url for finding the method, but should be good enough for now. - const clientEndpointMethod = url - .split('.') - .reduce((clientNode: any, pathPart) => { - // TODO: Fix clientNode type - return clientNode[pathPart] - }, client) - - const storeResponse: ResultResponse = - await clientEndpointMethod(...variables.args) // TODO: Not the best to use variables here as it's type is any. + const storeResponse: ResultResponse = + await getSpreeSdkMethodFromEndpointPath(client, url)(...variables.args) // TODO: Not the best to use variables here as it's type is any. if (storeResponse.success()) { return storeResponse.success() diff --git a/framework/spree/isomorphicConfig.ts b/framework/spree/isomorphicConfig.ts index 4d5938b78..650125842 100644 --- a/framework/spree/isomorphicConfig.ts +++ b/framework/spree/isomorphicConfig.ts @@ -5,12 +5,13 @@ const isomorphicConfig = { spreeApiHost: process.env.NEXT_PUBLIC_SPREE_API_HOST, defaultLocale: process.env.NEXT_PUBLIC_SPREE_DEFAULT_LOCALE, cartCookieName: process.env.NEXT_PUBLIC_SPREE_CART_COOKIE_NAME, + spreeImageHost: process.env.NEXT_PUBLIC_SPREE_IMAGE_HOST, } export default forceIsomorphicConfigValues( isomorphicConfig, - ['defaultLocale', 'cartCookieName'], - ['spreeApiHost'] + [], + ['spreeApiHost', 'defaultLocale', 'cartCookieName', 'spreeImageHost'] ) type IsomorphicConfig = typeof isomorphicConfig diff --git a/framework/spree/next.config.js b/framework/spree/next.config.js index 11b9ef289..13d530a49 100644 --- a/framework/spree/next.config.js +++ b/framework/spree/next.config.js @@ -2,7 +2,7 @@ const commerce = require('./commerce.config.json') module.exports = { commerce, - // images: { - // domains: [process.env.COMMERCE_IMAGE_HOST], - // }, + images: { + domains: [process.env.NEXT_PUBLIC_SPREE_ALLOWED_IMAGE_DOMAIN], + }, } diff --git a/framework/spree/types/index.ts b/framework/spree/types/index.ts index 5ddeee8e5..6b5e98b05 100644 --- a/framework/spree/types/index.ts +++ b/framework/spree/types/index.ts @@ -1,5 +1,35 @@ +import type { + JsonApiDocument, + JsonApiListResponse, + JsonApiSingleResponse, +} from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi' +import type { ResultResponse } from '@spree/storefront-api-v2-sdk/types/interfaces/ResultResponse' + export type UnknownObjectValues = Record export type NonUndefined = T extends undefined ? never : T export type ValueOf = T[keyof T] + +export type SpreeSdkMethodReturnType = Promise< + ResultResponse +> + +export type SpreeSdkMethod = (...args: any[]) => SpreeSdkMethodReturnType + +export type SpreeSdkVariables = { + methodPath: string + arguments: any[] +} + +export interface ImageStyle { + url: string + width: string + height: string +} + +export interface SpreeProductImage extends JsonApiDocument { + attributes: { + styles: ImageStyle[] + } +} diff --git a/framework/spree/utils/createGetAbsoluteImageUrl.ts b/framework/spree/utils/createGetAbsoluteImageUrl.ts new file mode 100644 index 000000000..37932f15d --- /dev/null +++ b/framework/spree/utils/createGetAbsoluteImageUrl.ts @@ -0,0 +1,20 @@ +import { SpreeProductImage } from '../types' +import getImageUrl from './getImageUrl' + +const createGetAbsoluteImageUrl = + (host: string) => + ( + image: SpreeProductImage, + minWidth: number, + minHeight: number + ): string | null => { + const url = getImageUrl(image, minWidth, minHeight) + + if (url === null) { + return null + } + + return `${host}${url}` + } + +export default createGetAbsoluteImageUrl diff --git a/framework/spree/utils/expandOptions.ts b/framework/spree/utils/expandOptions.ts new file mode 100644 index 000000000..75f4089e8 --- /dev/null +++ b/framework/spree/utils/expandOptions.ts @@ -0,0 +1,89 @@ +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 { RelationType } from '@spree/storefront-api-v2-sdk/types/interfaces/Relationships' +import SpreeResponseContentError from '../errors/SpreeResponseContentError' +import { findIncluded } from './jsonApi' + +const isColorProductOption = (productOption: ProductOption) => + productOption.displayName === 'Color' + +const expandOptions = ( + spreeSuccessResponse: IProducts, + spreeOptionValue: JsonApiDocument, + accumulatedOptions: ProductOption[] +): ProductOption[] => { + const spreeOptionTypeIdentifier = spreeOptionValue.relationships.option_type + .data as RelationType + + const existingOptionIndex = accumulatedOptions.findIndex( + (option) => option.id == spreeOptionTypeIdentifier.id + ) + + let option: ProductOption + + if (existingOptionIndex === -1) { + const spreeOptionType = findIncluded( + spreeSuccessResponse, + spreeOptionTypeIdentifier.type, + spreeOptionTypeIdentifier.id + ) + + if (!spreeOptionType) { + throw new SpreeResponseContentError( + `Option type with id ${spreeOptionTypeIdentifier.id} not found.` + ) + } + + option = { + id: spreeOptionType.id, + displayName: spreeOptionType.attributes.presentation, + values: [], + } + } else { + const existingOption = accumulatedOptions[existingOptionIndex] + + option = existingOption + } + + let optionValue: ProductOptionValues + + const label = isColorProductOption(option) + ? spreeOptionValue.attributes.name + : spreeOptionValue.attributes.presentation + + const productOptionValueExists = option.values.some( + (optionValue: ProductOptionValues) => optionValue.label === label + ) + + if (!productOptionValueExists) { + if (isColorProductOption(option)) { + optionValue = { + label, + hexColors: [spreeOptionValue.attributes.presentation], + } + } else { + optionValue = { + label, + } + } + + const expandedOptionValues = [...option.values, optionValue] + + const expandedOptions = [...accumulatedOptions] + + expandedOptions[existingOptionIndex] = { + ...option, + values: expandedOptionValues, + } + + return expandedOptions + } + + return accumulatedOptions +} + +export default expandOptions diff --git a/framework/spree/utils/forceIsomorphicConfigValues.ts b/framework/spree/utils/forceIsomorphicConfigValues.ts index 94c38c2af..359ec5167 100644 --- a/framework/spree/utils/forceIsomorphicConfigValues.ts +++ b/framework/spree/utils/forceIsomorphicConfigValues.ts @@ -4,7 +4,7 @@ import isServer from './isServer' const generateMisconfigurationErrorMessage = ( keys: Array -) => `${keys.join(', ')} must have values before running the Framework.` +) => `${keys.join(', ')} must have a value before running the Framework.` const forceIsomorphicConfigValues = < X extends keyof T, diff --git a/framework/spree/utils/getImageUrl.ts b/framework/spree/utils/getImageUrl.ts new file mode 100644 index 000000000..8594f5c34 --- /dev/null +++ b/framework/spree/utils/getImageUrl.ts @@ -0,0 +1,44 @@ +// Based on https://github.com/spark-solutions/spree2vuestorefront/blob/d88d85ae1bcd2ec99b13b81cd2e3c25600a0216e/src/utils/index.ts + +import type { ImageStyle, SpreeProductImage } from '../types' + +const getImageUrl = ( + image: SpreeProductImage, + minWidth: number, + _: number +): string | null => { + // every image is still resized in vue-storefront-api, no matter what getImageUrl returns + if (image) { + const { + attributes: { styles }, + } = image + const bestStyleIndex = styles.reduce( + (bSIndex: number | null, style: ImageStyle, styleIndex: number) => { + // assuming all images are the same dimensions, just scaled + if (bSIndex === null) { + return 0 + } + const bestStyle = styles[bSIndex] + const widthDiff = +bestStyle.width - minWidth + const minWidthDiff = +style.width - minWidth + if (widthDiff < 0 && minWidthDiff > 0) { + return styleIndex + } + if (widthDiff > 0 && minWidthDiff < 0) { + return bSIndex + } + return Math.abs(widthDiff) < Math.abs(minWidthDiff) + ? bSIndex + : styleIndex + }, + null + ) + + if (bestStyleIndex !== null) { + return styles[bestStyleIndex].url + } + } + return null +} + +export default getImageUrl diff --git a/framework/spree/utils/getMediaGallery.ts b/framework/spree/utils/getMediaGallery.ts new file mode 100644 index 000000000..fe5587aa2 --- /dev/null +++ b/framework/spree/utils/getMediaGallery.ts @@ -0,0 +1,30 @@ +// Based on https://github.com/spark-solutions/spree2vuestorefront/blob/d88d85ae1bcd2ec99b13b81cd2e3c25600a0216e/src/utils/index.ts + +import type { ProductImage } from '@commerce/types/product' +import type { SpreeProductImage } from '../types' + +const getMediaGallery = ( + images: SpreeProductImage[], + getImageUrl: ( + image: SpreeProductImage, + minWidth: number, + minHeight: number + ) => string | null +) => { + return images.reduce((productImages, _, imageIndex) => { + const imageUrl = getImageUrl(images[imageIndex], 9001, 9001) + + if (imageUrl) { + return [ + ...productImages, + { + url: imageUrl, + }, + ] + } + + return productImages + }, []) +} + +export default getMediaGallery diff --git a/framework/spree/utils/getSpreeSdkMethodFromEndpointPath.ts b/framework/spree/utils/getSpreeSdkMethodFromEndpointPath.ts new file mode 100644 index 000000000..f1ce31f86 --- /dev/null +++ b/framework/spree/utils/getSpreeSdkMethodFromEndpointPath.ts @@ -0,0 +1,56 @@ +import type { Client } from '@spree/storefront-api-v2-sdk' +import SpreeSdkMethodFromEndpointPathError from '../errors/SpreeSdkMethodFromEndpointPathError' +import { SpreeSdkMethod } from '../types' + +const getSpreeSdkMethodFromEndpointPath = < + ExactSpreeSdkClientType extends Client +>( + client: ExactSpreeSdkClientType, + path: string +) => { + const pathParts = path.split('.') + const reachedPath: string[] = [] + let node = >client + + console.log(`Looking for ${path} in Spree Sdk.`) + + while (reachedPath.length < pathParts.length - 1) { + const checkedPathPart = pathParts[reachedPath.length] + const checkedNode = node[checkedPathPart] + + console.log(`Checking part ${checkedPathPart}.`) + + if (typeof checkedNode !== 'object') { + throw new SpreeSdkMethodFromEndpointPathError( + `Couldn't reach ${path}. Farthest path reached was: ${reachedPath.join( + '.' + )}.` + ) + } + + if (checkedNode === null) { + throw new SpreeSdkMethodFromEndpointPathError( + `Path ${path} doesn't exist.` + ) + } + + node = >checkedNode + reachedPath.push(checkedPathPart) + } + + if ( + reachedPath.length !== pathParts.length - 1 || + typeof node[pathParts[reachedPath.length]] !== 'function' + ) { + throw new SpreeSdkMethodFromEndpointPathError( + `Couldn't reach ${path}. Farthest path reached was: ${reachedPath.join( + '.' + )}.` + ) + } + + return (...args: any[]) => + (node[pathParts[reachedPath.length]] as SpreeSdkMethod)(...args) +} + +export default getSpreeSdkMethodFromEndpointPath diff --git a/framework/spree/utils/jsonApi.ts b/framework/spree/utils/jsonApi.ts new file mode 100644 index 000000000..6dc30f89f --- /dev/null +++ b/framework/spree/utils/jsonApi.ts @@ -0,0 +1,46 @@ +// Based on https://github.com/spark-solutions/spree2vuestorefront + +import type { + JsonApiResponse, + JsonApiDocument, +} from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi' + +export const findIncluded = ( + response: JsonApiResponse, + objectType: string, + objectId: string +): T | null => { + if (!response.included) { + return null + } + + return ( + (response.included.find( + (includedObject) => + includedObject.type === objectType && includedObject.id === objectId + ) as T) || null + ) +} + +export const findIncludedOfType = ( + response: JsonApiResponse, + singlePrimaryRecord: JsonApiDocument, + objectRelationshipType: string +): T[] => { + if (!response.included) { + return [] + } + + const typeRelationships = + singlePrimaryRecord.relationships[objectRelationshipType] + + if (!typeRelationships) { + return [] + } + + return typeRelationships.data + .map((typeObject: JsonApiDocument) => + findIncluded(response, typeObject.type, typeObject.id) + ) + .filter((typeRecord: JsonApiDocument | null) => !!typeRecord) +}