mirror of
https://github.com/vercel/commerce.git
synced 2025-03-14 22:42:33 +00:00
Added better mutation types, and moved some hooks
This commit is contained in:
parent
c02d7fec62
commit
74da22d5f4
@ -33,7 +33,7 @@ const CartItem = ({
|
||||
currencyCode,
|
||||
})
|
||||
|
||||
const updateItem = useUpdateItem(item)
|
||||
const updateItem = useUpdateItem({ item })
|
||||
const removeItem = useRemoveItem()
|
||||
const [quantity, setQuantity] = useState(item.quantity)
|
||||
const [removing, setRemoving] = useState(false)
|
||||
|
@ -1,29 +1,24 @@
|
||||
import type { MutationHandler } from '@commerce/utils/types'
|
||||
import { useCallback } from 'react'
|
||||
import type { MutationHook } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
|
||||
import { normalizeCart } from '../lib/normalize'
|
||||
import type {
|
||||
AddCartItemBody,
|
||||
Cart,
|
||||
BigcommerceCart,
|
||||
CartItemBody,
|
||||
AddCartItemBody,
|
||||
} from '../types'
|
||||
import useCart from './use-cart'
|
||||
import { BigcommerceProvider } from '..'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'POST',
|
||||
}
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export default useAddItem as UseAddItem<BigcommerceProvider, CartItemBody>
|
||||
|
||||
export const handler: MutationHandler<Cart, {}, AddCartItemBody> = {
|
||||
export const handler: MutationHook<Cart, {}, CartItemBody> = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'GET',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({ input: { item }, options, fetch }) {
|
||||
async fetcher({ input: item, options, fetch }) {
|
||||
if (
|
||||
item.quantity &&
|
||||
(!Number.isInteger(item.quantity) || item.quantity! < 1)
|
||||
@ -34,20 +29,22 @@ export const handler: MutationHandler<Cart, {}, AddCartItemBody> = {
|
||||
}
|
||||
|
||||
const data = await fetch<BigcommerceCart, AddCartItemBody>({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { item },
|
||||
})
|
||||
|
||||
return normalizeCart(data)
|
||||
},
|
||||
useHook() {
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { mutate } = useCart()
|
||||
|
||||
return async function addItem({ input, fetch }) {
|
||||
const data = await fetch({ input })
|
||||
await mutate(data, false)
|
||||
return data
|
||||
}
|
||||
return useCallback(
|
||||
async function addItem(input) {
|
||||
const data = await fetch({ input })
|
||||
await mutate(data, false)
|
||||
return data
|
||||
},
|
||||
[fetch, mutate]
|
||||
)
|
||||
},
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { useCallback } from 'react'
|
||||
import debounce from 'lodash.debounce'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import type { HookContext, HookFetcherContext } from '@commerce/utils/types'
|
||||
import { ValidationError } from '@commerce/utils/errors'
|
||||
import useCartUpdateItem, {
|
||||
UpdateItemInput as UseUpdateItemInput,
|
||||
import useUpdateItem, {
|
||||
UpdateItemInput as UpdateItemInputBase,
|
||||
UseUpdateItem,
|
||||
} from '@commerce/cart/use-update-item'
|
||||
import { normalizeCart } from '../lib/normalize'
|
||||
import type {
|
||||
@ -15,49 +16,50 @@ import type {
|
||||
import { fetcher as removeFetcher } from './use-remove-item'
|
||||
import useCart from './use-cart'
|
||||
|
||||
const defaultOpts = {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'PUT',
|
||||
}
|
||||
|
||||
export type UpdateItemInput<T = any> = T extends LineItem
|
||||
? Partial<UseUpdateItemInput<LineItem>>
|
||||
: UseUpdateItemInput<LineItem>
|
||||
? Partial<UpdateItemInputBase<LineItem>>
|
||||
: UpdateItemInputBase<LineItem>
|
||||
|
||||
export const fetcher: HookFetcher<Cart | null, UpdateCartItemBody> = async (
|
||||
options,
|
||||
{ itemId, item },
|
||||
fetch
|
||||
) => {
|
||||
if (Number.isInteger(item.quantity)) {
|
||||
// Also allow the update hook to remove an item if the quantity is lower than 1
|
||||
if (item.quantity! < 1) {
|
||||
return removeFetcher(null, { itemId }, fetch)
|
||||
export default useUpdateItem as UseUpdateItem<typeof handler>
|
||||
|
||||
export const handler = {
|
||||
fetchOptions: {
|
||||
url: '/api/bigcommerce/cart',
|
||||
method: 'PUT',
|
||||
},
|
||||
async fetcher({
|
||||
input: { itemId, item },
|
||||
options,
|
||||
fetch,
|
||||
}: HookFetcherContext<UpdateCartItemBody>) {
|
||||
if (Number.isInteger(item.quantity)) {
|
||||
// Also allow the update hook to remove an item if the quantity is lower than 1
|
||||
if (item.quantity! < 1) {
|
||||
return removeFetcher(null, { itemId }, fetch)
|
||||
}
|
||||
} else if (item.quantity) {
|
||||
throw new ValidationError({
|
||||
message: 'The item quantity has to be a valid integer',
|
||||
})
|
||||
}
|
||||
} else if (item.quantity) {
|
||||
throw new ValidationError({
|
||||
message: 'The item quantity has to be a valid integer',
|
||||
|
||||
const data = await fetch<BigcommerceCart, UpdateCartItemBody>({
|
||||
...options,
|
||||
body: { itemId, item },
|
||||
})
|
||||
}
|
||||
|
||||
const data = await fetch<BigcommerceCart, UpdateCartItemBody>({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
body: { itemId, item },
|
||||
})
|
||||
|
||||
return normalizeCart(data)
|
||||
}
|
||||
|
||||
function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
|
||||
const useUpdateItem = <T extends LineItem | undefined = undefined>(
|
||||
item?: T
|
||||
return normalizeCart(data)
|
||||
},
|
||||
useHook: ({ fetch }: HookContext<Cart | null, UpdateCartItemBody>) => <
|
||||
T extends LineItem | undefined = undefined
|
||||
>(
|
||||
ctx: {
|
||||
item?: T
|
||||
wait?: number
|
||||
} = {}
|
||||
) => {
|
||||
const { mutate } = useCart()
|
||||
const fn = useCartUpdateItem<Cart | null, UpdateCartItemBody>(
|
||||
defaultOpts,
|
||||
customFetcher
|
||||
)
|
||||
const { item } = ctx
|
||||
const { mutate } = useCart() as any
|
||||
|
||||
return useCallback(
|
||||
debounce(async (input: UpdateItemInput<T>) => {
|
||||
@ -71,20 +73,16 @@ function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
|
||||
})
|
||||
}
|
||||
|
||||
const data = await fn({
|
||||
itemId,
|
||||
item: { productId, variantId, quantity: input.quantity },
|
||||
const data = await fetch({
|
||||
input: {
|
||||
itemId,
|
||||
item: { productId, variantId, quantity: input.quantity },
|
||||
},
|
||||
})
|
||||
await mutate(data, false)
|
||||
return data
|
||||
}, cfg?.wait ?? 500),
|
||||
[fn, mutate]
|
||||
}, ctx.wait ?? 500),
|
||||
[fetch, mutate]
|
||||
)
|
||||
}
|
||||
|
||||
useUpdateItem.extend = extendHook
|
||||
|
||||
return useUpdateItem
|
||||
},
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
||||
|
@ -1,5 +1,6 @@
|
||||
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 useWishlist } from './wishlist/use-wishlist'
|
||||
import { handler as useCustomer } from './customer/use-customer'
|
||||
import { handler as useSearch } from './product/use-search'
|
||||
@ -9,7 +10,7 @@ export const bigcommerceProvider = {
|
||||
locale: 'en-us',
|
||||
cartCookie: 'bc_cartId',
|
||||
fetcher,
|
||||
cart: { useCart, useAddItem },
|
||||
cart: { useCart, useAddItem, useUpdateItem },
|
||||
wishlist: { useWishlist },
|
||||
customer: { useCustomer },
|
||||
products: { useSearch },
|
||||
|
@ -43,9 +43,6 @@ export type CartItemBody = Core.CartItemBody & {
|
||||
optionSelections?: OptionSelections
|
||||
}
|
||||
|
||||
type X = Core.CartItemBody extends CartItemBody ? any : never
|
||||
type Y = CartItemBody extends Core.CartItemBody ? any : never
|
||||
|
||||
export type GetCartHandlerBody = Core.GetCartHandlerBody
|
||||
|
||||
export type AddCartItemBody = Core.AddCartItemBody<CartItemBody>
|
||||
|
@ -1,33 +1,7 @@
|
||||
import { useCallback } from 'react'
|
||||
import type {
|
||||
Prop,
|
||||
HookFetcherFn,
|
||||
UseHookInput,
|
||||
UseHookResponse,
|
||||
} from '../utils/types'
|
||||
import useHook, { useHookHandler } from '../utils/use-hook'
|
||||
import type { MutationHook, HookFetcherFn } from '../utils/types'
|
||||
import type { Cart, CartItemBody, AddCartItemBody } from '../types'
|
||||
import { Provider, useCommerce } from '..'
|
||||
import { BigcommerceProvider } from '@framework'
|
||||
|
||||
export type UseAddItemHandler<P extends Provider> = Prop<
|
||||
Prop<P, 'cart'>,
|
||||
'useAddItem'
|
||||
>
|
||||
|
||||
// Input expected by the action returned by the `useAddItem` hook
|
||||
export type UseAddItemInput<P extends Provider> = UseHookInput<
|
||||
UseAddItemHandler<P>
|
||||
>
|
||||
|
||||
export type UseAddItemResult<P extends Provider> = ReturnType<
|
||||
UseHookResponse<UseAddItemHandler<P>>
|
||||
>
|
||||
|
||||
export type UseAddItem<P extends Provider, Input> = Partial<
|
||||
UseAddItemInput<P>
|
||||
> extends UseAddItemInput<P>
|
||||
? (input?: UseAddItemInput<P>) => (input: Input) => UseAddItemResult<P>
|
||||
: (input: UseAddItemInput<P>) => (input: Input) => UseAddItemResult<P>
|
||||
import type { Provider } from '..'
|
||||
|
||||
export const fetcher: HookFetcherFn<
|
||||
Cart,
|
||||
@ -36,34 +10,15 @@ export const fetcher: HookFetcherFn<
|
||||
return fetch({ ...options, body: input })
|
||||
}
|
||||
|
||||
type X = UseAddItemResult<BigcommerceProvider>
|
||||
export type UseAddItem<
|
||||
H extends MutationHook<any, any, any> = MutationHook<Cart, {}, CartItemBody>
|
||||
> = ReturnType<H['useHook']>
|
||||
|
||||
export default function useAddItem<P extends Provider, Input>(
|
||||
input: UseAddItemInput<P>
|
||||
) {
|
||||
const { providerRef, fetcherRef } = useCommerce<P>()
|
||||
const fn = (provider: Provider) => provider.cart?.useAddItem!
|
||||
|
||||
const provider = providerRef.current
|
||||
const opts = provider.cart?.useAddItem
|
||||
|
||||
const fetcherFn = opts?.fetcher ?? fetcher
|
||||
const useHook = opts?.useHook ?? (() => () => {})
|
||||
const fetchFn = provider.fetcher ?? fetcherRef.current
|
||||
const action = useHook({ input })
|
||||
|
||||
return useCallback(
|
||||
function addItem(input: Input) {
|
||||
return action({
|
||||
input,
|
||||
fetch({ input }) {
|
||||
return fetcherFn({
|
||||
input,
|
||||
options: opts!.fetchOptions,
|
||||
fetch: fetchFn,
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
[input, fetchFn, opts?.fetchOptions]
|
||||
)
|
||||
const useAddItem: UseAddItem = (...args) => {
|
||||
const handler = useHookHandler(fn, fetcher)
|
||||
return handler(useHook(fn, fetcher))(...args)
|
||||
}
|
||||
|
||||
export default useAddItem
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { HookFetcher, HookFetcherOptions } from '../utils/types'
|
||||
import useAddItem from './use-add-item'
|
||||
// import useAddItem from './use-add-item'
|
||||
import useRemoveItem from './use-remove-item'
|
||||
import useUpdateItem from './use-update-item'
|
||||
|
||||
@ -9,9 +9,9 @@ export default function useCartActions<T, Input>(
|
||||
options: HookFetcherOptions,
|
||||
fetcher: HookFetcher<T, Input>
|
||||
) {
|
||||
const addItem = useAddItem<T, Input>(options, fetcher)
|
||||
// const addItem = useAddItem<T, Input>(options, fetcher)
|
||||
const updateItem = useUpdateItem<T, Input>(options, fetcher)
|
||||
const removeItem = useRemoveItem<T, Input>(options, fetcher)
|
||||
|
||||
return { addItem, updateItem, removeItem }
|
||||
return { updateItem, removeItem }
|
||||
}
|
||||
|
@ -1,11 +1,39 @@
|
||||
import useAction from '../utils/use-action'
|
||||
import type { CartItemBody } from '../types'
|
||||
import useHook, { useHookHandler } from '../utils/use-hook'
|
||||
import type { MutationHook, HookFetcherFn } from '../utils/types'
|
||||
import type { Cart, CartItemBody, LineItem, UpdateCartItemBody } from '../types'
|
||||
import type { Provider } from '..'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
// Input expected by the action returned by the `useUpdateItem` hook
|
||||
export type UpdateItemInput<T extends CartItemBody> = T & {
|
||||
id: string
|
||||
}
|
||||
|
||||
const useUpdateItem = useAction
|
||||
export type UseUpdateItem<
|
||||
H extends MutationHook<any, any, any> = MutationHook<
|
||||
Cart,
|
||||
{
|
||||
item?: LineItem
|
||||
wait?: number
|
||||
},
|
||||
UpdateItemInput<CartItemBody>,
|
||||
UpdateCartItemBody<CartItemBody>
|
||||
>
|
||||
> = ReturnType<H['useHook']>
|
||||
|
||||
export const fetcher: HookFetcherFn<any> = async ({
|
||||
options,
|
||||
input,
|
||||
fetch,
|
||||
}) => {
|
||||
return fetch({ ...options, body: input })
|
||||
}
|
||||
|
||||
const fn = (provider: Provider) => provider.cart?.useUpdateItem!
|
||||
|
||||
const useUpdateItem: UseUpdateItem = (input = {}) => {
|
||||
const handler = useHookHandler(fn, fetcher)
|
||||
return debounce(handler(useHook(fn, fetcher))(input), input.wait ?? 500)
|
||||
}
|
||||
|
||||
export default useUpdateItem
|
||||
|
@ -6,7 +6,12 @@ import {
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { Fetcher, HookHandler, MutationHandler } from './utils/types'
|
||||
import {
|
||||
Fetcher,
|
||||
HookHandler,
|
||||
MutationHandler,
|
||||
MutationHook,
|
||||
} from './utils/types'
|
||||
import type { FetchCartInput } from './cart/use-cart'
|
||||
import type { Cart, Wishlist, Customer, SearchProductsData } from './types'
|
||||
|
||||
@ -16,7 +21,9 @@ export type Provider = CommerceConfig & {
|
||||
fetcher: Fetcher
|
||||
cart?: {
|
||||
useCart?: HookHandler<Cart | null, any, FetchCartInput>
|
||||
useAddItem?: MutationHandler<Cart, any, any>
|
||||
useAddItem?: MutationHook<any, any, any>
|
||||
useUpdateItem?: MutationHook<any, any, any>
|
||||
useRemoveItem?: MutationHook<any, any, any>
|
||||
}
|
||||
wishlist?: {
|
||||
useWishlist?: HookHandler<Wishlist | null, any, any>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { LineItem } from '@framework/types'
|
||||
import type { ConfigInterface } from 'swr'
|
||||
import type { CommerceError } from './errors'
|
||||
import type { ResponseState } from './use-data'
|
||||
@ -31,16 +32,15 @@ export type HookFetcher<Data, Input = null, Result = any> = (
|
||||
fetch: <T = Result, Body = any>(options: FetcherOptions<Body>) => Promise<T>
|
||||
) => Data | Promise<Data>
|
||||
|
||||
export type HookFetcherFn<
|
||||
Data,
|
||||
Input = never,
|
||||
Result = any,
|
||||
Body = any
|
||||
> = (context: {
|
||||
export type HookFetcherFn<Data, Input = never, Result = any, Body = any> = (
|
||||
context: HookFetcherContext<Input, Result, Body>
|
||||
) => Data | Promise<Data>
|
||||
|
||||
export type HookFetcherContext<Input = never, Result = any, Body = any> = {
|
||||
options: HookFetcherOptions
|
||||
input: Input
|
||||
fetch: <T = Result, B = Body>(options: FetcherOptions<B>) => Promise<T>
|
||||
}) => Data | Promise<Data>
|
||||
}
|
||||
|
||||
export type HookFetcherOptions = { method?: string } & (
|
||||
| { query: string; url?: string }
|
||||
@ -53,8 +53,6 @@ export type HookSwrInput = [string, HookInputValue][]
|
||||
|
||||
export type HookFetchInput = { [k: string]: HookInputValue }
|
||||
|
||||
export type HookInput = {}
|
||||
|
||||
export type HookHandler<
|
||||
// Data obj returned by the hook and fetch operation
|
||||
Data,
|
||||
@ -94,6 +92,39 @@ export type MutationHandler<
|
||||
fetcher?: HookFetcherFn<Data, FetchInput>
|
||||
}
|
||||
|
||||
export type HookFunction<
|
||||
Input extends { [k: string]: unknown } | {},
|
||||
T
|
||||
> = keyof Input extends never
|
||||
? () => T
|
||||
: Partial<Input> extends Input
|
||||
? (input?: Input) => T
|
||||
: (input: Input) => T
|
||||
|
||||
export type MutationHook<
|
||||
// Data obj returned by the hook and fetch operation
|
||||
Data,
|
||||
// Input expected by the hook
|
||||
Input extends { [k: string]: unknown } = {},
|
||||
// Input expected by the action returned by the hook
|
||||
ActionInput extends { [k: string]: unknown } = {},
|
||||
// Input expected before doing a fetch operation
|
||||
FetchInput extends { [k: string]: unknown } = ActionInput
|
||||
> = {
|
||||
useHook(
|
||||
context: HookContext<Data, FetchInput>
|
||||
): HookFunction<Input, HookFunction<ActionInput, Data | Promise<Data>>>
|
||||
fetchOptions: HookFetcherOptions
|
||||
fetcher?: HookFetcherFn<Data, FetchInput>
|
||||
}
|
||||
|
||||
export type HookContext<
|
||||
Data,
|
||||
FetchInput extends { [k: string]: unknown } = {}
|
||||
> = {
|
||||
fetch: (context: { input: FetchInput }) => Data | Promise<Data>
|
||||
}
|
||||
|
||||
export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<
|
||||
Data,
|
||||
CommerceError,
|
||||
|
43
framework/commerce/utils/use-hook.ts
Normal file
43
framework/commerce/utils/use-hook.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { MutationHook } from './types'
|
||||
import { Provider, useCommerce } from '..'
|
||||
|
||||
export function useHookHandler<P extends Provider>(
|
||||
fn: (provider: P) => MutationHook<any, any, any>,
|
||||
fetcher: any
|
||||
) {
|
||||
const { providerRef } = useCommerce<P>()
|
||||
const provider = providerRef.current
|
||||
const opts = fn(provider)
|
||||
const handler =
|
||||
opts.useHook ??
|
||||
(() => {
|
||||
const { fetch } = useHook(fn, fetcher)
|
||||
return (input: any) => fetch({ input })
|
||||
})
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
export default function useHook<P extends Provider>(
|
||||
fn: (provider: P) => MutationHook<any, any, any>,
|
||||
fetcher: any
|
||||
) {
|
||||
const { providerRef, fetcherRef } = useCommerce<P>()
|
||||
const provider = providerRef.current
|
||||
const opts = fn(provider)
|
||||
const fetcherFn = opts.fetcher ?? fetcher
|
||||
const fetchFn = provider.fetcher ?? fetcherRef.current
|
||||
const fetch = useCallback(
|
||||
({ input }: { input: any }) => {
|
||||
return fetcherFn({
|
||||
input,
|
||||
options: opts.fetchOptions,
|
||||
fetch: fetchFn,
|
||||
})
|
||||
},
|
||||
[fetchFn, opts.fetchOptions]
|
||||
)
|
||||
|
||||
return { fetch }
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user