Adding multiple initial files

This commit is contained in:
Luis Alvarez 2021-03-12 17:25:01 -06:00
parent 18d8d9acc8
commit 647e8aaf4c
14 changed files with 448 additions and 2 deletions

View File

@ -0,0 +1,45 @@
import { parseCartItem } from '../../utils/parse-item'
import getCartCookie from '../../utils/get-cart-cookie'
import type { CartHandlers } from '..'
const addItem: CartHandlers['addItem'] = async ({
res,
body: { cartId, item },
config,
}) => {
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
if (!item.quantity) item.quantity = 1
const options = {
method: 'POST',
body: JSON.stringify({
line_items: [parseCartItem(item)],
...(!cartId && config.storeChannelId
? { channel_id: config.storeChannelId }
: {}),
}),
}
const { data } = cartId
? await config.storeApiFetch(
`/v3/carts/${cartId}/items?include=line_items.physical_items.options`,
options
)
: await config.storeApiFetch(
'/v3/carts?include=line_items.physical_items.options',
options
)
// Create or update the cart cookie
res.setHeader(
'Set-Cookie',
getCartCookie(config.cartCookie, data.id, config.cartCookieMaxAge)
)
res.status(200).json({ data })
}
export default addItem

View File

@ -0,0 +1,32 @@
import type { BigcommerceCart } from '../../../types'
import { BigcommerceApiError } from '../../utils/errors'
import getCartCookie from '../../utils/get-cart-cookie'
import type { CartHandlers } from '../'
// Return current cart info
const getCart: CartHandlers['getCart'] = async ({
res,
body: { cartId },
config,
}) => {
let result: { data?: BigcommerceCart } = {}
if (cartId) {
try {
result = await config.storeApiFetch(
`/v3/carts/${cartId}?include=line_items.physical_items.options`
)
} catch (error) {
if (error instanceof BigcommerceApiError && error.status === 404) {
// Remove the cookie if it exists but the cart wasn't found
res.setHeader('Set-Cookie', getCartCookie(config.cartCookie))
} else {
throw error
}
}
}
res.status(200).json({ data: result.data ?? null })
}
export default getCart

View File

@ -0,0 +1,33 @@
import getCartCookie from '../../utils/get-cart-cookie'
import type { CartHandlers } from '..'
const removeItem: CartHandlers['removeItem'] = async ({
res,
body: { cartId, itemId },
config,
}) => {
if (!cartId || !itemId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const result = await config.storeApiFetch<{ data: any } | null>(
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
{ method: 'DELETE' }
)
const data = result?.data ?? null
res.setHeader(
'Set-Cookie',
data
? // Update the cart cookie
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
: // Remove the cart cookie if the cart was removed (empty items)
getCartCookie(config.cartCookie)
)
res.status(200).json({ data })
}
export default removeItem

View File

@ -0,0 +1,35 @@
import { parseCartItem } from '../../utils/parse-item'
import getCartCookie from '../../utils/get-cart-cookie'
import type { CartHandlers } from '..'
const updateItem: CartHandlers['updateItem'] = async ({
res,
body: { cartId, itemId, item },
config,
}) => {
if (!cartId || !itemId || !item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const { data } = await config.storeApiFetch(
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
{
method: 'PUT',
body: JSON.stringify({
line_item: parseCartItem(item),
}),
}
)
// Update the cart cookie
res.setHeader(
'Set-Cookie',
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
)
res.status(200).json({ data })
}
export default updateItem

View File

@ -0,0 +1,78 @@
import isAllowedMethod from '../utils/is-allowed-method'
import createApiHandler, {
BigcommerceApiHandler,
BigcommerceHandler,
} from '../utils/create-api-handler'
import { BigcommerceApiError } from '../utils/errors'
import getCart from './handlers/get-cart'
import addItem from './handlers/add-item'
import updateItem from './handlers/update-item'
import removeItem from './handlers/remove-item'
import type {
BigcommerceCart,
GetCartHandlerBody,
AddCartItemHandlerBody,
UpdateCartItemHandlerBody,
RemoveCartItemHandlerBody,
} from '../../types'
export type CartHandlers = {
getCart: BigcommerceHandler<BigcommerceCart, GetCartHandlerBody>
addItem: BigcommerceHandler<BigcommerceCart, AddCartItemHandlerBody>
updateItem: BigcommerceHandler<BigcommerceCart, UpdateCartItemHandlerBody>
removeItem: BigcommerceHandler<BigcommerceCart, RemoveCartItemHandlerBody>
}
const METHODS = ['GET', 'POST', 'PUT', 'DELETE']
// TODO: a complete implementation should have schema validation for `req.body`
const cartApi: BigcommerceApiHandler<BigcommerceCart, CartHandlers> = async (
req,
res,
config,
handlers
) => {
if (!isAllowedMethod(req, res, METHODS)) 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']({ req, res, config, body })
}
// Create or add an item to the cart
if (req.method === 'POST') {
const body = { ...req.body, cartId }
return await handlers['addItem']({ req, res, config, body })
}
// Update item in cart
if (req.method === 'PUT') {
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 = { ...req.body, cartId }
return await handlers['removeItem']({ 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 }] })
}
}
export const handlers = { getCart, addItem, updateItem, removeItem }
export default createApiHandler(cartApi, handlers, {})

View File

@ -1,5 +1,5 @@
import type { RequestInit } from '@vercel/fetch' import type { RequestInit } from '@vercel/fetch'
import type { CommerceAPIConfig } from '@commerce/api' import { CommerceAPIConfig, createAPIProvider } from '@commerce/api'
import fetchGraphqlApi from './utils/fetch-graphql-api' import fetchGraphqlApi from './utils/fetch-graphql-api'
import fetchStoreApi from './utils/fetch-store-api' import fetchStoreApi from './utils/fetch-store-api'
@ -79,6 +79,24 @@ const config = new Config({
storeApiFetch: fetchStoreApi, storeApiFetch: fetchStoreApi,
}) })
const config2: BigcommerceConfig = {
commerceUrl: API_URL,
apiToken: API_TOKEN,
customerCookie: 'SHOP_TOKEN',
cartCookie: process.env.BIGCOMMERCE_CART_COOKIE ?? 'bc_cartId',
cartCookieMaxAge: ONE_DAY * 30,
fetch: fetchGraphqlApi,
applyLocale: true,
// REST API only
storeApiUrl: STORE_API_URL,
storeApiToken: STORE_API_TOKEN,
storeApiClientId: STORE_API_CLIENT_ID,
storeChannelId: STORE_CHANNEL_ID,
storeApiFetch: fetchStoreApi,
}
export const commerce = createAPIProvider({ config: config2 })
export function getConfig(userConfig?: Partial<BigcommerceConfig>) { export function getConfig(userConfig?: Partial<BigcommerceConfig>) {
return config.getConfig(userConfig) return config.getConfig(userConfig)
} }

View File

View File

@ -48,7 +48,7 @@ export default function createApiHandler<
operations?: Partial<H> operations?: Partial<H>
options?: Options extends {} ? Partial<Options> : never options?: Options extends {} ? Partial<Options> : never
} = {}): NextApiHandler { } = {}): NextApiHandler {
const ops = { ...operations, ...handlers } const ops = { ...handlers, ...operations }
const opts = { ...defaultOptions, ...options } const opts = { ...defaultOptions, ...options }
return function apiHandler(req, res) { return function apiHandler(req, res) {

View File

@ -0,0 +1,58 @@
import type { APIEndpoint, APIHandler } from '../utils/types'
import isAllowedMethod from '../utils/is-allowed-method'
import cn from 'classnames'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { APIProvider, CartHandlers } from '..'
cn({ yo: true })
const METHODS = ['GET', 'POST', 'PUT', 'DELETE']
const cartApi: APIEndpoint<APIProvider, CartHandlers> = async (ctx) => {
if (
!isAllowedOperation(ctx.req, ctx.res, {
GET: ctx.handlers['getCart'],
})
) {
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']({ req, res, config, body })
}
// Create or add an item to the cart
if (req.method === 'POST') {
const body = { ...req.body, cartId }
return await handlers['addItem']({ req, res, config, body })
}
// Update item in cart
if (req.method === 'PUT') {
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 = { ...req.body, cartId }
return await handlers['removeItem']({ 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 }] })
}
}

View File

@ -1,4 +1,44 @@
import type { RequestInit, Response } from '@vercel/fetch' import type { RequestInit, Response } from '@vercel/fetch'
import type { APIEndpoint, APIHandler } from './utils/types'
export type CartHandlers = {
getCart: APIHandler<any>
addItem: APIHandler<any>
updateItem: APIHandler<any>
removeItem: APIHandler<any>
}
export type CoreAPIProvider = {
config: CommerceAPIConfig
endpoints?: {
cart?: {
handler: APIEndpoint<any, any, CartHandlers, any>
handlers: CartHandlers
}
}
}
export type APIProvider<P extends CoreAPIProvider = CoreAPIProvider> = P & {
getConfig(userConfig?: Partial<P['config']>): P['config']
setConfig(newConfig: Partial<P['config']>): void
}
export function createAPIProvider<P extends CoreAPIProvider>(
provider: P
): APIProvider<P> {
return {
...provider,
getConfig(userConfig = {}) {
return Object.entries(userConfig).reduce(
(cfg, [key, value]) => Object.assign(cfg, { [key]: value }),
{ ...this.config }
)
},
setConfig(newConfig) {
Object.assign(this.config, newConfig)
},
}
}
export interface CommerceAPIConfig { export interface CommerceAPIConfig {
locale?: string locale?: string

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,
allowedHandlers: { [k in HTTP_METHODS]?: APIHandler<any> }
) {
const methods = Object.keys(allowedHandlers) as HTTP_METHODS[]
const allowedMethods = methods.reduce<HTTP_METHODS[]>((arr, method) => {
if (allowedHandlers[method]) {
arr.push(method)
}
return arr
}, [])
return isAllowedMethod(req, res, allowedMethods)
}

View File

@ -0,0 +1,45 @@
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
import type { APIProvider } from '..'
export type APIResponse<Data = any> = {
data: Data
errors?: { message: string; code?: string }[]
}
export type APIHandlerContext<
P extends APIProvider,
H extends APIHandlers<P, Data> = {},
Data = any,
Options extends {} = {}
> = {
req: NextApiRequest
res: NextApiResponse<APIResponse<Data>>
provider: P
config: P['config']
handlers: H
/**
* Custom configs that may be used by a particular handler
*/
options: Options
}
export type APIHandler<
P extends APIProvider,
H extends APIHandlers<P, Data> = {},
Data = any,
Body = any,
Options extends {} = {}
> = (
context: APIHandlerContext<P, H, Data, Options> & { body: Body }
) => void | Promise<void>
export type APIHandlers<P extends APIProvider, Data = any> = {
[k: string]: APIHandler<P, any, Data, any>
}
export type APIEndpoint<
P extends APIProvider = APIProvider,
H extends APIHandlers<P, Data> = {},
Data = any,
Options extends {} = {}
> = (context: APIHandlerContext<P, H, Data, Options>) => void | Promise<void>

View File

@ -160,6 +160,19 @@ interface Entity {
[prop: string]: any [prop: string]: any
} }
export interface Product2 {
id: string
name: string
description: string
sku?: string
slug?: string
path?: string
images: ProductImage[]
variants: ProductVariant2[]
price: ProductPrice
options: ProductOption[]
}
export interface Product extends Entity { export interface Product extends Entity {
name: string name: string
description: string description: string