4
0
forked from crowetic/commerce

Merge branch 'agnostic' of https://github.com/vercel/commerce into agnostic

This commit is contained in:
cond0r 2021-02-11 13:47:38 +02:00
commit 1450492826
11 changed files with 223 additions and 168 deletions

View File

@ -1,4 +1,4 @@
import useCommerceCart, { UseCart } from '@commerce/cart/use-cart' import useCart, { UseCart } from '@commerce/cart/use-cart'
import type { BigcommerceProvider } from '..' import type { BigcommerceProvider } from '..'
export default useCommerceCart as UseCart<BigcommerceProvider> export default useCart as UseCart<BigcommerceProvider>

View File

@ -1,4 +1,4 @@
import { ReactNode } from 'react' import type { ReactNode } from 'react'
import { import {
CommerceConfig, CommerceConfig,
CommerceProvider as CoreCommerceProvider, CommerceProvider as CoreCommerceProvider,

View File

@ -1,7 +1,10 @@
import { useMemo } from 'react'
import { FetcherError } from '@commerce/utils/errors' import { FetcherError } from '@commerce/utils/errors'
import type { Fetcher, HookHandler } from '@commerce/utils/types' import type { Fetcher, HookHandler } from '@commerce/utils/types'
import type { FetchCartInput } from '@commerce/cart/use-cart' import type { FetchCartInput } from '@commerce/cart/use-cart'
import { normalizeCart } from './lib/normalize' import { normalizeCart } from './lib/normalize'
import type { Wishlist } from './api/wishlist'
import useCustomer from './customer/use-customer'
import type { Cart } from './types' import type { Cart } from './types'
async function getText(res: Response) { async function getText(res: Response) {
@ -43,7 +46,7 @@ const fetcher: Fetcher = async ({
const useCart: HookHandler< const useCart: HookHandler<
Cart | null, Cart | null,
[], {},
FetchCartInput, FetchCartInput,
any, any,
any, any,
@ -53,26 +56,31 @@ const useCart: HookHandler<
url: '/api/bigcommerce/cart', url: '/api/bigcommerce/cart',
method: 'GET', method: 'GET',
}, },
swrOptions: {
revalidateOnFocus: false,
},
normalizer: normalizeCart, normalizer: normalizeCart,
onResponse(response) { useHook({ input, useData }) {
return Object.create(response, { const response = useData({
isEmpty: { swrOptions: { revalidateOnFocus: false, ...input.swrOptions },
get() {
return (response.data?.lineItems.length ?? 0) <= 0
},
enumerable: true,
},
}) })
return useMemo(
() =>
Object.create(response, {
isEmpty: {
get() {
return (response.data?.lineItems.length ?? 0) <= 0
},
enumerable: true,
},
}),
[response]
)
}, },
} }
const useWishlist: HookHandler< const useWishlist: HookHandler<
Cart | null, Wishlist | null,
[], { includeProducts?: boolean },
FetchCartInput, { customerId?: number; includeProducts: boolean },
any, any,
any, any,
{ isEmpty?: boolean } { isEmpty?: boolean }
@ -81,18 +89,44 @@ const useWishlist: HookHandler<
url: '/api/bigcommerce/wishlist', url: '/api/bigcommerce/wishlist',
method: 'GET', method: 'GET',
}, },
swrOptions: { fetcher({ input: { customerId, includeProducts }, options, fetch }) {
revalidateOnFocus: false, if (!customerId) return null
// Use a dummy base as we only care about the relative path
const url = new URL(options.url!, 'http://a')
if (includeProducts) url.searchParams.set('products', '1')
return fetch({
url: url.pathname + url.search,
method: options.method,
})
}, },
onResponse(response) { useHook({ input, useData }) {
return Object.create(response, { const { data: customer } = useCustomer()
isEmpty: { const response = useData({
get() { input: [
return (response.data?.lineItems.length ?? 0) <= 0 ['customerId', customer?.id],
}, ['includeProducts', input.includeProducts],
enumerable: true, ],
swrOptions: {
revalidateOnFocus: false,
...input.swrOptions,
}, },
}) })
return useMemo(
() =>
Object.create(response, {
isEmpty: {
get() {
return (response.data?.items?.length || 0) <= 0
},
enumerable: true,
},
}),
[response]
)
}, },
} }

View File

@ -1,78 +1,4 @@
import { HookFetcher } from '@commerce/utils/types' import useWishlist, { UseWishlist } from '@commerce/wishlist/use-wishlist'
import { SwrOptions } from '@commerce/utils/use-data' import type { BigcommerceProvider } from '..'
import useResponse from '@commerce/utils/use-response'
import useCommerceWishlist from '@commerce/wishlist/use-wishlist'
import type { Wishlist } from '../api/wishlist'
import useCustomer from '../customer/use-customer'
const defaultOpts = { export default useWishlist as UseWishlist<BigcommerceProvider>
url: '/api/bigcommerce/wishlist',
method: 'GET',
}
export type { Wishlist }
export interface UseWishlistOptions {
includeProducts?: boolean
}
export interface UseWishlistInput extends UseWishlistOptions {
customerId?: number
}
export const fetcher: HookFetcher<Wishlist | null, UseWishlistInput> = (
options,
{ customerId, includeProducts },
fetch
) => {
if (!customerId) return null
// Use a dummy base as we only care about the relative path
const url = new URL(options?.url ?? defaultOpts.url, 'http://a')
if (includeProducts) url.searchParams.set('products', '1')
return fetch({
url: url.pathname + url.search,
method: options?.method ?? defaultOpts.method,
})
}
export function extendHook(
customFetcher: typeof fetcher,
swrOptions?: SwrOptions<Wishlist | null, UseWishlistInput>
) {
const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => {
const { data: customer } = useCustomer()
const response = useCommerceWishlist(
defaultOpts,
[
['customerId', customer?.id],
['includeProducts', includeProducts],
],
customFetcher,
{
revalidateOnFocus: false,
...swrOptions,
}
)
const res = useResponse(response, {
descriptors: {
isEmpty: {
get() {
return (response.data?.items?.length || 0) <= 0
},
set: (x) => x,
},
},
})
return res
}
useWishlist.extend = extendHook
return useWishlist
}
export default extendHook(fetcher)

View File

@ -1,7 +1,11 @@
import { useMemo } from 'react'
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
import type { Cart } from '../types' import type { Cart } from '../types'
import type { HookFetcherFn } from '../utils/types' import type {
Prop,
HookFetcherFn,
UseHookInput,
UseHookResponse,
} from '../utils/types'
import useData from '../utils/use-data-2' import useData from '../utils/use-data-2'
import { Provider, useCommerce } from '..' import { Provider, useCommerce } from '..'
@ -9,18 +13,23 @@ export type FetchCartInput = {
cartId?: Cart['id'] cartId?: Cart['id']
} }
export type CartResponse<P extends Provider> = ReturnType< export type UseCartHandler<P extends Provider> = Prop<
NonNullable<NonNullable<NonNullable<P['cart']>['useCart']>['onResponse']> Prop<P, 'cart'>,
'useCart'
> >
export type UseCart<P extends Provider> = ( export type UseCartInput<P extends Provider> = UseHookInput<UseCartHandler<P>>
...input: UseCartInput<P>
) => CartResponse<P>
export type UseCartInput<P extends Provider> = NonNullable< export type CartResponse<P extends Provider> = UseHookResponse<
NonNullable<NonNullable<NonNullable<P['cart']>['useCart']>>['input'] UseCartHandler<P>
> >
export type UseCart<P extends Provider> = Partial<
UseCartInput<P>
> extends UseCartInput<P>
? (input?: UseCartInput<P>) => CartResponse<P>
: (input: UseCartInput<P>) => CartResponse<P>
export const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({ export const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({
options, options,
input: { cartId }, input: { cartId },
@ -31,25 +40,32 @@ export const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({
return data && normalize ? normalize(data) : data return data && normalize ? normalize(data) : data
} }
export default function useCart<P extends Provider>(...input: UseCartInput<P>) { export default function useCart<P extends Provider>(
input: UseCartInput<P> = {}
) {
const { providerRef, fetcherRef, cartCookie } = useCommerce<P>() const { providerRef, fetcherRef, cartCookie } = useCommerce<P>()
const provider = providerRef.current const provider = providerRef.current
const opts = provider.cart?.useCart const opts = provider.cart?.useCart
const fetcherFn = opts?.fetcher ?? fetcher const fetcherFn = opts?.fetcher ?? fetcher
const useHook = opts?.useHook ?? ((ctx) => ctx.useData())
const wrapper: typeof fetcher = (context) => { const wrapper: typeof fetcher = (context) => {
context.input.cartId = Cookies.get(cartCookie) context.input.cartId = Cookies.get(cartCookie)
return fetcherFn(context) return fetcherFn(context)
} }
const response = useData(
{ ...opts, fetcher: wrapper },
input,
provider.fetcher ?? fetcherRef.current
)
const memoizedResponse = useMemo(
() => (opts?.onResponse ? opts.onResponse(response) : response),
[response]
)
return memoizedResponse as CartResponse<P> return useHook({
input,
useData(ctx) {
const response = useData(
{ ...opts!, fetcher: wrapper },
ctx?.input ?? [],
provider.fetcher ?? fetcherRef.current,
ctx?.swrOptions ?? input.swrOptions
)
return response
},
})
} }

View File

@ -8,18 +8,18 @@ import {
} from 'react' } from 'react'
import * as React from 'react' import * as React from 'react'
import { Fetcher, HookHandler } from './utils/types' import { Fetcher, HookHandler } from './utils/types'
import { Cart } from './types'
import type { FetchCartInput } from './cart/use-cart' import type { FetchCartInput } from './cart/use-cart'
import type { Cart, Wishlist } from './types'
const Commerce = createContext<CommerceContextValue<any> | {}>({}) const Commerce = createContext<CommerceContextValue<any> | {}>({})
export type Provider = CommerceConfig & { export type Provider = CommerceConfig & {
fetcher: Fetcher fetcher: Fetcher
cart?: { cart?: {
useCart?: HookHandler<Cart | null, [...any], FetchCartInput> useCart?: HookHandler<Cart | null, any, FetchCartInput>
} }
wishlist?: { wishlist?: {
useWishlist?: HookHandler<Cart | null, [...any], FetchCartInput> useWishlist?: HookHandler<Wishlist | null, any, any>
} }
} }

View File

@ -1,3 +1,5 @@
import type { Wishlist as BCWishlist } from '@framework/api/wishlist'
export interface Discount { export interface Discount {
// The value of the discount, can be an amount or percentage // The value of the discount, can be an amount or percentage
value: number value: number
@ -87,6 +89,9 @@ export interface Cart {
discounts?: Discount[] discounts?: Discount[]
} }
// TODO: Properly define this type
export interface Wishlist extends BCWishlist {}
/** /**
* Cart mutations * Cart mutations
*/ */

View File

@ -4,11 +4,15 @@ import type { ResponseState } from './use-data'
export type Override<T, K> = Omit<T, keyof K> & K export type Override<T, K> = Omit<T, keyof K> & K
// Returns the properties in T with the properties in type K changed from optional to required /**
* Returns the properties in T with the properties in type K changed from optional to required
*/
export type PickRequired<T, K extends keyof T> = Omit<T, K> & export type PickRequired<T, K extends keyof T> = Omit<T, K> &
Required<Pick<T, K>> Required<Pick<T, K>>
// Core fetcher added by CommerceProvider /**
* Core fetcher added by CommerceProvider
*/
export type Fetcher<T = any, B = any> = ( export type Fetcher<T = any, B = any> = (
options: FetcherOptions<B> options: FetcherOptions<B>
) => T | Promise<T> ) => T | Promise<T>
@ -29,35 +33,36 @@ export type HookFetcher<Data, Input = null, Result = any> = (
export type HookFetcherFn< export type HookFetcherFn<
Data, Data,
Input = unknown, Input = never,
Result = any, Result = any,
Body = any Body = any
> = (context: { > = (context: {
options: HookFetcherOptions | null options: HookFetcherOptions
input: Input input: Input
fetch: <T = Result, B = Body>(options: FetcherOptions<B>) => Promise<T> fetch: <T = Result, B = Body>(options: FetcherOptions<B>) => Promise<T>
normalize?(data: Result): Data normalize?(data: Result): Data
}) => Data | Promise<Data> }) => Data | Promise<Data>
export type HookFetcherOptions = { export type HookFetcherOptions = { method?: string } & (
query?: string | { query: string; url?: string }
url?: string | { query?: string; url: string }
method?: string )
}
export type HookInputValue = string | number | boolean | undefined export type HookInputValue = string | number | boolean | undefined
export type HookInput = [string, HookInputValue][] export type HookSwrInput = [string, HookInputValue][]
export type HookFetchInput = { [k: string]: HookInputValue } export type HookFetchInput = { [k: string]: HookInputValue }
export type HookInput = {}
export type HookHandler< export type HookHandler<
// Data obj returned by the hook and fetch operation // Data obj returned by the hook and fetch operation
Data, Data,
// Input expected by the hook // Input expected by the hook
Input = [...any], Input extends { [k: string]: unknown } = {},
// Input expected before doing a fetch operation // Input expected before doing a fetch operation
FetchInput extends HookFetchInput = never, FetchInput extends HookFetchInput = {},
// Data returned by the API after a fetch operation // Data returned by the API after a fetch operation
Result = any, Result = any,
// Body expected by the API endpoint // Body expected by the API endpoint
@ -65,11 +70,14 @@ export type HookHandler<
// Custom state added to the response object of SWR // Custom state added to the response object of SWR
State = {} State = {}
> = { > = {
input?: Input useHook?(context: {
swrOptions?: SwrOptions<Data, FetchInput, Result> input: Input & { swrOptions?: SwrOptions<Data, FetchInput, Result> }
onResponse?(response: ResponseState<Data>): ResponseState<Data> & State useData(context?: {
onMutation?: any input?: HookFetchInput | HookSwrInput
fetchOptions?: HookFetcherOptions swrOptions?: SwrOptions<Data, FetchInput, Result>
}): ResponseState<Data>
}): ResponseState<Data> & State
fetchOptions: HookFetcherOptions
fetcher?: HookFetcherFn<Data, FetchInput, Result, Body> fetcher?: HookFetcherFn<Data, FetchInput, Result, Body>
normalizer?(data: Result): Data normalizer?(data: Result): Data
} }
@ -79,3 +87,20 @@ export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
CommerceError, CommerceError,
HookFetcher<Data, Input, Result> HookFetcher<Data, Input, Result>
> >
/**
* Returns the property K from type T excluding nullables
*/
export type Prop<T, K extends keyof T> = NonNullable<T[K]>
export type UseHookParameters<H extends HookHandler<any>> = Parameters<
Prop<H, 'useHook'>
>
export type UseHookResponse<H extends HookHandler<any>> = ReturnType<
Prop<H, 'useHook'>
>
export type UseHookInput<
H extends HookHandler<any>
> = UseHookParameters<H>[0]['input']

View File

@ -1,10 +1,11 @@
import useSWR, { responseInterface } from 'swr' import useSWR, { responseInterface } from 'swr'
import type { import type {
HookHandler, HookHandler,
HookInput, HookSwrInput,
HookFetchInput, HookFetchInput,
PickRequired, PickRequired,
Fetcher, Fetcher,
SwrOptions,
} from './types' } from './types'
import defineProperty from './define-property' import defineProperty from './define-property'
import { CommerceError } from './errors' import { CommerceError } from './errors'
@ -15,8 +16,8 @@ export type ResponseState<Result> = responseInterface<Result, CommerceError> & {
export type UseData = < export type UseData = <
Data = any, Data = any,
Input = [...any], Input extends { [k: string]: unknown } = {},
FetchInput extends HookFetchInput = never, FetchInput extends HookFetchInput = {},
Result = any, Result = any,
Body = any Body = any
>( >(
@ -24,13 +25,15 @@ export type UseData = <
HookHandler<Data, Input, FetchInput, Result, Body>, HookHandler<Data, Input, FetchInput, Result, Body>,
'fetcher' 'fetcher'
>, >,
input: HookInput, input: HookFetchInput | HookSwrInput,
fetcherFn: Fetcher fetcherFn: Fetcher,
swrOptions?: SwrOptions<Data, FetchInput, Result>
) => ResponseState<Data> ) => ResponseState<Data>
const useData: UseData = (options, input, fetcherFn) => { const useData: UseData = (options, input, fetcherFn, swrOptions) => {
const hookInput = Array.isArray(input) ? input : Object.entries(input)
const fetcher = async ( const fetcher = async (
url?: string, url: string,
query?: string, query?: string,
method?: string, method?: string,
...args: any[] ...args: any[]
@ -40,7 +43,7 @@ const useData: UseData = (options, input, fetcherFn) => {
options: { url, query, method }, options: { url, query, method },
// Transform the input array into an object // Transform the input array into an object
input: args.reduce((obj, val, i) => { input: args.reduce((obj, val, i) => {
obj[input[i][0]!] = val obj[hookInput[i][0]!] = val
return obj return obj
}, {}), }, {}),
fetch: fetcherFn, fetch: fetcherFn,
@ -59,11 +62,11 @@ const useData: UseData = (options, input, fetcherFn) => {
() => { () => {
const opts = options.fetchOptions const opts = options.fetchOptions
return opts return opts
? [opts.url, opts.query, opts.method, ...input.map((e) => e[1])] ? [opts.url, opts.query, opts.method, ...hookInput.map((e) => e[1])]
: null : null
}, },
fetcher, fetcher,
options.swrOptions swrOptions
) )
if (!('isLoading' in response)) { if (!('isLoading' in response)) {

View File

@ -1,5 +1,5 @@
import useSWR, { ConfigInterface, responseInterface } from 'swr' import useSWR, { ConfigInterface, responseInterface } from 'swr'
import type { HookInput, HookFetcher, HookFetcherOptions } from './types' import type { HookSwrInput, HookFetcher, HookFetcherOptions } from './types'
import defineProperty from './define-property' import defineProperty from './define-property'
import { CommerceError } from './errors' import { CommerceError } from './errors'
import { useCommerce } from '..' import { useCommerce } from '..'
@ -16,7 +16,7 @@ export type ResponseState<Result> = responseInterface<Result, CommerceError> & {
export type UseData = <Data = any, Input = null, Result = any>( export type UseData = <Data = any, Input = null, Result = any>(
options: HookFetcherOptions | (() => HookFetcherOptions | null), options: HookFetcherOptions | (() => HookFetcherOptions | null),
input: HookInput, input: HookSwrInput,
fetcherFn: HookFetcher<Data, Input, Result>, fetcherFn: HookFetcher<Data, Input, Result>,
swrOptions?: SwrOptions<Data, Input, Result> swrOptions?: SwrOptions<Data, Input, Result>
) => ResponseState<Data> ) => ResponseState<Data>

View File

@ -1,16 +1,62 @@
import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types' import type { Wishlist } from '../types'
import useData, { ResponseState, SwrOptions } from '../utils/use-data' import type {
Prop,
HookFetcherFn,
UseHookInput,
UseHookResponse,
} from '../utils/types'
import useData from '../utils/use-data-2'
import { Provider, useCommerce } from '..'
export type WishlistResponse<Result> = ResponseState<Result> & { export type UseWishlistHandler<P extends Provider> = Prop<
isEmpty?: boolean Prop<P, 'wishlist'>,
'useWishlist'
>
export type UseWishlistInput<P extends Provider> = UseHookInput<
UseWishlistHandler<P>
>
export type WishlistResponse<P extends Provider> = UseHookResponse<
UseWishlistHandler<P>
>
export type UseWishlist<P extends Provider> = Partial<
WishlistResponse<P>
> extends WishlistResponse<P>
? (input?: WishlistResponse<P>) => WishlistResponse<P>
: (input: WishlistResponse<P>) => WishlistResponse<P>
export const fetcher: HookFetcherFn<Wishlist | null> = async ({
options,
fetch,
normalize,
}) => {
const data = await fetch({ ...options })
return data && normalize ? normalize(data) : data
} }
export default function useWishlist<Result, Input = null>( export default function useWishlist<P extends Provider>(
options: HookFetcherOptions, input: UseWishlistInput<P> = {}
input: HookInput, ) {
fetcherFn: HookFetcher<Result, Input>, const { providerRef, fetcherRef } = useCommerce<P>()
swrOptions?: SwrOptions<Result, Input>
): WishlistResponse<Result> { const provider = providerRef.current
const response = useData(options, input, fetcherFn, swrOptions) const opts = provider.wishlist?.useWishlist
return response
const fetcherFn = opts?.fetcher ?? fetcher
const useHook = opts?.useHook ?? ((ctx) => ctx.useData())
return useHook({
input,
useData(ctx) {
const response = useData(
{ ...opts!, fetcher: fetcherFn },
ctx?.input ?? [],
provider.fetcher ?? fetcherRef.current,
ctx?.swrOptions ?? input.swrOptions
)
return response
},
})
} }