mirror of
https://github.com/vercel/commerce.git
synced 2025-06-18 21:21:21 +00:00
Adding multiple initial files
This commit is contained in:
parent
18d8d9acc8
commit
647e8aaf4c
@ -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
|
@ -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
|
@ -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
|
@ -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
|
78
framework/bigcommerce/api/endpoints/cart/index.ts
Normal file
78
framework/bigcommerce/api/endpoints/cart/index.ts
Normal 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, {})
|
@ -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)
|
||||||
}
|
}
|
||||||
|
0
framework/bigcommerce/api/provider.ts
Normal file
0
framework/bigcommerce/api/provider.ts
Normal 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) {
|
||||||
|
58
framework/commerce/api/endpoints/cart.ts
Normal file
58
framework/commerce/api/endpoints/cart.ts
Normal 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 }] })
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
30
framework/commerce/api/utils/is-allowed-method.ts
Normal file
30
framework/commerce/api/utils/is-allowed-method.ts
Normal 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
|
||||||
|
}
|
19
framework/commerce/api/utils/is-allowed-operation.ts
Normal file
19
framework/commerce/api/utils/is-allowed-operation.ts
Normal 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)
|
||||||
|
}
|
45
framework/commerce/api/utils/types.ts
Normal file
45
framework/commerce/api/utils/types.ts
Normal 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>
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user