From a3ef27f5e71df976d23926ea87d6724fbbb99a6f Mon Sep 17 00:00:00 2001 From: tniezg Date: Fri, 23 Jul 2021 11:23:42 +0200 Subject: [PATCH] Add basic Spree framework structure --- framework/spree/.env.template | 8 ++ framework/spree/README.md | 2 + framework/spree/commerce.config.json | 9 ++ framework/spree/createFetcher.ts | 88 +++++++++++++++++++ framework/spree/createProvider.ts | 43 +++++++++ framework/spree/index.tsx | 46 ++++++++++ framework/spree/next.config.js | 13 +++ framework/spree/product/use-search.tsx | 62 +++++++++++++ .../utils/convertSpreeErrorToGraphQlError.ts | 46 ++++++++++ 9 files changed, 317 insertions(+) create mode 100644 framework/spree/.env.template create mode 100644 framework/spree/README.md create mode 100644 framework/spree/commerce.config.json create mode 100644 framework/spree/createFetcher.ts create mode 100644 framework/spree/createProvider.ts create mode 100644 framework/spree/index.tsx create mode 100644 framework/spree/next.config.js create mode 100644 framework/spree/product/use-search.tsx create mode 100644 framework/spree/utils/convertSpreeErrorToGraphQlError.ts diff --git a/framework/spree/.env.template b/framework/spree/.env.template new file mode 100644 index 000000000..d6644591a --- /dev/null +++ b/framework/spree/.env.template @@ -0,0 +1,8 @@ +# Template to be used for creating .env* files (.env, .env.local etc.) in the project's root directory. + +COMMERCE_PROVIDER=spree + +SPREE_API_HOST = 'http://localhost:3000' + +# TODO: +# COMMERCE_IMAGE_HOST diff --git a/framework/spree/README.md b/framework/spree/README.md new file mode 100644 index 000000000..1d2f12f87 --- /dev/null +++ b/framework/spree/README.md @@ -0,0 +1,2 @@ +TODO: Base README on other Framework READMEs. +TODO: Link to demo site running NextJS Commerce communicating with Spree. diff --git a/framework/spree/commerce.config.json b/framework/spree/commerce.config.json new file mode 100644 index 000000000..4129d4854 --- /dev/null +++ b/framework/spree/commerce.config.json @@ -0,0 +1,9 @@ +{ + "provider": "spree", + "features": { + "wishlist": false, + "cart": false, + "search": false, + "customerAuth": false + } +} diff --git a/framework/spree/createFetcher.ts b/framework/spree/createFetcher.ts new file mode 100644 index 000000000..fafce34ee --- /dev/null +++ b/framework/spree/createFetcher.ts @@ -0,0 +1,88 @@ +import type { Fetcher } from '@commerce/utils/types' +import convertSpreeErrorToGraphQlError from './utils/convertSpreeErrorToGraphQlError' +import { makeClient } from '@spree/storefront-api-v2-sdk' +import type { ResultResponse } from '@spree/storefront-api-v2-sdk/types/interfaces/ResultResponse' +import type { + JsonApiDocument, + JsonApiListResponse, +} from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi' +import { errors } from '@spree/storefront-api-v2-sdk' +// import { API_TOKEN, API_URL } from './const' +// import { handleFetchResponse } from './utils' + +const createFetcher = (fetcherOptions: any): Fetcher => { + const { host } = fetcherOptions + const client = makeClient({ host }) + + //TODO: Add types to fetcherOptions + return async (requestOptions) => { + console.log('FETCHER') + // url?: string + // query?: string + // method?: string + // variables?: any + // body?: Body + const { url, method, variables, query } = requestOptions + const { locale, ...vars } = variables ?? {} + + if (!url) { + // TODO: Create a custom type for this error. + throw new Error('Url not provider for fetcher.') + } + + console.log( + `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. + + if (storeResponse.success()) { + return storeResponse.success() + } + + if (storeResponse.fail() instanceof errors.SpreeError) { + throw convertSpreeErrorToGraphQlError(storeResponse.fail()) + } + + throw storeResponse.fail() + } +} + +// import { Fetcher } from '@commerce/utils/types' + +// export const fetcher: Fetcher = async () => { +// console.log('FETCHER') +// const res = await fetch('./data.json') +// if (res.ok) { +// const { data } = await res.json() +// return data +// } +// throw res +// } + +export default createFetcher diff --git a/framework/spree/createProvider.ts b/framework/spree/createProvider.ts new file mode 100644 index 000000000..d8756bd73 --- /dev/null +++ b/framework/spree/createProvider.ts @@ -0,0 +1,43 @@ +import type { Provider } from '@commerce' +import { Config } from '.' +import fetcher from './fetcher' + +// import { handler as useCart } from './cart/use-cart' +// import { handler as useAddItem } from './cart/use-add-item' +// import { handler as useUpdateItem } from './cart/use-update-item' +// import { handler as useRemoveItem } from './cart/use-remove-item' + +// import { handler as useCustomer } from './customer/use-customer' +// import { handler as useSearch } from './product/use-search' + +// import { handler as useLogin } from './auth/use-login' +// import { handler as useLogout } from './auth/use-logout' +// import { handler as useSignup } from './auth/use-signup' + +// export const saleorProvider = { +// locale: 'en-us', +// cartCookie: '', +// cartCookieToken: '', +// fetcher, +// cart: { useCart, useAddItem, useUpdateItem, useRemoveItem }, +// customer: { useCustomer }, +// products: { useSearch }, +// auth: { useLogin, useLogout, useSignup }, +// } + +export const createProvider = (options: { config: Config }): Provider => { + const { config } = options + + return { + locale: '', // Not an optional key in TypeScript, but already set in config. So, just make it an empty string. + cartCookie: '', // Not an optional key in TypeScript, but already set in config. So, just make it an empty string. + fetcher: createFetcher({ host: config.store.host }), + // FIXME: Add dummy hooks for below based on framework/local EXCEPT use-product + cart: { useCart, useAddItem, useUpdateItem, useRemoveItem }, + customer: { useCustomer }, + products: { useSearch }, + auth: { useLogin, useLogout, useSignup }, + } +} + +export type { Provider } diff --git a/framework/spree/index.tsx b/framework/spree/index.tsx new file mode 100644 index 000000000..db8865117 --- /dev/null +++ b/framework/spree/index.tsx @@ -0,0 +1,46 @@ +import * as React from 'react' +import { ReactNode } from 'react' + +import { + CommerceConfig, + CommerceProvider as CoreCommerceProvider, + useCommerce as useCoreCommerce, +} from '@commerce' + +// import { provider, Provider } from './provider' +import { createProvider, Provider } from './createProvider' + +// export { provider } + +// TODO: Below is probably not needed. Expect default values to be set by NextJS Commerce and be ok for now. +// export const saleorConfig: CommerceConfig = { +// locale: 'en-us', +// cartCookie: Const.CHECKOUT_ID_COOKIE, +// } + +export type Config = { + store: { + host: string + } +} & CommerceConfig // This is the type that holds any custom values specifically for the Spree Framework. + +export type SpreeProps = { + children: ReactNode + provider: Provider + config: Config +} & Config + +export function CommerceProvider({ children, ...config }: SpreeProps) { + console.log('CommerceProvider called') + + // TODO: Make sure this doesn't get called all the time. If it does, useMemo. + const provider = createProvider({ config }) + + return ( + + {children} + + ) +} + +export const useCommerce = () => useCoreCommerce() diff --git a/framework/spree/next.config.js b/framework/spree/next.config.js new file mode 100644 index 000000000..0bef26aba --- /dev/null +++ b/framework/spree/next.config.js @@ -0,0 +1,13 @@ +const commerce = require('./commerce.config.json') + +module.exports = { + commerce, + store: { + host: process.env.SPREE_API_HOST, + }, + // images: { + // domains: [process.env.COMMERCE_IMAGE_HOST], + // }, + // locale: 'en-us', + // cartCookie: Const.CHECKOUT_ID_COOKIE, +} diff --git a/framework/spree/product/use-search.tsx b/framework/spree/product/use-search.tsx new file mode 100644 index 000000000..85bb68a77 --- /dev/null +++ b/framework/spree/product/use-search.tsx @@ -0,0 +1,62 @@ +import type { SWRHook, Fetcher } from '@commerce/utils/types' +import useSearch from '@commerce/product/use-search' +import type { UseSearch } from '@commerce/product/use-search' + +export const handler: SWRHook = { + fetchOptions: { + url: 'client.products.list', // Add custom option for method name later + query: '', + }, + async fetcher({ input, options, fetch }) { + // This method is only needed if the options need to be modified before calling the generic fetcher (created in createFetcher). + // TODO: Actually filter by input and query. + + console.log( + `Calling useSearch fetcher with input: ${JSON.stringify( + input + )} and options: ${JSON.stringify(options)}.` + ) + + // FIXME: IMPLEMENT + + return fetch({ + url: options.url, + variables: { args: [] }, // TODO: Actually provide args later. + }) + }, + // useHook is used for both, SWR and mutation requests to the store. + // useHook is called in React components. For example, after clicking `Add to cart`. + useHook: + ({ useData }) => + (input = {}) => { + console.log('useHook called') + + // useData calls the fetcher method (above). + // The difference between useHook and calling fetcher directly is + // useHook accepts swrOptions. + return useData({ + input: [ + ['search', input.search], + ['categoryId', input.categoryId], + ['brandId', input.brandId], + ['sort', input.sort], + ], + swrOptions: { + revalidateOnFocus: false, + // revalidateOnFocus: false means do not fetch products again when website is refocused in the web browser. + ...input.swrOptions, + }, + }) + }, + // (input = {}) => { + + // return { + // data: { + // // FIXME: Use actual fetcher + // products: [], + // }, + // } + // }, +} + +export default useSearch as UseSearch diff --git a/framework/spree/utils/convertSpreeErrorToGraphQlError.ts b/framework/spree/utils/convertSpreeErrorToGraphQlError.ts new file mode 100644 index 000000000..5db1a64b2 --- /dev/null +++ b/framework/spree/utils/convertSpreeErrorToGraphQlError.ts @@ -0,0 +1,46 @@ +import { FetcherError } from '@commerce/utils/errors' +import { errors } from '@spree/storefront-api-v2-sdk' + +const convertSpreeErrorToGraphQlError = ( + error: errors.SpreeError +): FetcherError => { + if (error instanceof errors.ExpandedSpreeError) { + // Assuming error.errors[key] is a list of strings. + + if ('base' in error.errors) { + const baseErrorMessage = error.errors.base as unknown as string + + return new FetcherError({ + status: error.serverResponse.status, + message: baseErrorMessage, + }) + } + + const fetcherErrors = Object.keys(error.errors).map((sdkErrorKey) => { + const errors = error.errors[sdkErrorKey] as string[] + + return { + message: `${sdkErrorKey} ${errors.join(', ')}`, + } + }) + + return new FetcherError({ + status: error.serverResponse.status, + errors: fetcherErrors, + }) + } + + if (error instanceof errors.BasicSpreeError) { + return new FetcherError({ + status: error.serverResponse.status, + message: error.summary, + }) + } + + return new FetcherError({ + status: error.serverResponse.status, + message: error.message, + }) +} + +export default convertSpreeErrorToGraphQlError