forked from crowetic/commerce
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 { CommerceAPIConfig } from 'lib/commerce/api'
|
||||||
import { GetAllProductsQueryVariables } from '../schema'
|
import { GetAllProductsQueryVariables } from '../schema'
|
||||||
import fetchAPI from './utils/fetch-api'
|
import fetchAPI from './utils/fetch-api'
|
||||||
|
import fetchStoreApi from './utils/fetch-store-api'
|
||||||
|
|
||||||
export interface Images {
|
export interface Images {
|
||||||
small?: ImageOptions
|
small?: ImageOptions
|
||||||
@ -28,6 +29,10 @@ export type ProductImageVariables = Pick<
|
|||||||
|
|
||||||
export interface BigcommerceConfigOptions extends CommerceAPIConfig {
|
export interface BigcommerceConfigOptions extends CommerceAPIConfig {
|
||||||
images?: Images
|
images?: Images
|
||||||
|
storeApiUrl: string
|
||||||
|
storeApiToken: string
|
||||||
|
storeApiClientId: string
|
||||||
|
storeApiFetch<T>(endpoint: string, options?: RequestInit): Promise<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BigcommerceConfig extends BigcommerceConfigOptions {
|
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_URL = process.env.BIGCOMMERCE_STOREFRONT_API_URL
|
||||||
const API_TOKEN = process.env.BIGCOMMERCE_STOREFRONT_API_TOKEN
|
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) {
|
if (!API_URL) {
|
||||||
throw new Error(
|
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 {
|
export class Config {
|
||||||
private config: BigcommerceConfig
|
private config: BigcommerceConfig
|
||||||
|
|
||||||
constructor(config: BigcommerceConfigOptions) {
|
constructor(config: BigcommerceConfigOptions) {
|
||||||
this.config = {
|
this.config = {
|
||||||
...config,
|
...config,
|
||||||
get imageVariables() {
|
imageVariables: this.getImageVariables(config.images),
|
||||||
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
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getConfig() {
|
getImageVariables(images?: Images) {
|
||||||
return this.config
|
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>) {
|
setConfig(newConfig: Partial<BigcommerceConfig>) {
|
||||||
@ -85,11 +106,17 @@ export class Config {
|
|||||||
const config = new Config({
|
const config = new Config({
|
||||||
commerceUrl: API_URL,
|
commerceUrl: API_URL,
|
||||||
apiToken: API_TOKEN,
|
apiToken: API_TOKEN,
|
||||||
|
cartCookie: process.env.BIGCOMMERCE_CART_COOKIE ?? 'bc_cartId',
|
||||||
fetch: fetchAPI,
|
fetch: fetchAPI,
|
||||||
|
// REST API only
|
||||||
|
storeApiUrl: STORE_API_URL,
|
||||||
|
storeApiToken: STORE_API_TOKEN,
|
||||||
|
storeApiClientId: STORE_API_CLIENT_ID,
|
||||||
|
storeApiFetch: fetchStoreApi,
|
||||||
})
|
})
|
||||||
|
|
||||||
export function getConfig() {
|
export function getConfig(userConfig?: Partial<BigcommerceConfig>) {
|
||||||
return config.getConfig()
|
return config.getConfig(userConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setConfig(newConfig: Partial<BigcommerceConfig>) {
|
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 {
|
export interface CommerceAPIConfig {
|
||||||
commerceUrl: string
|
commerceUrl: string
|
||||||
apiToken: string
|
apiToken: string
|
||||||
|
cartCookie: string
|
||||||
fetch<Q, V = any>(
|
fetch<Q, V = any>(
|
||||||
query: string,
|
query: string,
|
||||||
queryData?: CommerceAPIFetchOptions<V>
|
queryData?: CommerceAPIFetchOptions<V>
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
"@tailwindcss/ui": "^0.6.2",
|
"@tailwindcss/ui": "^0.6.2",
|
||||||
"@types/classnames": "^2.2.10",
|
"@types/classnames": "^2.2.10",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
|
"cookie": "^0.4.1",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"next": "^9.5.4-canary.23",
|
"next": "^9.5.4-canary.23",
|
||||||
"postcss-nested": "^5.0.1",
|
"postcss-nested": "^5.0.1",
|
||||||
@ -34,6 +35,7 @@
|
|||||||
"@graphql-codegen/schema-ast": "^1.17.8",
|
"@graphql-codegen/schema-ast": "^1.17.8",
|
||||||
"@graphql-codegen/typescript": "^1.17.10",
|
"@graphql-codegen/typescript": "^1.17.10",
|
||||||
"@graphql-codegen/typescript-operations": "^1.17.8",
|
"@graphql-codegen/typescript-operations": "^1.17.8",
|
||||||
|
"@types/cookie": "^0.4.0",
|
||||||
"@types/node": "^14.11.2",
|
"@types/node": "^14.11.2",
|
||||||
"@types/react": "^16.9.49",
|
"@types/react": "^16.9.49",
|
||||||
"graphql": "^15.3.0",
|
"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"
|
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
|
||||||
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
|
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":
|
"@types/eslint-scope@^3.7.0":
|
||||||
version "3.7.0"
|
version "3.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.0.tgz#4792816e31119ebd506902a482caec4951fabd86"
|
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"
|
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190"
|
||||||
integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA=
|
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:
|
copy-descriptor@^0.1.0:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
|
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user