Merge branch 'master' of github.com:okbel/e-comm-example

This commit is contained in:
Belen Curcio 2020-10-21 13:22:39 -03:00
commit b9ca1f275d
30 changed files with 432 additions and 66 deletions

View File

@ -8,7 +8,7 @@
},
"documents": [
{
"./lib/bigcommerce/api/{operations,fragments}/**/*.ts": {
"./lib/bigcommerce/api/**/*.ts": {
"noRequire": true
}
}

View File

@ -80,19 +80,19 @@ const cartApi: BigcommerceApiHandler<Cart, CartHandlers> = async (
// Create or add an item to the cart
if (req.method === 'POST') {
const body = { cartId, ...req.body }
const body = { ...req.body, cartId }
return await handlers['addItem']({ req, res, config, body })
}
// Update item in cart
if (req.method === 'PUT') {
const body = { cartId, ...req.body }
const body = { ...req.body, cartId }
return await handlers['updateItem']({ req, res, config, body })
}
// Remove an item from the cart
if (req.method === 'DELETE') {
const body = { cartId, ...req.body }
const body = { ...req.body, cartId }
return await handlers['removeItem']({ req, res, config, body })
}
} catch (error) {

View File

@ -0,0 +1,44 @@
import { GetLoggedInCustomerQuery } from '@lib/bigcommerce/schema'
import type { CustomersHandlers } from '..'
export const getLoggedInCustomerQuery = /* GraphQL */ `
query getLoggedInCustomer {
customer {
entityId
firstName
lastName
email
company
customerGroupId
notes
phone
addressCount
attributeCount
storeCredit {
value
currencyCode
}
}
}
`
const getLoggedInCustomer: CustomersHandlers['getLoggedInCustomer'] = async ({
res,
config,
}) => {
const data = await config.fetch<GetLoggedInCustomerQuery>(
getLoggedInCustomerQuery
)
const { customer } = data
if (!customer) {
return res.status(400).json({
data: null,
errors: [{ message: 'Customer not found', code: 'not_found' }],
})
}
res.status(200).json({ data: { customer } })
}
export default getLoggedInCustomer

View File

@ -0,0 +1,26 @@
import login from '../../operations/login'
import type { LoginHandlers } from '../login'
const loginHandler: LoginHandlers['login'] = async ({
res,
body: { email, password },
config,
}) => {
// TODO: Add proper validations with something like Ajv
if (!(email && password)) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
// TODO: validate the password and email
// Passwords must be at least 7 characters and contain both alphabetic
// and numeric characters.
// TODO: Currently not working, fix this asap.
const loginData = await login({ variables: { email, password }, config })
res.status(200).json({ data: null })
}
export default loginHandler

View File

@ -1,8 +1,8 @@
import { BigcommerceApiError } from '../../utils/errors'
import login from '../../operations/login'
import type { CustomersHandlers } from '..'
import { SignupHandlers } from '../signup'
const createCustomer: CustomersHandlers['createCustomer'] = async ({
const signup: SignupHandlers['signup'] = async ({
res,
body: { firstName, lastName, email, password },
config,
@ -57,6 +57,7 @@ const createCustomer: CustomersHandlers['createCustomer'] = async ({
console.log('DATA', result.data)
// TODO: Currently not working, fix this asap.
const loginData = await login({ variables: { email, password }, config })
console.log('LOGIN DATA', loginData)
@ -64,4 +65,4 @@ const createCustomer: CustomersHandlers['createCustomer'] = async ({
res.status(200).json({ data: result.data ?? null })
}
export default createCustomer
export default signup

View File

@ -4,42 +4,30 @@ import createApiHandler, {
} from '../utils/create-api-handler'
import isAllowedMethod from '../utils/is-allowed-method'
import { BigcommerceApiError } from '../utils/errors'
import createCustomer from './handlers/create-customer'
import getLoggedInCustomer from './handlers/get-logged-in-customer'
type Body<T> = Partial<T> | undefined
export type Customer = any
export type Customer = null
export type CreateCustomerBody = {
firstName: string
lastName: string
email: string
password: string
export type CustomerData = {
customer: Customer
}
export type CustomersHandlers = {
createCustomer: BigcommerceHandler<
Customer,
{ cartId?: string } & Body<CreateCustomerBody>
>
getLoggedInCustomer: BigcommerceHandler<CustomerData, null>
}
const METHODS = ['POST']
const METHODS = ['GET']
const customersApi: BigcommerceApiHandler<Customer, CustomersHandlers> = async (
req,
res,
config
) => {
const customersApi: BigcommerceApiHandler<
CustomerData,
CustomersHandlers
> = async (req, res, config, handlers) => {
if (!isAllowedMethod(req, res, METHODS)) return
const { cookies } = req
const cartId = cookies[config.cartCookie]
try {
if (req.method === 'POST') {
const body = { cartId, ...req.body }
return await handlers['createCustomer']({ req, res, config, body })
if (req.method === 'GET') {
const body = null
return await handlers['getLoggedInCustomer']({ req, res, config, body })
}
} catch (error) {
console.error(error)
@ -53,6 +41,6 @@ const customersApi: BigcommerceApiHandler<Customer, CustomersHandlers> = async (
}
}
const handlers = { createCustomer }
const handlers = { getLoggedInCustomer }
export default createApiHandler(customersApi, handlers, {})

View File

@ -0,0 +1,45 @@
import createApiHandler, {
BigcommerceApiHandler,
BigcommerceHandler,
} from '../utils/create-api-handler'
import isAllowedMethod from '../utils/is-allowed-method'
import { BigcommerceApiError } from '../utils/errors'
import login from './handlers/login'
export type LoginBody = {
email: string
password: string
}
export type LoginHandlers = {
login: BigcommerceHandler<null, Partial<LoginBody>>
}
const METHODS = ['POST']
const loginApi: BigcommerceApiHandler<null, LoginHandlers> = async (
req,
res,
config,
handlers
) => {
if (!isAllowedMethod(req, res, METHODS)) return
try {
const body = req.body ?? {}
return await handlers['login']({ req, res, config, body })
} catch (error) {
console.error(error)
const message =
error instanceof BigcommerceApiError
? 'An unexpected error ocurred with the Bigcommerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
const handlers = { login }
export default createApiHandler(loginApi, handlers, {})

View File

@ -0,0 +1,50 @@
import createApiHandler, {
BigcommerceApiHandler,
BigcommerceHandler,
} from '../utils/create-api-handler'
import isAllowedMethod from '../utils/is-allowed-method'
import { BigcommerceApiError } from '../utils/errors'
import signup from './handlers/signup'
export type SignupBody = {
firstName: string
lastName: string
email: string
password: string
}
export type SignupHandlers = {
signup: BigcommerceHandler<null, { cartId?: string } & Partial<SignupBody>>
}
const METHODS = ['POST']
const signupApi: BigcommerceApiHandler<null, SignupHandlers> = async (
req,
res,
config,
handlers
) => {
if (!isAllowedMethod(req, res, METHODS)) return
const { cookies } = req
const cartId = cookies[config.cartCookie]
try {
const body = { ...req.body, cartId }
return await handlers['signup']({ req, res, config, body })
} catch (error) {
console.error(error)
const message =
error instanceof BigcommerceApiError
? 'An unexpected error ocurred with the Bigcommerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
const handlers = { signup }
export default createApiHandler(signupApi, handlers, {})

View File

@ -3,7 +3,7 @@ import { BigcommerceConfig, getConfig } from '..'
export type BigcommerceApiHandler<
T = any,
H extends BigcommerceHandlers = {},
H extends BigcommerceHandlers<T> = {},
Options extends {} = {}
> = (
req: NextApiRequest,

View File

@ -47,7 +47,7 @@ export type Wishlist = {
export type WishlistList = Wishlist[]
export type WishlistHandlers = {
getAllWishlists: BigcommerceHandler<WishlistList, { customerId?: string }>
getAllWishlists: BigcommerceHandler<WishlistList, { customerId?: string }>
getWishlist: BigcommerceHandler<Wishlist, { wishlistId?: string }>
addWishlist: BigcommerceHandler<
Wishlist,
@ -57,7 +57,10 @@ export type WishlistHandlers = {
Wishlist,
{ wishlistId: string } & Body<AddWishlistBody>
>
addItem: BigcommerceHandler<Wishlist, { wishlistId: string } & Body<AddItemBody>>
addItem: BigcommerceHandler<
Wishlist,
{ wishlistId: string } & Body<AddItemBody>
>
removeItem: BigcommerceHandler<
Wishlist,
{ wishlistId: string } & Body<RemoveItemBody>
@ -86,13 +89,13 @@ const wishlistApi: BigcommerceApiHandler<Wishlist, WishlistHandlers> = async (
// Add an item to the wishlist
if (req.method === 'POST' && wishlistId) {
const body = { wishlistId, ...req.body }
const body = { ...req.body, wishlistId }
return await handlers['addItem']({ req, res, config, body })
}
// Update a wishlist
if (req.method === 'PUT' && wishlistId) {
const body = { wishlistId, ...req.body }
const body = { ...req.body, wishlistId }
return await handlers['updateWishlist']({ req, res, config, body })
}

View File

@ -26,8 +26,8 @@ export const fetcher: HookFetcher<Cart, AddItemBody> = (
}
return fetch({
url: options?.url ?? defaultOpts.url,
method: options?.method ?? defaultOpts.method,
...defaultOpts,
...options,
body: { item },
})
}

View File

@ -5,6 +5,7 @@ import type { Cart } from '../api/cart'
const defaultOpts = {
url: '/api/bigcommerce/cart',
method: 'GET',
}
export type { Cart }
@ -14,12 +15,7 @@ export const fetcher: HookFetcher<Cart | null, CartInput> = (
{ cartId },
fetch
) => {
return cartId
? fetch({
url: options?.url,
query: options?.query,
})
: null
return cartId ? fetch({ ...defaultOpts, ...options }) : null
}
export function extendHook(

View File

@ -19,8 +19,8 @@ export const fetcher: HookFetcher<Cart | null, RemoveItemBody> = (
fetch
) => {
return fetch({
url: options?.url ?? defaultOpts.url,
method: options?.method ?? defaultOpts.method,
...defaultOpts,
...options,
body: { itemId },
})
}

View File

@ -28,8 +28,8 @@ export const fetcher: HookFetcher<Cart | null, UpdateItemBody> = (
}
return fetch({
url: options?.url ?? defaultOpts.url,
method: options?.method ?? defaultOpts.method,
...defaultOpts,
...options,
body: { itemId, item },
})
}

View File

@ -1653,6 +1653,30 @@ export enum CurrencyCode {
Zwr = 'ZWR',
}
export type GetLoggedInCustomerQueryVariables = Exact<{ [key: string]: never }>
export type GetLoggedInCustomerQuery = { __typename?: 'Query' } & {
customer?: Maybe<
{ __typename?: 'Customer' } & Pick<
Customer,
| 'entityId'
| 'firstName'
| 'lastName'
| 'email'
| 'company'
| 'customerGroupId'
| 'notes'
| 'phone'
| 'addressCount'
| 'attributeCount'
> & {
storeCredit: Array<
{ __typename?: 'Money' } & Pick<Money, 'value' | 'currencyCode'>
>
}
>
}
export type CategoryTreeItemFragment = {
__typename?: 'CategoryTreeItem'
} & Pick<

View File

@ -0,0 +1,41 @@
import { ConfigInterface } from 'swr'
import { HookFetcher } from '@lib/commerce/utils/types'
import useCommerceCustomer, { CustomerInput } from '@lib/commerce/use-customer'
import type { Customer, CustomerData } from './api/customers'
const defaultOpts = {
url: '/api/bigcommerce/customer',
method: 'GET',
}
export type { Customer }
export const fetcher: HookFetcher<CustomerData | null, CustomerInput> = (
options,
{ cartId },
fetch
) => {
return cartId ? fetch({ ...defaultOpts, ...options }) : null
}
export function extendHook(
customFetcher: typeof fetcher,
swrOptions?: ConfigInterface
) {
const useCustomer = () => {
const cart = useCommerceCustomer<CustomerData | null>(
defaultOpts,
[],
customFetcher,
{ revalidateOnFocus: false, ...swrOptions }
)
return cart
}
useCustomer.extend = extendHook
return useCustomer
}
export default extendHook(fetcher)

View File

@ -0,0 +1,49 @@
import { useCallback } from 'react'
import { HookFetcher } from '@lib/commerce/utils/types'
import useCommerceLogin from '@lib/commerce/use-login'
import type { LoginBody } from './api/customers/login'
const defaultOpts = {
url: '/api/bigcommerce/customers/login',
method: 'POST',
}
export type LoginInput = LoginBody
export const fetcher: HookFetcher<null, LoginBody> = (
options,
{ email, password },
fetch
) => {
if (!(email && password)) {
throw new Error(
'A first name, last name, email and password are required to login'
)
}
return fetch({
...defaultOpts,
...options,
body: { email, password },
})
}
export function extendHook(customFetcher: typeof fetcher) {
const useLogin = () => {
const fn = useCommerceLogin<null, LoginInput>(defaultOpts, customFetcher)
return useCallback(
async function login(input: LoginInput) {
const data = await fn(input)
return data
},
[fn]
)
}
useLogin.extend = extendHook
return useLogin
}
export default extendHook(fetcher)

View File

@ -0,0 +1,49 @@
import { useCallback } from 'react'
import { HookFetcher } from '@lib/commerce/utils/types'
import useCommerceSignup from '@lib/commerce/use-signup'
import type { SignupBody } from './api/customers/signup'
const defaultOpts = {
url: '/api/bigcommerce/customers/signup',
method: 'POST',
}
export type SignupInput = SignupBody
export const fetcher: HookFetcher<null, SignupBody> = (
options,
{ firstName, lastName, email, password },
fetch
) => {
if (!(firstName && lastName && email && password)) {
throw new Error(
'A first name, last name, email and password are required to signup'
)
}
return fetch({
...defaultOpts,
...options,
body: { firstName, lastName, email, password },
})
}
export function extendHook(customFetcher: typeof fetcher) {
const useSignup = () => {
const fn = useCommerceSignup<null, SignupInput>(defaultOpts, customFetcher)
return useCallback(
async function signup(input: SignupInput) {
const data = await fn(input)
return data
},
[fn]
)
}
useSignup.extend = extendHook
return useSignup
}
export default extendHook(fetcher)

View File

@ -17,8 +17,8 @@ export const fetcher: HookFetcher<Wishlist, AddItemBody> = (
fetch
) => {
return fetch({
url: options?.url ?? defaultOpts.url,
method: options?.method ?? defaultOpts.method,
...defaultOpts,
...options,
body: { wishlistId, item },
})
}

View File

@ -19,8 +19,8 @@ export const fetcher: HookFetcher<Wishlist | null, RemoveItemBody> = (
fetch
) => {
return fetch({
url: options?.url ?? defaultOpts.url,
method: options?.method ?? defaultOpts.method,
...defaultOpts,
...options,
body: { wishlistId, itemId },
})
}

View File

@ -4,6 +4,7 @@ import type { Wishlist } from '../api/wishlist'
const defaultOpts = {
url: '/api/bigcommerce/wishlists',
method: 'GET',
}
export type { Wishlist }
@ -18,7 +19,8 @@ export const fetcher: HookFetcher<Wishlist | null, WishlistInput> = (
fetch
) => {
return fetch({
url: options?.url,
...defaultOpts,
...options,
body: { wishlistId },
})
}

View File

@ -14,8 +14,4 @@ export interface CommerceAPIFetchOptions<V> {
preview?: boolean
}
// TODO: define interfaces for all the available operations
// export interface CommerceAPI {
// getAllProducts(options?: { query: string }): Promise<any>;
// }
// TODO: define interfaces for all the available operations and API endpoints

View File

@ -0,0 +1,30 @@
import { responseInterface, ConfigInterface } from 'swr'
import Cookies from 'js-cookie'
import type { HookInput, HookFetcher, HookFetcherOptions } from './utils/types'
import useData from './utils/use-data'
import { useCommerce } from '.'
export type CustomerResponse<T> = responseInterface<T, Error>
export type CustomerInput = {
cartId: string | undefined
}
export default function useCustomer<T>(
options: HookFetcherOptions,
input: HookInput,
fetcherFn: HookFetcher<T | null, CustomerInput>,
swrOptions?: ConfigInterface<T | null>
) {
// TODO: Replace this with the login cookie
const { cartCookie } = useCommerce()
const fetcher: typeof fetcherFn = (options, input, fetch) => {
input.cartId = Cookies.get(cartCookie)
return fetcherFn(options, input, fetch)
}
const response = useData(options, input, fetcher, swrOptions)
return response as CustomerResponse<T>
}

View File

@ -0,0 +1,5 @@
import useAction from './utils/use-action'
const useLogin = useAction
export default useLogin

View File

@ -0,0 +1,5 @@
import useAction from './utils/use-action'
const useSignup = useAction
export default useSignup

View File

@ -9,9 +9,7 @@ export default function useAction<T, Input>(
const { fetcherRef } = useCommerce()
return useCallback(
function addItem(input: Input) {
return fetcher(options, input, fetcherRef.current)
},
(input: Input) => fetcher(options, input, fetcherRef.current),
[fetcher]
)
}

View File

@ -9,9 +9,15 @@ export default function useData<T, Input = any>(
swrOptions?: ConfigInterface<T>
) {
const { fetcherRef } = useCommerce()
const fetcher = (url?: string, query?: string, ...args: any[]) => {
const fetcher = (
url?: string,
query?: string,
method?: string,
...args: any[]
) => {
return fetcherFn(
{ url, query },
{ url, query, method },
// Transform the input array into an object
args.reduce((obj, val, i) => {
obj[input[i][0]!] = val
return obj
@ -23,7 +29,9 @@ export default function useData<T, Input = any>(
const response = useSWR(
() => {
const opts = typeof options === 'function' ? options() : options
return opts ? [opts.url, opts.query, ...input.map((e) => e[1])] : null
return opts
? [opts.url, opts.query, opts.method, ...input.map((e) => e[1])]
: null
},
fetcher,
swrOptions

View File

@ -0,0 +1,3 @@
import loginApi from '@lib/bigcommerce/api/customers/login'
export default loginApi()

View File

@ -0,0 +1,3 @@
import signupApi from '@lib/bigcommerce/api/customers/signup'
export default signupApi()