mirror of
https://github.com/vercel/commerce.git
synced 2025-03-14 22:42:33 +00:00
Added api builder
This commit is contained in:
parent
808ad87413
commit
c9f540cbd0
57
lib/bigcommerce/api/cart.ts
Normal file
57
lib/bigcommerce/api/cart.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { serialize, CookieSerializeOptions } from 'cookie'
|
||||
import isAllowedMethod from './utils/is-allowed-method'
|
||||
import createApiHandler, {
|
||||
BigcommerceApiHandler,
|
||||
} from './utils/create-api-handler'
|
||||
import { BigcommerceApiError } from './utils/errors'
|
||||
|
||||
type Cart = any
|
||||
|
||||
const METHODS = ['GET', 'POST', 'PUT', 'DELETE']
|
||||
|
||||
const cartApi: BigcommerceApiHandler = async (req, res, config) => {
|
||||
if (!isAllowedMethod(req, res, METHODS)) return
|
||||
|
||||
const { cookies } = req
|
||||
const cartId = cookies[config.cartCookie]
|
||||
|
||||
// Return current cart info
|
||||
if (req.method === 'GET') {
|
||||
let result: { data?: Cart } = {}
|
||||
|
||||
try {
|
||||
result = await config.storeApiFetch(
|
||||
`/v3/carts/${cartId}?include=redirect_urls`
|
||||
)
|
||||
} catch (error) {
|
||||
if (error instanceof BigcommerceApiError && error.status === 404) {
|
||||
// The cookie exists but the cart wasn't found, so, remove the cookie
|
||||
res.setHeader('Set-Cookie', getCartCookie(name))
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).json({ cart: result.data ?? null })
|
||||
}
|
||||
}
|
||||
|
||||
const ONE_DAY = 60 * 60 * 24
|
||||
const MAX_AGE = ONE_DAY * 30
|
||||
|
||||
function getCartCookie(name: string, cartId?: string) {
|
||||
const options: CookieSerializeOptions = cartId
|
||||
? {
|
||||
maxAge: MAX_AGE,
|
||||
expires: new Date(Date.now() + MAX_AGE * 1000),
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
}
|
||||
: { maxAge: -1, path: '/' } // Removes the cookie
|
||||
|
||||
return serialize(name, cartId || '', options)
|
||||
}
|
||||
|
||||
export default createApiHandler(cartApi)
|
@ -1,6 +1,7 @@
|
||||
import { CommerceAPIConfig } from 'lib/commerce/api'
|
||||
import { GetAllProductsQueryVariables } from '../schema'
|
||||
import fetchAPI from './utils/fetch-api'
|
||||
import fetchStoreApi from './utils/fetch-store-api'
|
||||
|
||||
export interface Images {
|
||||
small?: ImageOptions
|
||||
@ -28,6 +29,10 @@ export type ProductImageVariables = Pick<
|
||||
|
||||
export interface BigcommerceConfigOptions extends CommerceAPIConfig {
|
||||
images?: Images
|
||||
storeApiUrl: string
|
||||
storeApiToken: string
|
||||
storeApiClientId: string
|
||||
storeApiFetch<T>(endpoint: string, options?: RequestInit): Promise<T>
|
||||
}
|
||||
|
||||
export interface BigcommerceConfig extends BigcommerceConfigOptions {
|
||||
@ -36,6 +41,9 @@ export interface BigcommerceConfig extends BigcommerceConfigOptions {
|
||||
|
||||
const API_URL = process.env.BIGCOMMERCE_STOREFRONT_API_URL
|
||||
const API_TOKEN = process.env.BIGCOMMERCE_STOREFRONT_API_TOKEN
|
||||
const STORE_API_URL = process.env.BIGCOMMERCE_STORE_API_URL
|
||||
const STORE_API_TOKEN = process.env.BIGCOMMERCE_STORE_API_TOKEN
|
||||
const STORE_API_CLIENT_ID = process.env.BIGCOMMERCE_STORE_API_CLIENT_ID
|
||||
|
||||
if (!API_URL) {
|
||||
throw new Error(
|
||||
@ -49,32 +57,45 @@ if (!API_TOKEN) {
|
||||
)
|
||||
}
|
||||
|
||||
if (!(STORE_API_URL && STORE_API_TOKEN && STORE_API_CLIENT_ID)) {
|
||||
throw new Error(
|
||||
`The environment variables BIGCOMMERCE_STORE_API_URL, BIGCOMMERCE_STORE_API_TOKEN, BIGCOMMERCE_STORE_API_CLIENT_ID have to be set in order to access the REST API of your store`
|
||||
)
|
||||
}
|
||||
|
||||
export class Config {
|
||||
private config: BigcommerceConfig
|
||||
|
||||
constructor(config: BigcommerceConfigOptions) {
|
||||
this.config = {
|
||||
...config,
|
||||
get imageVariables() {
|
||||
const { images } = this
|
||||
return images
|
||||
? {
|
||||
imgSmallWidth: images.small?.width,
|
||||
imgSmallHeight: images.small?.height,
|
||||
imgMediumWidth: images.medium?.height,
|
||||
imgMediumHeight: images.medium?.height,
|
||||
imgLargeWidth: images.large?.height,
|
||||
imgLargeHeight: images.large?.height,
|
||||
imgXLWidth: images.xl?.height,
|
||||
imgXLHeight: images.xl?.height,
|
||||
}
|
||||
: undefined
|
||||
},
|
||||
imageVariables: this.getImageVariables(config.images),
|
||||
}
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
return this.config
|
||||
getImageVariables(images?: Images) {
|
||||
return images
|
||||
? {
|
||||
imgSmallWidth: images.small?.width,
|
||||
imgSmallHeight: images.small?.height,
|
||||
imgMediumWidth: images.medium?.height,
|
||||
imgMediumHeight: images.medium?.height,
|
||||
imgLargeWidth: images.large?.height,
|
||||
imgLargeHeight: images.large?.height,
|
||||
imgXLWidth: images.xl?.height,
|
||||
imgXLHeight: images.xl?.height,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
||||
getConfig(userConfig: Partial<BigcommerceConfig> = {}) {
|
||||
const { images: configImages, ...config } = this.config
|
||||
const images = { ...configImages, ...userConfig.images }
|
||||
|
||||
return Object.assign(config, userConfig, {
|
||||
images,
|
||||
imageVariables: this.getImageVariables(images),
|
||||
})
|
||||
}
|
||||
|
||||
setConfig(newConfig: Partial<BigcommerceConfig>) {
|
||||
@ -85,11 +106,17 @@ export class Config {
|
||||
const config = new Config({
|
||||
commerceUrl: API_URL,
|
||||
apiToken: API_TOKEN,
|
||||
cartCookie: process.env.BIGCOMMERCE_CART_COOKIE ?? 'bc_cartId',
|
||||
fetch: fetchAPI,
|
||||
// REST API only
|
||||
storeApiUrl: STORE_API_URL,
|
||||
storeApiToken: STORE_API_TOKEN,
|
||||
storeApiClientId: STORE_API_CLIENT_ID,
|
||||
storeApiFetch: fetchStoreApi,
|
||||
})
|
||||
|
||||
export function getConfig() {
|
||||
return config.getConfig()
|
||||
export function getConfig(userConfig?: Partial<BigcommerceConfig>) {
|
||||
return config.getConfig(userConfig)
|
||||
}
|
||||
|
||||
export function setConfig(newConfig: Partial<BigcommerceConfig>) {
|
||||
|
18
lib/bigcommerce/api/utils/create-api-handler.ts
Normal file
18
lib/bigcommerce/api/utils/create-api-handler.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
|
||||
import { BigcommerceConfig, getConfig } from '..'
|
||||
|
||||
export type BigcommerceApiHandler = (
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
config: BigcommerceConfig
|
||||
) => void | Promise<void>
|
||||
|
||||
export default function createApiHandler(handler: BigcommerceApiHandler) {
|
||||
return function getApiHandler({
|
||||
config,
|
||||
}: { config?: BigcommerceConfig } = {}): NextApiHandler {
|
||||
return function apiHandler(req, res) {
|
||||
return handler(req, res, getConfig(config))
|
||||
}
|
||||
}
|
||||
}
|
32
lib/bigcommerce/api/utils/errors.ts
Normal file
32
lib/bigcommerce/api/utils/errors.ts
Normal file
@ -0,0 +1,32 @@
|
||||
// Used for GraphQL errors
|
||||
export class BigcommerceError extends Error {
|
||||
status?: number
|
||||
|
||||
constructor(msg: string, res?: Response) {
|
||||
super(msg)
|
||||
this.name = 'BigcommerceError'
|
||||
|
||||
if (res) {
|
||||
this.status = res.status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class BigcommerceApiError extends Error {
|
||||
status: number
|
||||
res: Response
|
||||
|
||||
constructor(msg: string, res: Response) {
|
||||
super(msg)
|
||||
this.name = 'BigcommerceApiError'
|
||||
this.status = res.status
|
||||
this.res = res
|
||||
}
|
||||
}
|
||||
|
||||
export class BigcommerceNetworkError extends Error {
|
||||
constructor(msg: string) {
|
||||
super(msg)
|
||||
this.name = 'BigcommerceNetworkError'
|
||||
}
|
||||
}
|
67
lib/bigcommerce/api/utils/fetch-store-api.ts
Normal file
67
lib/bigcommerce/api/utils/fetch-store-api.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { getConfig } from '..'
|
||||
import { BigcommerceApiError, BigcommerceNetworkError } from './errors'
|
||||
|
||||
export default async function fetchStoreApi<T>(
|
||||
endpoint: string,
|
||||
options?: RequestInit
|
||||
): Promise<T> {
|
||||
const config = getConfig()
|
||||
let res: Response
|
||||
|
||||
try {
|
||||
res = await fetch(config.storeApiUrl + endpoint, {
|
||||
...options,
|
||||
headers: {
|
||||
...options?.headers,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Auth-Token': config.storeApiToken,
|
||||
'X-Auth-Client': config.storeApiClientId,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
throw new BigcommerceNetworkError(
|
||||
`Fetch to Bigcommerce failed: ${error.message}`
|
||||
)
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new BigcommerceApiError(await getErrorText(res), res)
|
||||
}
|
||||
|
||||
const contentType = res.headers.get('Content-Type')
|
||||
|
||||
if (contentType?.includes('application/json')) {
|
||||
throw new BigcommerceApiError(
|
||||
`Fetch to Bigcommerce API failed, expected JSON content but found: ${contentType}`,
|
||||
res
|
||||
)
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async function getErrorText(res: Response) {
|
||||
return `Big Commerce API error (${res.status}) \n${JSON.stringify(
|
||||
getRawHeaders(res)
|
||||
)}\n ${await getTextOrNull(res)}`
|
||||
}
|
||||
|
||||
function getRawHeaders(res: Response) {
|
||||
const headers: { [key: string]: string } = {}
|
||||
|
||||
res.headers.forEach((value, key) => {
|
||||
headers[key] = value
|
||||
})
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
function getTextOrNull(res: Response) {
|
||||
try {
|
||||
return res.text()
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
}
|
28
lib/bigcommerce/api/utils/is-allowed-method.ts
Normal file
28
lib/bigcommerce/api/utils/is-allowed-method.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export default function isAllowedMethod(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
allowedMethods: string[]
|
||||
) {
|
||||
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
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
export interface CommerceAPIConfig {
|
||||
commerceUrl: string
|
||||
apiToken: string
|
||||
cartCookie: string
|
||||
fetch<Q, V = any>(
|
||||
query: string,
|
||||
queryData?: CommerceAPIFetchOptions<V>
|
||||
|
@ -21,6 +21,7 @@
|
||||
"@tailwindcss/ui": "^0.6.2",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"classnames": "^2.2.6",
|
||||
"cookie": "^0.4.1",
|
||||
"lodash": "^4.17.20",
|
||||
"next": "^9.5.4-canary.23",
|
||||
"postcss-nested": "^5.0.1",
|
||||
@ -34,6 +35,7 @@
|
||||
"@graphql-codegen/schema-ast": "^1.17.8",
|
||||
"@graphql-codegen/typescript": "^1.17.10",
|
||||
"@graphql-codegen/typescript-operations": "^1.17.8",
|
||||
"@types/cookie": "^0.4.0",
|
||||
"@types/node": "^14.11.2",
|
||||
"@types/react": "^16.9.49",
|
||||
"graphql": "^15.3.0",
|
||||
|
10
yarn.lock
10
yarn.lock
@ -1507,6 +1507,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
|
||||
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
|
||||
|
||||
"@types/cookie@^0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.0.tgz#14f854c0f93d326e39da6e3b6f34f7d37513d108"
|
||||
integrity sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg==
|
||||
|
||||
"@types/eslint-scope@^3.7.0":
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.0.tgz#4792816e31119ebd506902a482caec4951fabd86"
|
||||
@ -2704,6 +2709,11 @@ convert-source-map@^0.3.3:
|
||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190"
|
||||
integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA=
|
||||
|
||||
cookie@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
|
||||
integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
|
||||
|
||||
copy-descriptor@^0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
|
||||
|
Loading…
x
Reference in New Issue
Block a user