Added framework folder

This commit is contained in:
Luis Alvarez
2020-12-29 19:14:49 -05:00
parent 5b5c8702a3
commit ed0783cfb3
88 changed files with 11839 additions and 3 deletions

View File

@@ -0,0 +1,14 @@
type Header = string | number | string[] | undefined
export default function concatHeader(prev: Header, val: Header) {
if (!val) return prev
if (!prev) return val
if (Array.isArray(prev)) return prev.concat(String(val))
prev = String(prev)
if (Array.isArray(val)) return [prev].concat(val)
return [prev, String(val)]
}

View File

@@ -0,0 +1,58 @@
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
import { BigcommerceConfig, getConfig } from '..'
export type BigcommerceApiHandler<
T = any,
H extends BigcommerceHandlers = {},
Options extends {} = {}
> = (
req: NextApiRequest,
res: NextApiResponse<BigcommerceApiResponse<T>>,
config: BigcommerceConfig,
handlers: H,
// Custom configs that may be used by a particular handler
options: Options
) => void | Promise<void>
export type BigcommerceHandler<T = any, Body = null> = (options: {
req: NextApiRequest
res: NextApiResponse<BigcommerceApiResponse<T>>
config: BigcommerceConfig
body: Body
}) => void | Promise<void>
export type BigcommerceHandlers<T = any> = {
[k: string]: BigcommerceHandler<T, any>
}
export type BigcommerceApiResponse<T> = {
data: T | null
errors?: { message: string; code?: string }[]
}
export default function createApiHandler<
T = any,
H extends BigcommerceHandlers = {},
Options extends {} = {}
>(
handler: BigcommerceApiHandler<T, H, Options>,
handlers: H,
defaultOptions: Options
) {
return function getApiHandler({
config,
operations,
options,
}: {
config?: BigcommerceConfig
operations?: Partial<H>
options?: Options extends {} ? Partial<Options> : never
} = {}): NextApiHandler {
const ops = { ...operations, ...handlers }
const opts = { ...defaultOptions, ...options }
return function apiHandler(req, res) {
return handler(req, res, getConfig(config), ops, opts)
}
}
}

View File

@@ -0,0 +1,25 @@
import type { Response } from '@vercel/fetch'
// Used for GraphQL errors
export class BigcommerceGraphQLError extends Error {}
export class BigcommerceApiError extends Error {
status: number
res: Response
data: any
constructor(msg: string, res: Response, data?: any) {
super(msg)
this.name = 'BigcommerceApiError'
this.status = res.status
this.res = res
this.data = data
}
}
export class BigcommerceNetworkError extends Error {
constructor(msg: string) {
super(msg)
this.name = 'BigcommerceNetworkError'
}
}

View File

@@ -0,0 +1,38 @@
import { FetcherError } from '@commerce/utils/errors'
import type { GraphQLFetcher } from '@commerce/api'
import { getConfig } from '..'
import fetch from './fetch'
const fetchGraphqlApi: GraphQLFetcher = async (
query: string,
{ variables, preview } = {},
fetchOptions
) => {
// log.warn(query)
const config = getConfig()
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
...fetchOptions,
method: 'POST',
headers: {
Authorization: `Bearer ${config.apiToken}`,
...fetchOptions?.headers,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
variables,
}),
})
const json = await res.json()
if (json.errors) {
throw new FetcherError({
errors: json.errors ?? [{ message: 'Failed to fetch Bigcommerce API' }],
status: res.status,
})
}
return { data: json.data, res }
}
export default fetchGraphqlApi

View File

@@ -0,0 +1,71 @@
import type { RequestInit, Response } from '@vercel/fetch'
import { getConfig } from '..'
import { BigcommerceApiError, BigcommerceNetworkError } from './errors'
import fetch from './fetch'
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}`
)
}
const contentType = res.headers.get('Content-Type')
const isJSON = contentType?.includes('application/json')
if (!res.ok) {
const data = isJSON ? await res.json() : await getTextOrNull(res)
const headers = getRawHeaders(res)
const msg = `Big Commerce API error (${
res.status
}) \nHeaders: ${JSON.stringify(headers, null, 2)}\n${
typeof data === 'string' ? data : JSON.stringify(data, null, 2)
}`
throw new BigcommerceApiError(msg, res, data)
}
if (res.status !== 204 && !isJSON) {
throw new BigcommerceApiError(
`Fetch to Bigcommerce API failed, expected JSON content but found: ${contentType}`,
res
)
}
// If something was removed, the response will be empty
return res.status === 204 ? null : await res.json()
}
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
}
}

View File

@@ -0,0 +1,3 @@
import zeitFetch from '@vercel/fetch'
export default zeitFetch()

View File

@@ -0,0 +1,5 @@
export default function filterEdges<T>(
edges: (T | null | undefined)[] | null | undefined
) {
return edges?.filter((edge): edge is T => !!edge) ?? []
}

View File

@@ -0,0 +1,20 @@
import { serialize, CookieSerializeOptions } from 'cookie'
export default function getCartCookie(
name: string,
cartId?: string,
maxAge?: number
) {
const options: CookieSerializeOptions =
cartId && maxAge
? {
maxAge,
expires: new Date(Date.now() + maxAge * 1000),
secure: process.env.NODE_ENV === 'production',
path: '/',
sameSite: 'lax',
}
: { maxAge: -1, path: '/' } // Removes the cookie
return serialize(name, cartId || '', options)
}

View 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
}

View File

@@ -0,0 +1,14 @@
import type { ItemBody as WishlistItemBody } from '../wishlist'
import type { ItemBody } from '../cart'
export const parseWishlistItem = (item: WishlistItemBody) => ({
product_id: item.productId,
variant_id: item.variantId,
})
export const parseCartItem = (item: ItemBody) => ({
quantity: item.quantity,
product_id: item.productId,
variant_id: item.variantId,
option_selections: item.optionSelections
})

View File

@@ -0,0 +1,21 @@
import type { ProductNode } from '../operations/get-all-products'
import type { RecursivePartial } from './types'
export default function setProductLocaleMeta(
node: RecursivePartial<ProductNode>
) {
if (node.localeMeta?.edges) {
node.localeMeta.edges = node.localeMeta.edges.filter((edge) => {
const { key, value } = edge?.node ?? {}
if (key && key in node) {
;(node as any)[key] = value
return false
}
return true
})
if (!node.localeMeta.edges.length) {
delete node.localeMeta
}
}
}

View File

@@ -0,0 +1,7 @@
export type RecursivePartial<T> = {
[P in keyof T]?: RecursivePartial<T[P]>
}
export type RecursiveRequired<T> = {
[P in keyof T]-?: RecursiveRequired<T[P]>
}