[WIP] Node.js provider for the API (#252)

* Adding multiple initial files

* Updated the default cart endpoint

* Fixes

* Updated CommerceAPI class for better usage

* Adding more migration changes

* Taking multiple steps into better API types

* Adding more experimental types

* Removed many testing types

* Adding types, fixes and other updates

* Updated commerce types

* Updated types for hooks now using the API

* Updated mutation types

* Simplified cart types for the provider

* Updated cart hooks

* Remove normalizers from the hooks

* Updated cart endpoint

* Removed cart handlers

* bug fixes

* Improve quantity input behavior in cart item

* Removed endpoints folder

* Making progress on api operations

* Moved method

* Moved types

* Changed the way ops are created

* Added customer endpoint

* Login endpoint

* Added logout endpoint

* Add missing logout files

* Added signup endpoint

* Removed customers old endpoints

* Moved endpoints to nested folder

* Removed old customer endpoint builders

* Updated login operation

* Updated login operation

* Added getAllPages operation

* Renamed endpoint operations to handlers

* Changed import

* Renamed operations to handlers in usage

* Moved getAllPages everywhere

* Moved getPage

* Updated getPage usage

* Moved getSiteInfo

* Added def types for product

* Updated type

* moved products catalog endpoint

* removed old catalog endpoint

* Moved wishlist

* Removed commerce.endpoint

* Replaced references to commerce.endpoint

* Updated catalog products

* Moved checkout api

* Added the get customer wishlist operation

* Removed old wishlist stuff

* Added getAllProductPaths operation

* updated reference to operation

* Moved getAllProducts

* Updated getProduct operation

* Removed old getConfig and references

* Removed is-allowed-method from BC

* Updated types for auth hooks

* Updated useCustomer and core types

* Updated useData and util hooks

* Updated useSearch hook

* Updated types for useWishlist

* Added index for types

* Fixes

* Updated urls to the API

* Renamed fetchInput to fetcherInput

* Updated fetch type

* Fixes in search hook

* Updated Shopify Provider Structure (#340)

* Add codegen, update fragments & schemas

* Update checkout-create.ts

* Update checkout-create.ts

* Update README.md

* Update product mutations & queries

* Uptate customer fetch types

* Update schemas

* Start updates

* Moved Page, AllPages & Site Info

* Moved product, all products (paths)

* Add translations, update operations & fixes

* Update api endpoints, types & fixes

* Add api checkout endpoint

* Updates

* Fixes

* Update commerce.config.json

Co-authored-by: B <curciobelen@gmail.com>

* Added category type and normalizer

* updated init script to exclude other providers

* Excluded swell and venture temporarily

* Fix category & color normalization

* Fixed category normalizer in shopify

* Don't use getSlug for category on /search

* Update colors.ts

Co-authored-by: cond0r <pinte_catalin@yahoo.com>
Co-authored-by: B <curciobelen@gmail.com>
This commit is contained in:
Luis Alvarez D
2021-06-01 03:18:10 -05:00
committed by GitHub
parent 0792eabd4c
commit a98c95d447
249 changed files with 4646 additions and 2981 deletions

View File

@@ -0,0 +1,62 @@
import type { CartSchema } from '../../types/cart'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..'
const cartEndpoint: GetAPISchema<
any,
CartSchema<any>
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers, config } = ctx
if (
!isAllowedOperation(req, res, {
GET: handlers['getCart'],
POST: handlers['addItem'],
PUT: handlers['updateItem'],
DELETE: handlers['removeItem'],
})
) {
return
}
const { cookies } = req
const cartId = cookies[config.cartCookie]
try {
// Return current cart info
if (req.method === 'GET') {
const body = { cartId }
return await handlers['getCart']({ ...ctx, body })
}
// Create or add an item to the cart
if (req.method === 'POST') {
const body = { ...req.body, cartId }
return await handlers['addItem']({ ...ctx, body })
}
// Update item in cart
if (req.method === 'PUT') {
const body = { ...req.body, cartId }
return await handlers['updateItem']({ ...ctx, body })
}
// Remove an item from the cart
if (req.method === 'DELETE') {
const body = { ...req.body, cartId }
return await handlers['removeItem']({ ...ctx, body })
}
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default cartEndpoint

View File

@@ -0,0 +1,31 @@
import type { ProductsSchema } from '../../../types/product'
import { CommerceAPIError } from '../../utils/errors'
import isAllowedOperation from '../../utils/is-allowed-operation'
import type { GetAPISchema } from '../..'
const productsEndpoint: GetAPISchema<
any,
ProductsSchema
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers } = ctx
if (!isAllowedOperation(req, res, { GET: handlers['getProducts'] })) {
return
}
try {
const body = req.query
return await handlers['getProducts']({ ...ctx, body })
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default productsEndpoint

View File

@@ -0,0 +1,35 @@
import type { CheckoutSchema } from '../../types/checkout'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..'
const checkoutEndpoint: GetAPISchema<
any,
CheckoutSchema
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers } = ctx
if (
!isAllowedOperation(req, res, {
GET: handlers['checkout'],
})
) {
return
}
try {
const body = null
return await handlers['checkout']({ ...ctx, body })
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default checkoutEndpoint

View File

@@ -0,0 +1,35 @@
import type { CustomerSchema } from '../../types/customer'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..'
const customerEndpoint: GetAPISchema<
any,
CustomerSchema<any>
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers } = ctx
if (
!isAllowedOperation(req, res, {
GET: handlers['getLoggedInCustomer'],
})
) {
return
}
try {
const body = null
return await handlers['getLoggedInCustomer']({ ...ctx, body })
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default customerEndpoint

View File

@@ -0,0 +1,35 @@
import type { LoginSchema } from '../../types/login'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..'
const loginEndpoint: GetAPISchema<
any,
LoginSchema<any>
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers } = ctx
if (
!isAllowedOperation(req, res, {
POST: handlers['login'],
})
) {
return
}
try {
const body = req.body ?? {}
return await handlers['login']({ ...ctx, body })
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default loginEndpoint

View File

@@ -0,0 +1,37 @@
import type { LogoutSchema } from '../../types/logout'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..'
const logoutEndpoint: GetAPISchema<
any,
LogoutSchema
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers } = ctx
if (
!isAllowedOperation(req, res, {
GET: handlers['logout'],
})
) {
return
}
try {
const redirectTo = req.query.redirect_to
const body = typeof redirectTo === 'string' ? { redirectTo } : {}
return await handlers['logout']({ ...ctx, body })
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default logoutEndpoint

View File

@@ -0,0 +1,38 @@
import type { SignupSchema } from '../../types/signup'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..'
const signupEndpoint: GetAPISchema<
any,
SignupSchema
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers, config } = ctx
if (
!isAllowedOperation(req, res, {
POST: handlers['signup'],
})
) {
return
}
const { cookies } = req
const cartId = cookies[config.cartCookie]
try {
const body = { ...req.body, cartId }
return await handlers['signup']({ ...ctx, body })
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default signupEndpoint

View File

@@ -0,0 +1,58 @@
import type { WishlistSchema } from '../../types/wishlist'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..'
const wishlistEndpoint: GetAPISchema<
any,
WishlistSchema<any>
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers, config } = ctx
if (
!isAllowedOperation(req, res, {
GET: handlers['getWishlist'],
POST: handlers['addItem'],
DELETE: handlers['removeItem'],
})
) {
return
}
const { cookies } = req
const customerToken = cookies[config.customerCookie]
try {
// Return current wishlist info
if (req.method === 'GET') {
const body = {
customerToken,
includeProducts: req.query.products === '1',
}
return await handlers['getWishlist']({ ...ctx, body })
}
// Add an item to the wishlist
if (req.method === 'POST') {
const body = { ...req.body, customerToken }
return await handlers['addItem']({ ...ctx, body })
}
// Remove an item from the wishlist
if (req.method === 'DELETE') {
const body = { ...req.body, customerToken }
return await handlers['removeItem']({ ...ctx, body })
}
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default wishlistEndpoint

View File

@@ -1,7 +1,154 @@
import type { NextApiHandler } from 'next'
import type { RequestInit, Response } from '@vercel/fetch'
import type { APIEndpoint, APIHandler } from './utils/types'
import type { CartSchema } from '../types/cart'
import type { CustomerSchema } from '../types/customer'
import type { LoginSchema } from '../types/login'
import type { LogoutSchema } from '../types/logout'
import type { SignupSchema } from '../types/signup'
import type { ProductsSchema } from '../types/product'
import type { WishlistSchema } from '../types/wishlist'
import type { CheckoutSchema } from '../types/checkout'
import {
defaultOperations,
OPERATIONS,
AllOperations,
APIOperations,
} from './operations'
export type APISchemas =
| CartSchema
| CustomerSchema
| LoginSchema
| LogoutSchema
| SignupSchema
| ProductsSchema
| WishlistSchema
| CheckoutSchema
export type GetAPISchema<
C extends CommerceAPI<any>,
S extends APISchemas = APISchemas
> = {
schema: S
endpoint: EndpointContext<C, S['endpoint']>
}
export type EndpointContext<
C extends CommerceAPI,
E extends EndpointSchemaBase
> = {
handler: Endpoint<C, E>
handlers: EndpointHandlers<C, E>
}
export type EndpointSchemaBase = {
options: {}
handlers: {
[k: string]: { data?: any; body?: any }
}
}
export type Endpoint<
C extends CommerceAPI,
E extends EndpointSchemaBase
> = APIEndpoint<C, EndpointHandlers<C, E>, any, E['options']>
export type EndpointHandlers<
C extends CommerceAPI,
E extends EndpointSchemaBase
> = {
[H in keyof E['handlers']]: APIHandler<
C,
EndpointHandlers<C, E>,
E['handlers'][H]['data'],
E['handlers'][H]['body'],
E['options']
>
}
export type APIProvider = {
config: CommerceAPIConfig
operations: APIOperations<any>
}
export type CommerceAPI<
P extends APIProvider = APIProvider
> = CommerceAPICore<P> & AllOperations<P>
export class CommerceAPICore<P extends APIProvider = APIProvider> {
constructor(readonly provider: P) {}
getConfig(userConfig: Partial<P['config']> = {}): P['config'] {
return Object.entries(userConfig).reduce(
(cfg, [key, value]) => Object.assign(cfg, { [key]: value }),
{ ...this.provider.config }
)
}
setConfig(newConfig: Partial<P['config']>) {
Object.assign(this.provider.config, newConfig)
}
}
export function getCommerceApi<P extends APIProvider>(
customProvider: P
): CommerceAPI<P> {
const commerce = Object.assign(
new CommerceAPICore(customProvider),
defaultOperations as AllOperations<P>
)
const ops = customProvider.operations
OPERATIONS.forEach((k) => {
const op = ops[k]
if (op) {
commerce[k] = op({ commerce }) as AllOperations<P>[typeof k]
}
})
return commerce
}
export function getEndpoint<
P extends APIProvider,
T extends GetAPISchema<any, any>
>(
commerce: CommerceAPI<P>,
context: T['endpoint'] & {
config?: P['config']
options?: T['schema']['endpoint']['options']
}
): NextApiHandler {
const cfg = commerce.getConfig(context.config)
return function apiHandler(req, res) {
return context.handler({
req,
res,
commerce,
config: cfg,
handlers: context.handlers,
options: context.options ?? {},
})
}
}
export const createEndpoint = <API extends GetAPISchema<any, any>>(
endpoint: API['endpoint']
) => <P extends APIProvider>(
commerce: CommerceAPI<P>,
context?: Partial<API['endpoint']> & {
config?: P['config']
options?: API['schema']['endpoint']['options']
}
): NextApiHandler => {
return getEndpoint(commerce, { ...endpoint, ...context })
}
export interface CommerceAPIConfig {
locale?: string
locales?: string[]
commerceUrl: string
apiToken: string
cartCookie: string

View File

@@ -0,0 +1,177 @@
import type { ServerResponse } from 'http'
import type { LoginOperation } from '../types/login'
import type { GetAllPagesOperation, GetPageOperation } from '../types/page'
import type { GetSiteInfoOperation } from '../types/site'
import type { GetCustomerWishlistOperation } from '../types/wishlist'
import type {
GetAllProductPathsOperation,
GetAllProductsOperation,
GetProductOperation,
} from '../types/product'
import type { APIProvider, CommerceAPI } from '.'
const noop = () => {
throw new Error('Not implemented')
}
export const OPERATIONS = [
'login',
'getAllPages',
'getPage',
'getSiteInfo',
'getCustomerWishlist',
'getAllProductPaths',
'getAllProducts',
'getProduct',
] as const
export const defaultOperations = OPERATIONS.reduce((ops, k) => {
ops[k] = noop
return ops
}, {} as { [K in AllowedOperations]: typeof noop })
export type AllowedOperations = typeof OPERATIONS[number]
export type Operations<P extends APIProvider> = {
login: {
<T extends LoginOperation>(opts: {
variables: T['variables']
config?: P['config']
res: ServerResponse
}): Promise<T['data']>
<T extends LoginOperation>(
opts: {
variables: T['variables']
config?: P['config']
res: ServerResponse
} & OperationOptions
): Promise<T['data']>
}
getAllPages: {
<T extends GetAllPagesOperation>(opts?: {
config?: P['config']
preview?: boolean
}): Promise<T['data']>
<T extends GetAllPagesOperation>(
opts: {
config?: P['config']
preview?: boolean
} & OperationOptions
): Promise<T['data']>
}
getPage: {
<T extends GetPageOperation>(opts: {
variables: T['variables']
config?: P['config']
preview?: boolean
}): Promise<T['data']>
<T extends GetPageOperation>(
opts: {
variables: T['variables']
config?: P['config']
preview?: boolean
} & OperationOptions
): Promise<T['data']>
}
getSiteInfo: {
<T extends GetSiteInfoOperation>(opts: {
config?: P['config']
preview?: boolean
}): Promise<T['data']>
<T extends GetSiteInfoOperation>(
opts: {
config?: P['config']
preview?: boolean
} & OperationOptions
): Promise<T['data']>
}
getCustomerWishlist: {
<T extends GetCustomerWishlistOperation>(opts: {
variables: T['variables']
config?: P['config']
includeProducts?: boolean
}): Promise<T['data']>
<T extends GetCustomerWishlistOperation>(
opts: {
variables: T['variables']
config?: P['config']
includeProducts?: boolean
} & OperationOptions
): Promise<T['data']>
}
getAllProductPaths: {
<T extends GetAllProductPathsOperation>(opts: {
variables?: T['variables']
config?: P['config']
}): Promise<T['data']>
<T extends GetAllProductPathsOperation>(
opts: {
variables?: T['variables']
config?: P['config']
} & OperationOptions
): Promise<T['data']>
}
getAllProducts: {
<T extends GetAllProductsOperation>(opts: {
variables?: T['variables']
config?: P['config']
preview?: boolean
}): Promise<T['data']>
<T extends GetAllProductsOperation>(
opts: {
variables?: T['variables']
config?: P['config']
preview?: boolean
} & OperationOptions
): Promise<T['data']>
}
getProduct: {
<T extends GetProductOperation>(opts: {
variables: T['variables']
config?: P['config']
preview?: boolean
}): Promise<T['data']>
<T extends GetProductOperation>(
opts: {
variables: T['variables']
config?: P['config']
preview?: boolean
} & OperationOptions
): Promise<T['data']>
}
}
export type APIOperations<P extends APIProvider> = {
[K in keyof Operations<P>]?: (ctx: OperationContext<P>) => Operations<P>[K]
}
export type AllOperations<P extends APIProvider> = {
[K in keyof APIOperations<P>]-?: P['operations'][K] extends (
...args: any
) => any
? ReturnType<P['operations'][K]>
: typeof noop
}
export type OperationContext<P extends APIProvider> = {
commerce: CommerceAPI<P>
}
export type OperationOptions =
| { query: string; url?: never }
| { query?: never; url: string }

View File

@@ -0,0 +1,22 @@
import type { Response } from '@vercel/fetch'
export class CommerceAPIError extends Error {
status: number
res: Response
data: any
constructor(msg: string, res: Response, data?: any) {
super(msg)
this.name = 'CommerceApiError'
this.status = res.status
this.res = res
this.data = data
}
}
export class CommerceNetworkError extends Error {
constructor(msg: string) {
super(msg)
this.name = 'CommerceNetworkError'
}
}

View File

@@ -0,0 +1,30 @@
import type { NextApiRequest, NextApiResponse } from 'next'
export type HTTP_METHODS = 'OPTIONS' | 'GET' | 'POST' | 'PUT' | 'DELETE'
export default function isAllowedMethod(
req: NextApiRequest,
res: NextApiResponse,
allowedMethods: HTTP_METHODS[]
) {
const methods = allowedMethods.includes('OPTIONS')
? allowedMethods
: [...allowedMethods, 'OPTIONS']
if (!req.method || !methods.includes(req.method)) {
res.status(405)
res.setHeader('Allow', methods.join(', '))
res.end()
return false
}
if (req.method === 'OPTIONS') {
res.status(200)
res.setHeader('Allow', methods.join(', '))
res.setHeader('Content-Length', '0')
res.end()
return false
}
return true
}

View File

@@ -0,0 +1,19 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import isAllowedMethod, { HTTP_METHODS } from './is-allowed-method'
import { APIHandler } from './types'
export default function isAllowedOperation(
req: NextApiRequest,
res: NextApiResponse,
allowedOperations: { [k in HTTP_METHODS]?: APIHandler<any, any> }
) {
const methods = Object.keys(allowedOperations) as HTTP_METHODS[]
const allowedMethods = methods.reduce<HTTP_METHODS[]>((arr, method) => {
if (allowedOperations[method]) {
arr.push(method)
}
return arr
}, [])
return isAllowedMethod(req, res, allowedMethods)
}

View File

@@ -0,0 +1,49 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import type { CommerceAPI } from '..'
export type ErrorData = { message: string; code?: string }
export type APIResponse<Data = any> =
| { data: Data; errors?: ErrorData[] }
// If `data` doesn't include `null`, then `null` is only allowed on errors
| (Data extends null
? { data: null; errors?: ErrorData[] }
: { data: null; errors: ErrorData[] })
export type APIHandlerContext<
C extends CommerceAPI,
H extends APIHandlers<C> = {},
Data = any,
Options extends {} = {}
> = {
req: NextApiRequest
res: NextApiResponse<APIResponse<Data>>
commerce: C
config: C['provider']['config']
handlers: H
/**
* Custom configs that may be used by a particular handler
*/
options: Options
}
export type APIHandler<
C extends CommerceAPI,
H extends APIHandlers<C> = {},
Data = any,
Body = any,
Options extends {} = {}
> = (
context: APIHandlerContext<C, H, Data, Options> & { body: Body }
) => void | Promise<void>
export type APIHandlers<C extends CommerceAPI> = {
[k: string]: APIHandler<C, any, any, any, any>
}
export type APIEndpoint<
C extends CommerceAPI = CommerceAPI,
H extends APIHandlers<C> = {},
Data = any,
Options extends {} = {}
> = (context: APIHandlerContext<C, H, Data, Options>) => void | Promise<void>

View File

@@ -1,13 +1,14 @@
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { MutationHook, HookFetcherFn } from '../utils/types'
import type { LoginHook } from '../types/login'
import type { Provider } from '..'
export type UseLogin<
H extends MutationHook<any, any, any> = MutationHook<null, {}, {}>
H extends MutationHook<LoginHook<any>> = MutationHook<LoginHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<null, {}> = mutationFetcher
export const fetcher: HookFetcherFn<LoginHook> = mutationFetcher
const fn = (provider: Provider) => provider.auth?.useLogin!

View File

@@ -1,13 +1,14 @@
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../utils/types'
import type { LogoutHook } from '../types/logout'
import type { Provider } from '..'
export type UseLogout<
H extends MutationHook<any, any, any> = MutationHook<null>
H extends MutationHook<LogoutHook<any>> = MutationHook<LogoutHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<null> = mutationFetcher
export const fetcher: HookFetcherFn<LogoutHook> = mutationFetcher
const fn = (provider: Provider) => provider.auth?.useLogout!

View File

@@ -1,13 +1,14 @@
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../utils/types'
import type { SignupHook } from '../types/signup'
import type { Provider } from '..'
export type UseSignup<
H extends MutationHook<any, any, any> = MutationHook<null>
H extends MutationHook<SignupHook<any>> = MutationHook<SignupHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<null> = mutationFetcher
export const fetcher: HookFetcherFn<SignupHook> = mutationFetcher
const fn = (provider: Provider) => provider.auth?.useSignup!

View File

@@ -1,17 +1,14 @@
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../utils/types'
import type { Cart, CartItemBody, AddCartItemBody } from '../types'
import type { AddItemHook } from '../types/cart'
import type { Provider } from '..'
export type UseAddItem<
H extends MutationHook<any, any, any> = MutationHook<Cart, {}, CartItemBody>
H extends MutationHook<AddItemHook<any>> = MutationHook<AddItemHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<
Cart,
AddCartItemBody<CartItemBody>
> = mutationFetcher
export const fetcher: HookFetcherFn<AddItemHook> = mutationFetcher
const fn = (provider: Provider) => provider.cart?.useAddItem!

View File

@@ -1,28 +1,19 @@
import Cookies from 'js-cookie'
import { useHook, useSWRHook } from '../utils/use-hook'
import type { HookFetcherFn, SWRHook } from '../utils/types'
import type { Cart } from '../types'
import type { SWRHook, HookFetcherFn } from '../utils/types'
import type { GetCartHook } from '../types/cart'
import { Provider, useCommerce } from '..'
export type FetchCartInput = {
cartId?: Cart['id']
}
export type UseCart<
H extends SWRHook<any, any, any> = SWRHook<
Cart | null,
{},
FetchCartInput,
{ isEmpty?: boolean }
>
H extends SWRHook<GetCartHook<any>> = SWRHook<GetCartHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<Cart | null, FetchCartInput> = async ({
export const fetcher: HookFetcherFn<GetCartHook> = async ({
options,
input: { cartId },
fetch,
}) => {
return cartId ? await fetch({ ...options }) : null
return cartId ? await fetch(options) : null
}
const fn = (provider: Provider) => provider.cart?.useCart!

View File

@@ -1,29 +1,14 @@
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../utils/types'
import type { Cart, LineItem, RemoveCartItemBody } from '../types'
import type { RemoveItemHook } from '../types/cart'
import type { Provider } from '..'
/**
* Input expected by the action returned by the `useRemoveItem` hook
*/
export type RemoveItemInput = {
id: string
}
export type UseRemoveItem<
H extends MutationHook<any, any, any> = MutationHook<
Cart | null,
{ item?: LineItem },
RemoveItemInput,
RemoveCartItemBody
>
H extends MutationHook<RemoveItemHook<any>> = MutationHook<RemoveItemHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<
Cart | null,
RemoveCartItemBody
> = mutationFetcher
export const fetcher: HookFetcherFn<RemoveItemHook> = mutationFetcher
const fn = (provider: Provider) => provider.cart?.useRemoveItem!

View File

@@ -1,32 +1,14 @@
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../utils/types'
import type { Cart, CartItemBody, LineItem, UpdateCartItemBody } from '../types'
import type { UpdateItemHook } from '../types/cart'
import type { Provider } from '..'
/**
* Input expected by the action returned by the `useUpdateItem` hook
*/
export type UpdateItemInput<T extends CartItemBody> = T & {
id: string
}
export type UseUpdateItem<
H extends MutationHook<any, any, any> = MutationHook<
Cart | null,
{
item?: LineItem
wait?: number
},
UpdateItemInput<CartItemBody>,
UpdateCartItemBody<CartItemBody>
>
H extends MutationHook<UpdateItemHook<any>> = MutationHook<UpdateItemHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<
Cart | null,
UpdateCartItemBody<CartItemBody>
> = mutationFetcher
export const fetcher: HookFetcherFn<UpdateItemHook> = mutationFetcher
const fn = (provider: Provider) => provider.cart?.useUpdateItem!

View File

@@ -56,6 +56,19 @@ function withCommerceConfig(nextConfig = {}) {
tsconfig.compilerOptions.paths['@framework'] = [`framework/${name}`]
tsconfig.compilerOptions.paths['@framework/*'] = [`framework/${name}/*`]
// When running for production it may be useful to exclude the other providers
// from TS checking
if (process.env.VERCEL) {
const exclude = tsconfig.exclude.filter(
(item) => !item.startsWith('framework/')
)
tsconfig.exclude = PROVIDERS.reduce((exclude, current) => {
if (current !== name) exclude.push(`framework/${current}`)
return exclude
}, exclude)
}
fs.writeFileSync(
tsconfigPath,
prettier.format(JSON.stringify(tsconfig), { parser: 'json' })

View File

@@ -1,14 +1,14 @@
import { useHook, useSWRHook } from '../utils/use-hook'
import { SWRFetcher } from '../utils/default-fetcher'
import type { CustomerHook } from '../types/customer'
import type { HookFetcherFn, SWRHook } from '../utils/types'
import type { Customer } from '../types'
import { Provider } from '..'
import type { Provider } from '..'
export type UseCustomer<
H extends SWRHook<any, any, any> = SWRHook<Customer | null>
H extends SWRHook<CustomerHook<any>> = SWRHook<CustomerHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<Customer | null, any> = SWRFetcher
export const fetcher: HookFetcherFn<CustomerHook> = SWRFetcher
const fn = (provider: Provider) => provider.customer?.useCustomer!

View File

@@ -6,35 +6,44 @@ import {
useMemo,
useRef,
} from 'react'
import { Fetcher, SWRHook, MutationHook } from './utils/types'
import type { FetchCartInput } from './cart/use-cart'
import type { Cart, Wishlist, Customer, SearchProductsData } from './types'
import type {
Customer,
Wishlist,
Cart,
Product,
Signup,
Login,
Logout,
} from '@commerce/types'
import type { Fetcher, SWRHook, MutationHook } from './utils/types'
const Commerce = createContext<CommerceContextValue<any> | {}>({})
export type Provider = CommerceConfig & {
fetcher: Fetcher
cart?: {
useCart?: SWRHook<Cart | null, any, FetchCartInput>
useAddItem?: MutationHook<any, any, any>
useUpdateItem?: MutationHook<any, any, any>
useRemoveItem?: MutationHook<any, any, any>
useCart?: SWRHook<Cart.GetCartHook>
useAddItem?: MutationHook<Cart.AddItemHook>
useUpdateItem?: MutationHook<Cart.UpdateItemHook>
useRemoveItem?: MutationHook<Cart.RemoveItemHook>
}
wishlist?: {
useWishlist?: SWRHook<Wishlist | null, any, any>
useAddItem?: MutationHook<any, any, any>
useRemoveItem?: MutationHook<any, any, any>
useWishlist?: SWRHook<Wishlist.GetWishlistHook>
useAddItem?: MutationHook<Wishlist.AddItemHook>
useRemoveItem?: MutationHook<Wishlist.RemoveItemHook>
}
customer?: {
useCustomer?: SWRHook<Customer | null, any, any>
useCustomer?: SWRHook<Customer.CustomerHook>
}
products?: {
useSearch?: SWRHook<SearchProductsData, any, any>
useSearch?: SWRHook<Product.SearchProductsHook>
}
auth?: {
useSignup?: MutationHook<any, any, any>
useLogin?: MutationHook<any, any, any>
useLogout?: MutationHook<any, any, any>
useSignup?: MutationHook<Signup.SignupHook>
useLogin?: MutationHook<Login.LoginHook>
useLogout?: MutationHook<Logout.LogoutHook>
}
}

View File

@@ -149,7 +149,7 @@ export const handler: SWRHook<
{ isEmpty?: boolean }
> = {
fetchOptions: {
url: '/api/bigcommerce/cart',
url: '/api/cart',
method: 'GET',
},
async fetcher({ input: { cartId }, options, fetch }) {
@@ -197,7 +197,7 @@ export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<Cart, {}, CartItemBody> = {
fetchOptions: {
url: '/api/bigcommerce/cart',
url: '/api/cart',
method: 'POST',
},
async fetcher({ input: item, options, fetch }) {

View File

@@ -1,14 +1,14 @@
import { useHook, useSWRHook } from '../utils/use-hook'
import { SWRFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, SWRHook } from '../utils/types'
import type { SearchProductsData } from '../types'
import { Provider } from '..'
import type { SearchProductsHook } from '../types/product'
import type { Provider } from '..'
export type UseSearch<
H extends SWRHook<any, any, any> = SWRHook<SearchProductsData>
H extends SWRHook<SearchProductsHook<any>> = SWRHook<SearchProductsHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<SearchProductsData, any> = SWRFetcher
export const fetcher: HookFetcherFn<SearchProductsHook> = SWRFetcher
const fn = (provider: Provider) => provider.products?.useSearch!

View File

@@ -1,213 +0,0 @@
import type { Wishlist as BCWishlist } from '../bigcommerce/api/wishlist'
import type { Customer as BCCustomer } from '../bigcommerce/api/customers'
import type { SearchProductsData as BCSearchProductsData } from '../bigcommerce/api/catalog/products'
export type Discount = {
// The value of the discount, can be an amount or percentage
value: number
}
export type LineItem = {
id: string
variantId: string
productId: string
name: string
quantity: number
discounts: Discount[]
// A human-friendly unique string automatically generated from the products name
path: string
variant: ProductVariant
}
export type Measurement = {
value: number
unit: 'KILOGRAMS' | 'GRAMS' | 'POUNDS' | 'OUNCES'
}
export type Image = {
url: string
altText?: string
width?: number
height?: number
}
export type ProductVariant = {
id: string
// The SKU (stock keeping unit) associated with the product variant.
sku: string
// The product variants title, or the product's name.
name: string
// Whether a customer needs to provide a shipping address when placing
// an order for the product variant.
requiresShipping: boolean
// The product variants price after all discounts are applied.
price: number
// Product variants price, as quoted by the manufacturer/distributor.
listPrice: number
// Image associated with the product variant. Falls back to the product image
// if no image is available.
image?: Image
// Indicates whether this product variant is in stock.
isInStock?: boolean
// Indicates if the product variant is available for sale.
availableForSale?: boolean
// The variant's weight. If a weight was not explicitly specified on the
// variant this will be the product's weight.
weight?: Measurement
// The variant's height. If a height was not explicitly specified on the
// variant, this will be the product's height.
height?: Measurement
// The variant's width. If a width was not explicitly specified on the
// variant, this will be the product's width.
width?: Measurement
// The variant's depth. If a depth was not explicitly specified on the
// variant, this will be the product's depth.
depth?: Measurement
}
// Shopping cart, a.k.a Checkout
export type Cart = {
id: string
// ID of the customer to which the cart belongs.
customerId?: string
// The email assigned to this cart
email?: string
// The date and time when the cart was created.
createdAt: string
// The currency used for this cart
currency: { code: string }
// Specifies if taxes are included in the line items.
taxesIncluded: boolean
lineItems: LineItem[]
// The sum of all the prices of all the items in the cart.
// Duties, taxes, shipping and discounts excluded.
lineItemsSubtotalPrice: number
// Price of the cart before duties, shipping and taxes.
subtotalPrice: number
// The sum of all the prices of all the items in the cart.
// Duties, taxes and discounts included.
totalPrice: number
// Discounts that have been applied on the cart.
discounts?: Discount[]
}
// TODO: Properly define this type
export interface Wishlist extends BCWishlist {}
// TODO: Properly define this type
export interface Customer extends BCCustomer {}
// TODO: Properly define this type
export interface SearchProductsData extends BCSearchProductsData {}
/**
* Cart mutations
*/
// Base cart item body used for cart mutations
export type CartItemBody = {
variantId: string
productId?: string
quantity?: number
}
// Body used by the `getCart` operation handler
export type GetCartHandlerBody = {
cartId?: string
}
// Body used by the add item to cart operation
export type AddCartItemBody<T extends CartItemBody> = {
item: T
}
// Body expected by the add item to cart operation handler
export type AddCartItemHandlerBody<T extends CartItemBody> = Partial<
AddCartItemBody<T>
> & {
cartId?: string
}
// Body used by the update cart item operation
export type UpdateCartItemBody<T extends CartItemBody> = {
itemId: string
item: T
}
// Body expected by the update cart item operation handler
export type UpdateCartItemHandlerBody<T extends CartItemBody> = Partial<
UpdateCartItemBody<T>
> & {
cartId?: string
}
// Body used by the remove cart item operation
export type RemoveCartItemBody = {
itemId: string
}
// Body expected by the remove cart item operation handler
export type RemoveCartItemHandlerBody = Partial<RemoveCartItemBody> & {
cartId?: string
}
export type Category = {
id: string
name: string
slug: string
path: string
}
export type Page = any
/**
* Temporal types
*/
interface Entity {
id: string | number
[prop: string]: any
}
export interface Product extends Entity {
name: string
description: string
descriptionHtml?: string
slug?: string
path?: string
images: ProductImage[]
variants: ProductVariant2[]
price: ProductPrice
options: ProductOption[]
sku?: string
}
interface ProductOption extends Entity {
displayName: string
values: ProductOptionValues[]
}
interface ProductOptionValues {
label: string
hexColors?: string[]
}
interface ProductImage {
url: string
alt?: string
}
interface ProductVariant2 {
id: string | number
options: ProductOption[]
}
interface ProductPrice {
value: number
currencyCode: 'USD' | 'ARS' | string | undefined
retailPrice?: number
salePrice?: number
listPrice?: number
extendedSalePrice?: number
extendedListPrice?: number
}

View File

@@ -0,0 +1,179 @@
import type { Discount, Measurement, Image } from './common'
export type SelectedOption = {
// The option's id.
id?: string
// The product options name.
name: string
/// The product options value.
value: string
}
export type LineItem = {
id: string
variantId: string
productId: string
name: string
quantity: number
discounts: Discount[]
// A human-friendly unique string automatically generated from the products name
path: string
variant: ProductVariant
options?: SelectedOption[]
}
export type ProductVariant = {
id: string
// The SKU (stock keeping unit) associated with the product variant.
sku: string
// The product variants title, or the product's name.
name: string
// Whether a customer needs to provide a shipping address when placing
// an order for the product variant.
requiresShipping: boolean
// The product variants price after all discounts are applied.
price: number
// Product variants price, as quoted by the manufacturer/distributor.
listPrice: number
// Image associated with the product variant. Falls back to the product image
// if no image is available.
image?: Image
// Indicates whether this product variant is in stock.
isInStock?: boolean
// Indicates if the product variant is available for sale.
availableForSale?: boolean
// The variant's weight. If a weight was not explicitly specified on the
// variant this will be the product's weight.
weight?: Measurement
// The variant's height. If a height was not explicitly specified on the
// variant, this will be the product's height.
height?: Measurement
// The variant's width. If a width was not explicitly specified on the
// variant, this will be the product's width.
width?: Measurement
// The variant's depth. If a depth was not explicitly specified on the
// variant, this will be the product's depth.
depth?: Measurement
}
// Shopping cart, a.k.a Checkout
export type Cart = {
id: string
// ID of the customer to which the cart belongs.
customerId?: string
// The email assigned to this cart
email?: string
// The date and time when the cart was created.
createdAt: string
// The currency used for this cart
currency: { code: string }
// Specifies if taxes are included in the line items.
taxesIncluded: boolean
lineItems: LineItem[]
// The sum of all the prices of all the items in the cart.
// Duties, taxes, shipping and discounts excluded.
lineItemsSubtotalPrice: number
// Price of the cart before duties, shipping and taxes.
subtotalPrice: number
// The sum of all the prices of all the items in the cart.
// Duties, taxes and discounts included.
totalPrice: number
// Discounts that have been applied on the cart.
discounts?: Discount[]
}
/**
* Base cart item body used for cart mutations
*/
export type CartItemBody = {
variantId: string
productId?: string
quantity?: number
}
/**
* Hooks schema
*/
export type CartTypes = {
cart?: Cart
item: LineItem
itemBody: CartItemBody
}
export type CartHooks<T extends CartTypes = CartTypes> = {
getCart: GetCartHook<T>
addItem: AddItemHook<T>
updateItem: UpdateItemHook<T>
removeItem: RemoveItemHook<T>
}
export type GetCartHook<T extends CartTypes = CartTypes> = {
data: T['cart'] | null
input: {}
fetcherInput: { cartId?: string }
swrState: { isEmpty: boolean }
}
export type AddItemHook<T extends CartTypes = CartTypes> = {
data: T['cart']
input?: T['itemBody']
fetcherInput: T['itemBody']
body: { item: T['itemBody'] }
actionInput: T['itemBody']
}
export type UpdateItemHook<T extends CartTypes = CartTypes> = {
data: T['cart'] | null
input: { item?: T['item']; wait?: number }
fetcherInput: { itemId: string; item: T['itemBody'] }
body: { itemId: string; item: T['itemBody'] }
actionInput: T['itemBody'] & { id: string }
}
export type RemoveItemHook<T extends CartTypes = CartTypes> = {
data: T['cart'] | null
input: { item?: T['item'] }
fetcherInput: { itemId: string }
body: { itemId: string }
actionInput: { id: string }
}
/**
* API Schema
*/
export type CartSchema<T extends CartTypes = CartTypes> = {
endpoint: {
options: {}
handlers: CartHandlers<T>
}
}
export type CartHandlers<T extends CartTypes = CartTypes> = {
getCart: GetCartHandler<T>
addItem: AddItemHandler<T>
updateItem: UpdateItemHandler<T>
removeItem: RemoveItemHandler<T>
}
export type GetCartHandler<T extends CartTypes = CartTypes> = GetCartHook<T> & {
body: { cartId?: string }
}
export type AddItemHandler<T extends CartTypes = CartTypes> = AddItemHook<T> & {
body: { cartId: string }
}
export type UpdateItemHandler<
T extends CartTypes = CartTypes
> = UpdateItemHook<T> & {
data: T['cart']
body: { cartId: string }
}
export type RemoveItemHandler<
T extends CartTypes = CartTypes
> = RemoveItemHook<T> & {
body: { cartId: string }
}

View File

@@ -0,0 +1,10 @@
export type CheckoutSchema = {
endpoint: {
options: {}
handlers: {
checkout: {
data: null
}
}
}
}

View File

@@ -0,0 +1,16 @@
export type Discount = {
// The value of the discount, can be an amount or percentage
value: number
}
export type Measurement = {
value: number
unit: 'KILOGRAMS' | 'GRAMS' | 'POUNDS' | 'OUNCES'
}
export type Image = {
url: string
altText?: string
width?: number
height?: number
}

View File

@@ -0,0 +1,22 @@
// TODO: define this type
export type Customer = any
export type CustomerTypes = {
customer: Customer
}
export type CustomerHook<T extends CustomerTypes = CustomerTypes> = {
data: T['customer'] | null
fetchData: { customer: T['customer'] } | null
}
export type CustomerSchema<T extends CustomerTypes = CustomerTypes> = {
endpoint: {
options: {}
handlers: {
getLoggedInCustomer: {
data: { customer: T['customer'] } | null
}
}
}
}

View File

@@ -0,0 +1,25 @@
import * as Cart from './cart'
import * as Checkout from './checkout'
import * as Common from './common'
import * as Customer from './customer'
import * as Login from './login'
import * as Logout from './logout'
import * as Page from './page'
import * as Product from './product'
import * as Signup from './signup'
import * as Site from './site'
import * as Wishlist from './wishlist'
export type {
Cart,
Checkout,
Common,
Customer,
Login,
Logout,
Page,
Product,
Signup,
Site,
Wishlist,
}

View File

@@ -0,0 +1,29 @@
export type LoginBody = {
email: string
password: string
}
export type LoginTypes = {
body: LoginBody
}
export type LoginHook<T extends LoginTypes = LoginTypes> = {
data: null
actionInput: LoginBody
fetcherInput: LoginBody
body: T['body']
}
export type LoginSchema<T extends LoginTypes = LoginTypes> = {
endpoint: {
options: {}
handlers: {
login: LoginHook<T>
}
}
}
export type LoginOperation = {
data: { result?: string }
variables: unknown
}

View File

@@ -0,0 +1,17 @@
export type LogoutTypes = {
body: { redirectTo?: string }
}
export type LogoutHook<T extends LogoutTypes = LogoutTypes> = {
data: null
body: T['body']
}
export type LogoutSchema<T extends LogoutTypes = LogoutTypes> = {
endpoint: {
options: {}
handlers: {
logout: LogoutHook<T>
}
}
}

View File

@@ -0,0 +1,28 @@
// TODO: define this type
export type Page = {
// ID of the Web page.
id: string
// Page name, as displayed on the storefront.
name: string
// Relative URL on the storefront for this page.
url?: string
// HTML or variable that populates this pages `<body>` element, in default/desktop view. Required in POST if page type is `raw`.
body: string
// If true, this page appears in the storefronts navigation menu.
is_visible?: boolean
// Order in which this page should display on the storefront. (Lower integers specify earlier display.)
sort_order?: number
}
export type PageTypes = {
page: Page
}
export type GetAllPagesOperation<T extends PageTypes = PageTypes> = {
data: { pages: T['page'][] }
}
export type GetPageOperation<T extends PageTypes = PageTypes> = {
data: { page?: T['page'] }
variables: { id: string }
}

View File

@@ -0,0 +1,99 @@
export type ProductImage = {
url: string
alt?: string
}
export type ProductPrice = {
value: number
currencyCode?: 'USD' | 'ARS' | string
retailPrice?: number
salePrice?: number
listPrice?: number
extendedSalePrice?: number
extendedListPrice?: number
}
export type ProductOption = {
__typename?: 'MultipleChoiceOption'
id: string
displayName: string
values: ProductOptionValues[]
}
export type ProductOptionValues = {
label: string
hexColors?: string[]
}
export type ProductVariant = {
id: string | number
options: ProductOption[]
availableForSale?: boolean
}
export type Product = {
id: string
name: string
description: string
descriptionHtml?: string
sku?: string
slug?: string
path?: string
images: ProductImage[]
variants: ProductVariant[]
price: ProductPrice
options: ProductOption[]
}
export type SearchProductsBody = {
search?: string
categoryId?: string | number
brandId?: string | number
sort?: string
locale?: string
}
export type ProductTypes = {
product: Product
searchBody: SearchProductsBody
}
export type SearchProductsHook<T extends ProductTypes = ProductTypes> = {
data: {
products: T['product'][]
found: boolean
}
body: T['searchBody']
input: T['searchBody']
fetcherInput: T['searchBody']
}
export type ProductsSchema<T extends ProductTypes = ProductTypes> = {
endpoint: {
options: {}
handlers: {
getProducts: SearchProductsHook<T>
}
}
}
export type GetAllProductPathsOperation<
T extends ProductTypes = ProductTypes
> = {
data: { products: Pick<T['product'], 'path'>[] }
variables: { first?: number }
}
export type GetAllProductsOperation<T extends ProductTypes = ProductTypes> = {
data: { products: T['product'][] }
variables: {
relevance?: 'featured' | 'best_selling' | 'newest'
ids?: string[]
first?: number
}
}
export type GetProductOperation<T extends ProductTypes = ProductTypes> = {
data: { product?: T['product'] }
variables: { path: string; slug?: never } | { path?: never; slug: string }
}

View File

@@ -0,0 +1,26 @@
export type SignupBody = {
firstName: string
lastName: string
email: string
password: string
}
export type SignupTypes = {
body: SignupBody
}
export type SignupHook<T extends SignupTypes = SignupTypes> = {
data: null
body: T['body']
actionInput: T['body']
fetcherInput: T['body']
}
export type SignupSchema<T extends SignupTypes = SignupTypes> = {
endpoint: {
options: {}
handlers: {
signup: SignupHook<T>
}
}
}

View File

@@ -0,0 +1,20 @@
export type Category = {
id: string
name: string
slug: string
path: string
}
export type Brand = any
export type SiteTypes = {
category: Category
brand: Brand
}
export type GetSiteInfoOperation<T extends SiteTypes = SiteTypes> = {
data: {
categories: T['category'][]
brands: T['brand'][]
}
}

View File

@@ -0,0 +1,60 @@
// TODO: define this type
export type Wishlist = any
export type WishlistItemBody = {
variantId: string | number
productId: string
}
export type WishlistTypes = {
wishlist: Wishlist
itemBody: WishlistItemBody
}
export type GetWishlistHook<T extends WishlistTypes = WishlistTypes> = {
data: T['wishlist'] | null
body: { includeProducts?: boolean }
input: { includeProducts?: boolean }
fetcherInput: { customerId: string; includeProducts?: boolean }
swrState: { isEmpty: boolean }
}
export type AddItemHook<T extends WishlistTypes = WishlistTypes> = {
data: T['wishlist']
body: { item: T['itemBody'] }
fetcherInput: { item: T['itemBody'] }
actionInput: T['itemBody']
}
export type RemoveItemHook<T extends WishlistTypes = WishlistTypes> = {
data: T['wishlist'] | null
body: { itemId: string }
fetcherInput: { itemId: string }
actionInput: { id: string }
input: { wishlist?: { includeProducts?: boolean } }
}
export type WishlistSchema<T extends WishlistTypes = WishlistTypes> = {
endpoint: {
options: {}
handlers: {
getWishlist: GetWishlistHook<T> & {
data: T['wishlist'] | null
body: { customerToken?: string }
}
addItem: AddItemHook<T> & {
body: { customerToken?: string }
}
removeItem: RemoveItemHook<T> & {
body: { customerToken?: string }
}
}
}
}
export type GetCustomerWishlistOperation<
T extends WishlistTypes = WishlistTypes
> = {
data: { wishlist?: T['wishlist'] }
variables: { customerId: string }
}

View File

@@ -1,9 +1,9 @@
import type { HookFetcherFn } from './types'
export const SWRFetcher: HookFetcherFn<any, any> = ({ options, fetch }) =>
export const SWRFetcher: HookFetcherFn<any> = ({ options, fetch }) =>
fetch(options)
export const mutationFetcher: HookFetcherFn<any, any> = ({
export const mutationFetcher: HookFetcherFn<any> = ({
input,
options,
fetch,

View File

@@ -36,14 +36,19 @@ 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 = undefined, Result = any, Body = any> = (
context: HookFetcherContext<Input, Result, Body>
) => Data | Promise<Data>
export type HookFetcherFn<H extends HookSchemaBase> = (
context: HookFetcherContext<H>
) => H['data'] | Promise<H['data']>
export type HookFetcherContext<Input = undefined, Result = any, Body = any> = {
export type HookFetcherContext<H extends HookSchemaBase> = {
options: HookFetcherOptions
input: Input
fetch: <T = Result, B = Body>(options: FetcherOptions<B>) => Promise<T>
input: H['fetcherInput']
fetch: <
T = H['fetchData'] extends {} | null ? H['fetchData'] : any,
B = H['body']
>(
options: FetcherOptions<B>
) => Promise<T>
}
export type HookFetcherOptions = { method?: string } & (
@@ -58,7 +63,7 @@ export type HookSWRInput = [string, HookInputValue][]
export type HookFetchInput = { [k: string]: HookInputValue }
export type HookFunction<
Input extends { [k: string]: unknown } | null,
Input extends { [k: string]: unknown } | undefined,
T
> = keyof Input extends never
? () => T
@@ -66,62 +71,72 @@ export type HookFunction<
? (input?: Input) => T
: (input: Input) => T
export type SWRHook<
// Data obj returned by the hook and fetch operation
Data,
export type HookSchemaBase = {
// Data obj returned by the hook
data: any
// Input expected by the hook
Input extends { [k: string]: unknown } = {},
// Input expected before doing a fetch operation
FetchInput extends HookFetchInput = {},
input?: {}
// Input expected before doing a fetch operation (aka fetch handler)
fetcherInput?: {}
// Body object expected by the fetch operation
body?: {}
// Data returned by the fetch operation
fetchData?: any
}
export type SWRHookSchemaBase = HookSchemaBase & {
// Custom state added to the response object of SWR
State = {}
> = {
swrState?: {}
}
export type MutationSchemaBase = HookSchemaBase & {
// Input expected by the action returned by the hook
actionInput?: {}
}
/**
* Generates a SWR hook handler based on the schema of a hook
*/
export type SWRHook<H extends SWRHookSchemaBase> = {
useHook(
context: SWRHookContext<Data, FetchInput>
context: SWRHookContext<H>
): HookFunction<
Input & { swrOptions?: SwrOptions<Data, FetchInput> },
ResponseState<Data> & State
H['input'] & { swrOptions?: SwrOptions<H['data'], H['fetcherInput']> },
ResponseState<H['data']> & H['swrState']
>
fetchOptions: HookFetcherOptions
fetcher?: HookFetcherFn<Data, FetchInput>
fetcher?: HookFetcherFn<H>
}
export type SWRHookContext<
Data,
FetchInput extends { [k: string]: unknown } = {}
> = {
export type SWRHookContext<H extends SWRHookSchemaBase> = {
useData(context?: {
input?: HookFetchInput | HookSWRInput
swrOptions?: SwrOptions<Data, FetchInput>
}): ResponseState<Data>
swrOptions?: SwrOptions<H['data'], H['fetcherInput']>
}): ResponseState<H['data']>
}
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
> = {
/**
* Generates a mutation hook handler based on the schema of a hook
*/
export type MutationHook<H extends MutationSchemaBase> = {
useHook(
context: MutationHookContext<Data, FetchInput>
): HookFunction<Input, HookFunction<ActionInput, Data | Promise<Data>>>
context: MutationHookContext<H>
): HookFunction<
H['input'],
HookFunction<H['actionInput'], H['data'] | Promise<H['data']>>
>
fetchOptions: HookFetcherOptions
fetcher?: HookFetcherFn<Data, FetchInput>
fetcher?: HookFetcherFn<H>
}
export type MutationHookContext<
Data,
FetchInput extends { [k: string]: unknown } | null = {}
> = {
fetch: keyof FetchInput extends never
? () => Data | Promise<Data>
: Partial<FetchInput> extends FetchInput
? (context?: { input?: FetchInput }) => Data | Promise<Data>
: (context: { input: FetchInput }) => Data | Promise<Data>
export type MutationHookContext<H extends MutationSchemaBase> = {
fetch: keyof H['fetcherInput'] extends never
? () => H['data'] | Promise<H['data']>
: Partial<H['fetcherInput']> extends H['fetcherInput']
? (context?: {
input?: H['fetcherInput']
}) => H['data'] | Promise<H['data']>
: (context: { input: H['fetcherInput'] }) => H['data'] | Promise<H['data']>
}
export type SwrOptions<Data, Input = null, Result = any> = ConfigInterface<

View File

@@ -2,10 +2,11 @@ import useSWR, { responseInterface } from 'swr'
import type {
HookSWRInput,
HookFetchInput,
Fetcher,
SwrOptions,
HookFetcherOptions,
HookFetcherFn,
Fetcher,
SwrOptions,
SWRHookSchemaBase,
} from './types'
import defineProperty from './define-property'
import { CommerceError } from './errors'
@@ -14,15 +15,15 @@ export type ResponseState<Result> = responseInterface<Result, CommerceError> & {
isLoading: boolean
}
export type UseData = <Data = any, FetchInput extends HookFetchInput = {}>(
export type UseData = <H extends SWRHookSchemaBase>(
options: {
fetchOptions: HookFetcherOptions
fetcher: HookFetcherFn<Data, FetchInput>
fetcher: HookFetcherFn<H>
},
input: HookFetchInput | HookSWRInput,
fetcherFn: Fetcher,
swrOptions?: SwrOptions<Data, FetchInput>
) => ResponseState<Data>
swrOptions?: SwrOptions<H['data'], H['fetcherInput']>
) => ResponseState<H['data']>
const useData: UseData = (options, input, fetcherFn, swrOptions) => {
const hookInput = Array.isArray(input) ? input : Object.entries(input)

View File

@@ -10,14 +10,14 @@ export function useFetcher() {
export function useHook<
P extends Provider,
H extends MutationHook<any, any, any> | SWRHook<any, any, any>
H extends MutationHook<any> | SWRHook<any>
>(fn: (provider: P) => H) {
const { providerRef } = useCommerce<P>()
const provider = providerRef.current
return fn(provider)
}
export function useSWRHook<H extends SWRHook<any, any, any>>(
export function useSWRHook<H extends SWRHook<any>>(
hook: PickRequired<H, 'fetcher'>
) {
const fetcher = useFetcher()
@@ -30,7 +30,7 @@ export function useSWRHook<H extends SWRHook<any, any, any>>(
})
}
export function useMutationHook<H extends MutationHook<any, any, any>>(
export function useMutationHook<H extends MutationHook<any>>(
hook: PickRequired<H, 'fetcher'>
) {
const fetcher = useFetcher()

View File

@@ -1,10 +1,11 @@
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { MutationHook } from '../utils/types'
import type { AddItemHook } from '../types/wishlist'
import type { Provider } from '..'
export type UseAddItem<
H extends MutationHook<any, any, any> = MutationHook<any, {}, {}>
H extends MutationHook<AddItemHook<any>> = MutationHook<AddItemHook>
> = ReturnType<H['useHook']>
export const fetcher = mutationFetcher

View File

@@ -1,28 +1,20 @@
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../utils/types'
import type { RemoveItemHook } from '../types/wishlist'
import type { Provider } from '..'
export type RemoveItemInput = {
id: string | number
}
export type UseRemoveItem<
H extends MutationHook<any, any, any> = MutationHook<
any | null,
{ wishlist?: any },
RemoveItemInput,
{}
>
H extends MutationHook<RemoveItemHook<any>> = MutationHook<RemoveItemHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<any | null, {}> = mutationFetcher
export const fetcher: HookFetcherFn<RemoveItemHook> = mutationFetcher
const fn = (provider: Provider) => provider.wishlist?.useRemoveItem!
const useRemoveItem: UseRemoveItem = (input) => {
const useRemoveItem: UseRemoveItem = (...args) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(input)
return useMutationHook({ fetcher, ...hook })(...args)
}
export default useRemoveItem

View File

@@ -1,25 +1,20 @@
import { useHook, useSWRHook } from '../utils/use-hook'
import { SWRFetcher } from '../utils/default-fetcher'
import type { HookFetcherFn, SWRHook } from '../utils/types'
import type { Wishlist } from '../types'
import type { GetWishlistHook } from '../types/wishlist'
import type { Provider } from '..'
export type UseWishlist<
H extends SWRHook<any, any, any> = SWRHook<
Wishlist | null,
{ includeProducts?: boolean },
{ customerId?: number; includeProducts: boolean },
{ isEmpty?: boolean }
>
H extends SWRHook<GetWishlistHook<any>> = SWRHook<GetWishlistHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<Wishlist | null, any> = SWRFetcher
export const fetcher: HookFetcherFn<GetWishlistHook> = SWRFetcher
const fn = (provider: Provider) => provider.wishlist?.useWishlist!
const useWishlist: UseWishlist = (input) => {
const useWishlist: UseWishlist = (...args) => {
const hook = useHook(fn)
return useSWRHook({ fetcher, ...hook })(input)
return useSWRHook({ fetcher, ...hook })(...args)
}
export default useWishlist