Add basic Spree framework structure

This commit is contained in:
tniezg 2021-07-23 11:23:42 +02:00
parent dd6ad7556e
commit a3ef27f5e7
9 changed files with 317 additions and 0 deletions

View File

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

View File

@ -0,0 +1,2 @@
TODO: Base README on other Framework READMEs.
TODO: Link to demo site running NextJS Commerce communicating with Spree.

View File

@ -0,0 +1,9 @@
{
"provider": "spree",
"features": {
"wishlist": false,
"cart": false,
"search": false,
"customerAuth": false
}
}

View File

@ -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<JsonApiDocument | JsonApiListResponse> =
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

View File

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

46
framework/spree/index.tsx Normal file
View File

@ -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 (
<CoreCommerceProvider provider={provider} config={config}>
{children}
</CoreCommerceProvider>
)
}
export const useCommerce = () => useCoreCommerce<Provider>()

View File

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

View File

@ -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<any> = {
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<typeof handler>

View File

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