4
0
forked from crowetic/commerce

remove lib/bigcommerce and lib/commerce

This commit is contained in:
luc 2020-10-27 02:51:53 +01:00
parent 5962657b7d
commit ae822df689
86 changed files with 0 additions and 11235 deletions

View File

@ -1,37 +0,0 @@
import { parseCartItem } from '../../utils/parse-item'
import getCartCookie from '../../utils/get-cart-cookie'
import type { CartHandlers } from '..'
// Return current cart info
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)],
}),
}
const { data } = cartId
? await config.storeApiFetch(`/v3/carts/${cartId}/items`, options)
: await config.storeApiFetch('/v3/carts', 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

@ -1,29 +0,0 @@
import { BigcommerceApiError } from '../../utils/errors'
import getCartCookie from '../../utils/get-cart-cookie'
import type { Cart, CartHandlers } from '..'
// Return current cart info
const getCart: CartHandlers['getCart'] = async ({
res,
body: { cartId },
config,
}) => {
let result: { data?: Cart } = {}
if (cartId) {
try {
result = await config.storeApiFetch(`/v3/carts/${cartId}`)
} 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

@ -1,34 +0,0 @@
import getCartCookie from '../../utils/get-cart-cookie'
import type { CartHandlers } from '..'
// Return current cart info
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}`,
{ 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

@ -1,36 +0,0 @@
import { parseCartItem } from '../../utils/parse-item'
import getCartCookie from '../../utils/get-cart-cookie'
import type { CartHandlers } from '..'
// Return current cart info
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}`,
{
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

@ -1,110 +0,0 @@
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'
export type ItemBody = {
productId: number
variantId: number
quantity?: number
}
export type AddItemBody = { item: ItemBody }
export type UpdateItemBody = { itemId: string; item: ItemBody }
export type RemoveItemBody = { itemId: string }
// TODO: this type should match:
// https://developer.bigcommerce.com/api-reference/cart-checkout/server-server-cart-api/cart/getacart#responses
export type Cart = {
id: string
parent_id?: string
customer_id: number
email: string
currency: { code: string }
tax_included: boolean
base_amount: number
discount_amount: number
cart_amount: number
line_items: {
custom_items: any[]
digital_items: any[]
gift_certificates: any[]
physical_items: any[]
}
// TODO: add missing fields
}
export type CartHandlers = {
getCart: BigcommerceHandler<Cart, { cartId?: string }>
addItem: BigcommerceHandler<Cart, { cartId?: string } & Partial<AddItemBody>>
updateItem: BigcommerceHandler<
Cart,
{ cartId?: string } & Partial<UpdateItemBody>
>
removeItem: BigcommerceHandler<
Cart,
{ cartId?: string } & Partial<RemoveItemBody>
>
}
const METHODS = ['GET', 'POST', 'PUT', 'DELETE']
// TODO: a complete implementation should have schema validation for `req.body`
const cartApi: BigcommerceApiHandler<Cart, 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,73 +0,0 @@
import getAllProducts, { ProductEdge } from '../../operations/get-all-products'
import type { ProductsHandlers } from '../products'
const SORT: { [key: string]: string | undefined } = {
latest: 'id',
trending: 'total_sold',
price: 'price',
}
const LIMIT = 12
// Return current cart info
const getProducts: ProductsHandlers['getProducts'] = async ({
res,
body: { search, category, brand, sort },
config,
}) => {
// Use a dummy base as we only care about the relative path
const url = new URL('/v3/catalog/products', 'http://a')
url.searchParams.set('is_visible', 'true')
url.searchParams.set('limit', String(LIMIT))
if (search) url.searchParams.set('keyword', search)
if (category && Number.isInteger(Number(category)))
url.searchParams.set('categories:in', category)
if (brand && Number.isInteger(Number(brand)))
url.searchParams.set('brand_id', brand)
if (sort) {
const [_sort, direction] = sort.split('-')
const sortValue = SORT[_sort]
if (sortValue && direction) {
url.searchParams.set('sort', sortValue)
url.searchParams.set('direction', direction)
}
}
// We only want the id of each product
url.searchParams.set('include_fields', 'id')
const { data } = await config.storeApiFetch<{ data: { id: number }[] }>(
url.pathname + url.search
)
const entityIds = data.map((p) => p.id)
const found = entityIds.length > 0
// We want the GraphQL version of each product
const graphqlData = await getAllProducts({
variables: { first: LIMIT, entityIds },
config,
})
// Put the products in an object that we can use to get them by id
const productsById = graphqlData.products.reduce<{
[k: number]: ProductEdge
}>((prods, p) => {
prods[p.node.entityId] = p
return prods
}, {})
const products: ProductEdge[] = found ? [] : graphqlData.products
// Populate the products array with the graphql products, in the order
// assigned by the list of entity ids
entityIds.forEach((id) => {
const product = productsById[id]
if (product) products.push(product)
})
res.status(200).json({ data: { products, found } })
}
export default getProducts

View File

@ -1,48 +0,0 @@
import isAllowedMethod from '../utils/is-allowed-method'
import createApiHandler, {
BigcommerceApiHandler,
BigcommerceHandler,
} from '../utils/create-api-handler'
import { BigcommerceApiError } from '../utils/errors'
import type { ProductEdge } from '../operations/get-all-products'
import getProducts from './handlers/get-products'
export type SearchProductsData = {
products: ProductEdge[]
found: boolean
}
export type ProductsHandlers = {
getProducts: BigcommerceHandler<
SearchProductsData,
{ search?: 'string'; category?: string; brand?: string; sort?: string }
>
}
const METHODS = ['GET']
// TODO: a complete implementation should have schema validation for `req.body`
const productsApi: BigcommerceApiHandler<
SearchProductsData,
ProductsHandlers
> = async (req, res, config, handlers) => {
if (!isAllowedMethod(req, res, METHODS)) return
try {
const body = req.query
return await handlers['getProducts']({ 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 = { getProducts }
export default createApiHandler(productsApi, handlers, {})

View File

@ -1,76 +0,0 @@
import isAllowedMethod from './utils/is-allowed-method'
import createApiHandler, {
BigcommerceApiHandler,
} from './utils/create-api-handler'
import { BigcommerceApiError } from './utils/errors'
const METHODS = ['GET']
const fullCheckout = true
// TODO: a complete implementation should have schema validation for `req.body`
const checkoutApi: BigcommerceApiHandler<any> = async (req, res, config) => {
if (!isAllowedMethod(req, res, METHODS)) return
const { cookies } = req
const cartId = cookies[config.cartCookie]
try {
if (!cartId) {
return res.status(404).end()
}
const { data } = await config.storeApiFetch(
`/v3/carts/${cartId}/redirect_urls`,
{
method: 'POST',
}
)
if (fullCheckout) {
res.redirect(data.checkout_url)
return
}
// TODO: make the embedded checkout work too!
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Checkout</title>
<script src="https://checkout-sdk.bigcommerce.com/v1/loader.js"></script>
<script>
window.onload = function() {
checkoutKitLoader.load('checkout-sdk').then(function (service) {
service.embedCheckout({
containerId: 'checkout',
url: '${data.embedded_checkout_url}'
});
});
}
</script>
</head>
<body>
<div id="checkout"></div>
</body>
</html>
`
res.status(200)
res.setHeader('Content-Type', 'text/html')
res.write(html)
res.end()
} 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 default createApiHandler(checkoutApi, {}, {})

View File

@ -1,59 +0,0 @@
import type { GetLoggedInCustomerQuery } from '../../../schema'
import type { CustomersHandlers } from '..'
export const getLoggedInCustomerQuery = /* GraphQL */ `
query getLoggedInCustomer {
customer {
entityId
firstName
lastName
email
company
customerGroupId
notes
phone
addressCount
attributeCount
storeCredit {
value
currencyCode
}
}
}
`
export type Customer = NonNullable<GetLoggedInCustomerQuery['customer']>
const getLoggedInCustomer: CustomersHandlers['getLoggedInCustomer'] = async ({
req,
res,
config,
}) => {
const token = req.cookies[config.customerCookie]
if (token) {
const { data } = await config.fetch<GetLoggedInCustomerQuery>(
getLoggedInCustomerQuery,
undefined,
{
headers: {
cookie: `${config.customerCookie}=${token}`,
},
}
)
const { customer } = data
if (!customer) {
return res.status(400).json({
data: null,
errors: [{ message: 'Customer not found', code: 'not_found' }],
})
}
return res.status(200).json({ data: { customer } })
}
res.status(200).json({ data: null })
}
export default getLoggedInCustomer

View File

@ -1,49 +0,0 @@
import { FetcherError } from '../../../../commerce/utils/errors'
import login from '../../operations/login'
import type { LoginHandlers } from '../login'
const invalidCredentials = /invalid credentials/i
const loginHandler: LoginHandlers['login'] = async ({
res,
body: { email, password },
config,
}) => {
// TODO: Add proper validations with something like Ajv
if (!(email && password)) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
// TODO: validate the password and email
// Passwords must be at least 7 characters and contain both alphabetic
// and numeric characters.
try {
await login({ variables: { email, password }, config, res })
} catch (error) {
// Check if the email and password didn't match an existing account
if (
error instanceof FetcherError &&
invalidCredentials.test(error.message)
) {
return res.status(401).json({
data: null,
errors: [
{
message:
'Cannot find an account that matches the provided credentials',
code: 'invalid_credentials',
},
],
})
}
throw error
}
res.status(200).json({ data: null })
}
export default loginHandler

View File

@ -1,23 +0,0 @@
import { serialize } from 'cookie'
import { LogoutHandlers } from '../logout'
const logoutHandler: LogoutHandlers['logout'] = async ({
res,
body: { redirectTo },
config,
}) => {
// Remove the cookie
res.setHeader(
'Set-Cookie',
serialize(config.customerCookie, '', { maxAge: -1, path: '/' })
)
// Only allow redirects to a relative URL
if (redirectTo?.startsWith('/')) {
res.redirect(redirectTo)
} else {
res.status(200).json({ data: null })
}
}
export default logoutHandler

View File

@ -1,62 +0,0 @@
import { BigcommerceApiError } from '../../utils/errors'
import login from '../../operations/login'
import { SignupHandlers } from '../signup'
const signup: SignupHandlers['signup'] = async ({
res,
body: { firstName, lastName, email, password },
config,
}) => {
// TODO: Add proper validations with something like Ajv
if (!(firstName && lastName && email && password)) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
// TODO: validate the password and email
// Passwords must be at least 7 characters and contain both alphabetic
// and numeric characters.
try {
await config.storeApiFetch('/v3/customers', {
method: 'POST',
body: JSON.stringify([
{
first_name: firstName,
last_name: lastName,
email,
authentication: {
new_password: password,
},
},
]),
})
} catch (error) {
if (error instanceof BigcommerceApiError && error.status === 422) {
const hasEmailError = '0.email' in error.data?.errors
// If there's an error with the email, it most likely means it's duplicated
if (hasEmailError) {
return res.status(400).json({
data: null,
errors: [
{
message: 'The email is already in use',
code: 'duplicated_email',
},
],
})
}
}
throw error
}
// Login the customer right after creating it
await login({ variables: { email, password }, res, config })
res.status(200).json({ data: null })
}
export default signup

View File

@ -1,46 +0,0 @@
import createApiHandler, {
BigcommerceApiHandler,
BigcommerceHandler,
} from '../utils/create-api-handler'
import isAllowedMethod from '../utils/is-allowed-method'
import { BigcommerceApiError } from '../utils/errors'
import getLoggedInCustomer, {
Customer,
} from './handlers/get-logged-in-customer'
export type { Customer }
export type CustomerData = {
customer: Customer
}
export type CustomersHandlers = {
getLoggedInCustomer: BigcommerceHandler<CustomerData>
}
const METHODS = ['GET']
const customersApi: BigcommerceApiHandler<
CustomerData,
CustomersHandlers
> = async (req, res, config, handlers) => {
if (!isAllowedMethod(req, res, METHODS)) return
try {
const body = null
return await handlers['getLoggedInCustomer']({ 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 }] })
}
}
const handlers = { getLoggedInCustomer }
export default createApiHandler(customersApi, handlers, {})

View File

@ -1,45 +0,0 @@
import createApiHandler, {
BigcommerceApiHandler,
BigcommerceHandler,
} from '../utils/create-api-handler'
import isAllowedMethod from '../utils/is-allowed-method'
import { BigcommerceApiError } from '../utils/errors'
import login from './handlers/login'
export type LoginBody = {
email: string
password: string
}
export type LoginHandlers = {
login: BigcommerceHandler<null, Partial<LoginBody>>
}
const METHODS = ['POST']
const loginApi: BigcommerceApiHandler<null, LoginHandlers> = async (
req,
res,
config,
handlers
) => {
if (!isAllowedMethod(req, res, METHODS)) return
try {
const body = req.body ?? {}
return await handlers['login']({ 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 }] })
}
}
const handlers = { login }
export default createApiHandler(loginApi, handlers, {})

View File

@ -1,42 +0,0 @@
import createApiHandler, {
BigcommerceApiHandler,
BigcommerceHandler,
} from '../utils/create-api-handler'
import isAllowedMethod from '../utils/is-allowed-method'
import { BigcommerceApiError } from '../utils/errors'
import logout from './handlers/logout'
export type LogoutHandlers = {
logout: BigcommerceHandler<null, { redirectTo?: string }>
}
const METHODS = ['GET']
const logoutApi: BigcommerceApiHandler<null, LogoutHandlers> = async (
req,
res,
config,
handlers
) => {
if (!isAllowedMethod(req, res, METHODS)) return
try {
const redirectTo = req.query.redirect_to
const body = typeof redirectTo === 'string' ? { redirectTo } : {}
return await handlers['logout']({ 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 }] })
}
}
const handlers = { logout }
export default createApiHandler(logoutApi, handlers, {})

View File

@ -1,50 +0,0 @@
import createApiHandler, {
BigcommerceApiHandler,
BigcommerceHandler,
} from '../utils/create-api-handler'
import isAllowedMethod from '../utils/is-allowed-method'
import { BigcommerceApiError } from '../utils/errors'
import signup from './handlers/signup'
export type SignupBody = {
firstName: string
lastName: string
email: string
password: string
}
export type SignupHandlers = {
signup: BigcommerceHandler<null, { cartId?: string } & Partial<SignupBody>>
}
const METHODS = ['POST']
const signupApi: BigcommerceApiHandler<null, SignupHandlers> = async (
req,
res,
config,
handlers
) => {
if (!isAllowedMethod(req, res, METHODS)) return
const { cookies } = req
const cartId = cookies[config.cartCookie]
try {
const body = { ...req.body, cartId }
return await handlers['signup']({ 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 }] })
}
}
const handlers = { signup }
export default createApiHandler(signupApi, handlers, {})

File diff suppressed because it is too large Load Diff

View File

@ -1,329 +0,0 @@
/**
* This file was auto-generated by swagger-to-ts.
* Do not make direct changes to the file.
*/
export interface definitions {
blogPost_Full: {
/**
* ID of this blog post. (READ-ONLY)
*/
id?: number
} & definitions['blogPost_Base']
addresses: {
/**
* Full URL of where the resource is located.
*/
url?: string
/**
* Resource being accessed.
*/
resource?: string
}
formField: {
/**
* Name of the form field
*/
name?: string
/**
* Value of the form field
*/
value?: string
}
page_Full: {
/**
* ID of the page.
*/
id?: number
} & definitions['page_Base']
redirect: {
/**
* Numeric ID of the redirect.
*/
id?: number
/**
* The path from which to redirect.
*/
path: string
forward: definitions['forward']
/**
* URL of the redirect. READ-ONLY
*/
url?: string
}
forward: {
/**
* The type of redirect. If it is a `manual` redirect then type will always be manual. Dynamic redirects will have the type of the page. Such as product or category.
*/
type?: string
/**
* Reference of the redirect. Dynamic redirects will have the category or product number. Manual redirects will have the url that is being directed to.
*/
ref?: number
}
customer_Full: {
/**
* Unique numeric ID of this customer. This is a READ-ONLY field; do not set or modify its value in a POST or PUT request.
*/
id?: number
/**
* Not returned in any responses, but accepts up to two fields allowing you to set the customers password. If a password is not supplied, it is generated automatically. For further information about using this object, please see the Customers resource documentation.
*/
_authentication?: {
force_reset?: string
password?: string
password_confirmation?: string
}
/**
* The name of the company for which the customer works.
*/
company?: string
/**
* First name of the customer.
*/
first_name: string
/**
* Last name of the customer.
*/
last_name: string
/**
* Email address of the customer.
*/
email: string
/**
* Phone number of the customer.
*/
phone?: string
/**
* Date on which the customer registered from the storefront or was created in the control panel. This is a READ-ONLY field; do not set or modify its value in a POST or PUT request.
*/
date_created?: string
/**
* Date on which the customer updated their details in the storefront or was updated in the control panel. This is a READ-ONLY field; do not set or modify its value in a POST or PUT request.
*/
date_modified?: string
/**
* The amount of credit the customer has. (Float, Float as String, Integer)
*/
store_credit?: string
/**
* The customers IP address when they signed up.
*/
registration_ip_address?: string
/**
* The group to which the customer belongs.
*/
customer_group_id?: number
/**
* Store-owner notes on the customer.
*/
notes?: string
/**
* Used to identify customers who fall into special sales-tax categories in particular, those who are fully or partially exempt from paying sales tax. Can be blank, or can contain a single AvaTax code. (The codes are case-sensitive.) Stores that subscribe to BigCommerces Avalara Premium integration will use this code to determine how/whether to apply sales tax. Does not affect sales-tax calculations for stores that do not subscribe to Avalara Premium.
*/
tax_exempt_category?: string
/**
* Records whether the customer would like to receive marketing content from this store. READ-ONLY.This is a READ-ONLY field; do not set or modify its value in a POST or PUT request.
*/
accepts_marketing?: boolean
addresses?: definitions['addresses']
/**
* Array of custom fields. This is a READ-ONLY field; do not set or modify its value in a POST or PUT request.
*/
form_fields?: definitions['formField'][]
/**
* Force a password change on next login.
*/
reset_pass_on_login?: boolean
}
categoryAccessLevel: {
/**
* + `all` - Customers can access all categories
* + `specific` - Customers can access a specific list of categories
* + `none` - Customers are prevented from viewing any of the categories in this group.
*/
type?: 'all' | 'specific' | 'none'
/**
* Is an array of category IDs and should be supplied only if `type` is specific.
*/
categories?: string[]
}
timeZone: {
/**
* a string identifying the time zone, in the format: <Continent-name>/<City-name>.
*/
name?: string
/**
* a negative or positive number, identifying the offset from UTC/GMT, in seconds, during winter/standard time.
*/
raw_offset?: number
/**
* "-/+" offset from UTC/GMT, in seconds, during summer/daylight saving time.
*/
dst_offset?: number
/**
* a boolean indicating whether this time zone observes daylight saving time.
*/
dst_correction?: boolean
date_format?: definitions['dateFormat']
}
count_Response: { count?: number }
dateFormat: {
/**
* string that defines dates display format, in the pattern: M jS Y
*/
display?: string
/**
* string that defines the CSV export format for orders, customers, and products, in the pattern: M jS Y
*/
export?: string
/**
* string that defines dates extended-display format, in the pattern: M jS Y @ g:i A.
*/
extended_display?: string
}
blogTags: { tag?: string; post_ids?: number[] }[]
blogPost_Base: {
/**
* Title of this blog post.
*/
title: string
/**
* URL for the public blog post.
*/
url?: string
/**
* URL to preview the blog post. (READ-ONLY)
*/
preview_url?: string
/**
* Text body of the blog post.
*/
body: string
/**
* Tags to characterize the blog post.
*/
tags?: string[]
/**
* Summary of the blog post. (READ-ONLY)
*/
summary?: string
/**
* Whether the blog post is published.
*/
is_published?: boolean
published_date?: definitions['publishedDate']
/**
* Published date in `ISO 8601` format.
*/
published_date_iso8601?: string
/**
* Description text for this blog posts `<meta/>` element.
*/
meta_description?: string
/**
* Keywords for this blog posts `<meta/>` element.
*/
meta_keywords?: string
/**
* Name of the blog posts author.
*/
author?: string
/**
* Local path to a thumbnail uploaded to `product_images/` via [WebDav](https://support.bigcommerce.com/s/article/File-Access-WebDAV).
*/
thumbnail_path?: string
}
publishedDate: { timezone_type?: string; date?: string; timezone?: string }
/**
* Not returned in any responses, but accepts up to two fields allowing you to set the customers password. If a password is not supplied, it is generated automatically. For further information about using this object, please see the Customers resource documentation.
*/
authentication: {
force_reset?: string
password?: string
password_confirmation?: string
}
customer_Base: { [key: string]: any }
page_Base: {
/**
* ID of any parent Web page.
*/
parent_id?: number
/**
* `page`: free-text page
* `link`: link to another web address
* `rss_feed`: syndicated content from an RSS feed
* `contact_form`: When the store's contact form is used.
*/
type: 'page' | 'rss_feed' | 'contact_form' | 'raw' | 'link'
/**
* Where the pages type is a contact form: object whose members are the fields enabled (in the control panel) for storefront display. Possible members are:`fullname`: full name of the customer submitting the form; `phone`: customers phone number, as submitted on the form; `companyname`: customers submitted company name; `orderno`: customers submitted order number; `rma`: customers submitted RMA (Return Merchandise Authorization) number.
*/
contact_fields?: string
/**
* Where the pages type is a contact form: email address that receives messages sent via the form.
*/
email?: string
/**
* Page name, as displayed on the storefront.
*/
name: string
/**
* Relative URL on the storefront for this page.
*/
url?: string
/**
* Description contained within this pages `<meta/>` element.
*/
meta_description?: string
/**
* HTML or variable that populates this pages `<body>` element, in default/desktop view. Required in POST if page type is `raw`.
*/
body: string
/**
* HTML to use for this page's body when viewed in the mobile template (deprecated).
*/
mobile_body?: string
/**
* If true, this page has a mobile version.
*/
has_mobile_version?: boolean
/**
* If true, this page appears in the storefronts navigation menu.
*/
is_visible?: boolean
/**
* If true, this page is the storefronts home page.
*/
is_homepage?: boolean
/**
* Text specified for this pages `<title>` element. (If empty, the value of the name property is used.)
*/
meta_title?: string
/**
* Layout template for this page. This field is writable only for stores with a Blueprint theme applied.
*/
layout_file?: string
/**
* Order in which this page should display on the storefront. (Lower integers specify earlier display.)
*/
sort_order?: number
/**
* Comma-separated list of keywords that shoppers can use to locate this page when searching the store.
*/
search_keywords?: string
/**
* Comma-separated list of SEO-relevant keywords to include in the pages `<meta/>` element.
*/
meta_keywords?: string
/**
* If page type is `rss_feed` the n this field is visisble. Required in POST required for `rss page` type.
*/
feed: string
/**
* If page type is `link` this field is returned. Required in POST to create a `link` page.
*/
link: string
content_type?: 'application/json' | 'text/javascript' | 'text/html'
}
}

View File

@ -1,142 +0,0 @@
/**
* This file was auto-generated by swagger-to-ts.
* Do not make direct changes to the file.
*/
export interface definitions {
wishlist_Post: {
/**
* The customer id.
*/
customer_id: number
/**
* Whether the wishlist is available to the public.
*/
is_public?: boolean
/**
* The title of the wishlist.
*/
name?: string
/**
* Array of Wishlist items.
*/
items?: {
/**
* The ID of the product.
*/
product_id?: number
/**
* The variant ID of the product.
*/
variant_id?: number
}[]
}
wishlist_Put: {
/**
* The customer id.
*/
customer_id: number
/**
* Whether the wishlist is available to the public.
*/
is_public?: boolean
/**
* The title of the wishlist.
*/
name?: string
/**
* Array of Wishlist items.
*/
items?: {
/**
* The ID of the item
*/
id?: number
/**
* The ID of the product.
*/
product_id?: number
/**
* The variant ID of the item.
*/
variant_id?: number
}[]
}
wishlist_Full: {
/**
* Wishlist ID, provided after creating a wishlist with a POST.
*/
id?: number
/**
* The ID the customer to which the wishlist belongs.
*/
customer_id?: number
/**
* The Wishlist's name.
*/
name?: string
/**
* Whether the Wishlist is available to the public.
*/
is_public?: boolean
/**
* The token of the Wishlist. This is created internally within BigCommerce. The Wishlist ID is to be used for external apps. Read-Only
*/
token?: string
/**
* Array of Wishlist items
*/
items?: definitions['wishlistItem_Full'][]
}
wishlistItem_Full: {
/**
* The ID of the item
*/
id?: number
/**
* The ID of the product.
*/
product_id?: number
/**
* The variant ID of the item.
*/
variant_id?: number
}
wishlistItem_Post: {
/**
* The ID of the product.
*/
product_id?: number
/**
* The variant ID of the product.
*/
variant_id?: number
}
/**
* Data about the response, including pagination and collection totals.
*/
pagination: {
/**
* Total number of items in the result set.
*/
total?: number
/**
* Total number of items in the collection response.
*/
count?: number
/**
* The amount of items returned in the collection per page, controlled by the limit parameter.
*/
per_page?: number
/**
* The page you are currently on within the collection.
*/
current_page?: number
/**
* The total number of pages in the collection.
*/
total_pages?: number
}
error: { status?: number; title?: string; type?: string }
metaCollection: { pagination?: definitions['pagination'] }
}

View File

@ -1,9 +0,0 @@
export const categoryTreeItemFragment = /* GraphQL */ `
fragment categoryTreeItem on CategoryTreeItem {
entityId
name
path
description
productCount
}
`

View File

@ -1,106 +0,0 @@
export const swatchOptionFragment = /* GraphQL */ `
fragment swatchOption on SwatchOptionValue {
isDefault
hexColors
}
`
export const multipleChoiceOptionFragment = /* GraphQL */ `
fragment multipleChoiceOption on MultipleChoiceOption {
entityId
values {
edges {
node {
label
...swatchOption
}
}
}
}
${swatchOptionFragment}
`
export const productInfoFragment = /* GraphQL */ `
fragment productInfo on Product {
entityId
name
path
brand {
entityId
}
description
prices {
price {
value
currencyCode
}
salePrice {
value
currencyCode
}
retailPrice {
value
currencyCode
}
}
images {
edges {
node {
urlOriginal
altText
isDefault
}
}
}
variants {
edges {
node {
entityId
defaultImage {
urlOriginal
altText
isDefault
}
}
}
}
productOptions {
edges {
node {
entityId
displayName
...multipleChoiceOption
}
}
}
localeMeta: metafields(namespace: $locale, keys: ["name", "description"])
@include(if: $hasLocale) {
edges {
node {
key
value
}
}
}
}
${multipleChoiceOptionFragment}
`
export const productConnectionFragment = /* GraphQL */ `
fragment productConnnection on ProductConnection {
pageInfo {
startCursor
endCursor
}
edges {
cursor
node {
...productInfo
}
}
}
${productInfoFragment}
`

View File

@ -1,85 +0,0 @@
import type { RequestInit } from '@vercel/fetch'
import type { CommerceAPIConfig } from '../../commerce/api'
import fetchGraphqlApi from './utils/fetch-graphql-api'
import fetchStoreApi from './utils/fetch-store-api'
export interface BigcommerceConfig extends CommerceAPIConfig {
// Indicates if the returned metadata with translations should be applied to the
// data or returned as it is
applyLocale?: boolean
storeApiUrl: string
storeApiToken: string
storeApiClientId: string
storeApiFetch<T>(endpoint: string, options?: RequestInit): Promise<T>
}
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(
`The environment variable BIGCOMMERCE_STOREFRONT_API_URL is missing and it's required to access your store`
)
}
if (!API_TOKEN) {
throw new Error(
`The environment variable BIGCOMMERCE_STOREFRONT_API_TOKEN is missing and it's required to access your store`
)
}
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: Omit<BigcommerceConfig, 'customerCookie'>) {
this.config = {
...config,
// The customerCookie is not customizable for now, BC sets the cookie and it's
// not important to rename it
customerCookie: 'SHOP_TOKEN',
}
}
getConfig(userConfig: Partial<BigcommerceConfig> = {}) {
return Object.entries(userConfig).reduce<BigcommerceConfig>(
(cfg, [key, value]) => Object.assign(cfg, { [key]: value }),
{ ...this.config }
)
}
setConfig(newConfig: Partial<BigcommerceConfig>) {
Object.assign(this.config, newConfig)
}
}
const ONE_DAY = 60 * 60 * 24
const config = new Config({
commerceUrl: API_URL,
apiToken: API_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,
storeApiFetch: fetchStoreApi,
})
export function getConfig(userConfig?: Partial<BigcommerceConfig>) {
return config.getConfig(userConfig)
}
export function setConfig(newConfig: Partial<BigcommerceConfig>) {
return config.setConfig(newConfig)
}

View File

@ -1,43 +0,0 @@
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import { BigcommerceConfig, getConfig } from '..'
import { definitions } from '../definitions/store-content'
export type Page = definitions['page_Full']
export type GetAllPagesResult<
T extends { pages: any[] } = { pages: Page[] }
> = T
async function getAllPages(opts?: {
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetAllPagesResult>
async function getAllPages<T extends { pages: any[] }>(opts: {
url: string
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetAllPagesResult<T>>
async function getAllPages({
config,
preview,
}: {
url?: string
config?: BigcommerceConfig
preview?: boolean
} = {}): Promise<GetAllPagesResult> {
config = getConfig(config)
// RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `url`
const { data } = await config.storeApiFetch<
RecursivePartial<{ data: Page[] }>
>('/v3/content/pages')
const pages = (data as RecursiveRequired<typeof data>) ?? []
return {
pages: preview ? pages : pages.filter((p) => p.is_visible),
}
}
export default getAllPages

View File

@ -1,71 +0,0 @@
import type {
GetAllProductPathsQuery,
GetAllProductPathsQueryVariables,
} from '../../schema'
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import filterEdges from '../utils/filter-edges'
import { BigcommerceConfig, getConfig } from '..'
export const getAllProductPathsQuery = /* GraphQL */ `
query getAllProductPaths($first: Int = 100) {
site {
products(first: $first) {
edges {
node {
path
}
}
}
}
}
`
export type ProductPath = NonNullable<
NonNullable<GetAllProductPathsQuery['site']['products']['edges']>[0]
>
export type ProductPaths = ProductPath[]
export type { GetAllProductPathsQueryVariables }
export type GetAllProductPathsResult<
T extends { products: any[] } = { products: ProductPaths }
> = T
async function getAllProductPaths(opts?: {
variables?: GetAllProductPathsQueryVariables
config?: BigcommerceConfig
}): Promise<GetAllProductPathsResult>
async function getAllProductPaths<
T extends { products: any[] },
V = any
>(opts: {
query: string
variables?: V
config?: BigcommerceConfig
}): Promise<GetAllProductPathsResult<T>>
async function getAllProductPaths({
query = getAllProductPathsQuery,
variables,
config,
}: {
query?: string
variables?: GetAllProductPathsQueryVariables
config?: BigcommerceConfig
} = {}): Promise<GetAllProductPathsResult> {
config = getConfig(config)
// RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `query`
const { data } = await config.fetch<
RecursivePartial<GetAllProductPathsQuery>
>(query, { variables })
const products = data.site?.products?.edges
return {
products: filterEdges(products as RecursiveRequired<typeof products>),
}
}
export default getAllProductPaths

View File

@ -1,129 +0,0 @@
import type {
GetAllProductsQuery,
GetAllProductsQueryVariables,
} from '../../schema'
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import filterEdges from '../utils/filter-edges'
import setProductLocaleMeta from '../utils/set-product-locale-meta'
import { productConnectionFragment } from '../fragments/product'
import { BigcommerceConfig, getConfig } from '..'
export const getAllProductsQuery = /* GraphQL */ `
query getAllProducts(
$hasLocale: Boolean = false
$locale: String = "null"
$entityIds: [Int!]
$first: Int = 10
$products: Boolean = false
$featuredProducts: Boolean = false
$bestSellingProducts: Boolean = false
$newestProducts: Boolean = false
) {
site {
products(first: $first, entityIds: $entityIds) @include(if: $products) {
...productConnnection
}
featuredProducts(first: $first) @include(if: $featuredProducts) {
...productConnnection
}
bestSellingProducts(first: $first) @include(if: $bestSellingProducts) {
...productConnnection
}
newestProducts(first: $first) @include(if: $newestProducts) {
...productConnnection
}
}
}
${productConnectionFragment}
`
export type ProductEdge = NonNullable<
NonNullable<GetAllProductsQuery['site']['products']['edges']>[0]
>
export type ProductNode = ProductEdge['node']
export type GetAllProductsResult<
T extends Record<keyof GetAllProductsResult, any[]> = {
products: ProductEdge[]
}
> = T
const FIELDS = [
'products',
'featuredProducts',
'bestSellingProducts',
'newestProducts',
]
export type ProductTypes =
| 'products'
| 'featuredProducts'
| 'bestSellingProducts'
| 'newestProducts'
export type ProductVariables = { field?: ProductTypes } & Omit<
GetAllProductsQueryVariables,
ProductTypes | 'hasLocale'
>
async function getAllProducts(opts?: {
variables?: ProductVariables
config?: BigcommerceConfig
}): Promise<GetAllProductsResult>
async function getAllProducts<
T extends Record<keyof GetAllProductsResult, any[]>,
V = any
>(opts: {
query: string
variables?: V
config?: BigcommerceConfig
}): Promise<GetAllProductsResult<T>>
async function getAllProducts({
query = getAllProductsQuery,
variables: { field = 'products', ...vars } = {},
config,
}: {
query?: string
variables?: ProductVariables
config?: BigcommerceConfig
} = {}): Promise<GetAllProductsResult> {
config = getConfig(config)
const locale = vars.locale || config.locale
const variables: GetAllProductsQueryVariables = {
...vars,
locale,
hasLocale: !!locale,
}
if (!FIELDS.includes(field)) {
throw new Error(
`The field variable has to match one of ${FIELDS.join(', ')}`
)
}
variables[field] = true
// RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `query`
const { data } = await config.fetch<RecursivePartial<GetAllProductsQuery>>(
query,
{ variables }
)
const edges = data.site?.[field]?.edges
const products = filterEdges(edges as RecursiveRequired<typeof edges>)
if (locale && config.applyLocale) {
products.forEach((product: RecursivePartial<ProductEdge>) => {
if (product.node) setProductLocaleMeta(product.node)
})
}
return { products }
}
export default getAllProducts

View File

@ -1,34 +0,0 @@
import { GetCustomerIdQuery } from '../../schema'
import { BigcommerceConfig, getConfig } from '..'
export const getCustomerIdQuery = /* GraphQL */ `
query getCustomerId {
customer {
entityId
}
}
`
async function getCustomerId({
customerToken,
config,
}: {
customerToken: string
config?: BigcommerceConfig
}): Promise<number | undefined> {
config = getConfig(config)
const { data } = await config.fetch<GetCustomerIdQuery>(
getCustomerIdQuery,
undefined,
{
headers: {
cookie: `${config.customerCookie}=${customerToken}`,
},
}
)
return data?.customer?.entityId
}
export default getCustomerId

View File

@ -1,51 +0,0 @@
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import { BigcommerceConfig, getConfig } from '..'
import { definitions } from '../definitions/wishlist'
export type Wishlist = definitions['wishlist_Full']
export type GetCustomerWishlistResult<
T extends { wishlist?: any } = { wishlist?: Wishlist }
> = T
export type GetCustomerWishlistVariables = {
customerId: number
}
async function getCustomerWishlist(opts: {
variables: GetCustomerWishlistVariables
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetCustomerWishlistResult>
async function getCustomerWishlist<
T extends { wishlist?: any },
V = any
>(opts: {
url: string
variables: V
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetCustomerWishlistResult<T>>
async function getCustomerWishlist({
config,
variables,
}: {
url?: string
variables: GetCustomerWishlistVariables
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetCustomerWishlistResult> {
config = getConfig(config)
const { data } = await config.storeApiFetch<
RecursivePartial<{ data: Wishlist[] }>
>(`/v3/wishlists?customer_id=${variables.customerId}`)
const wishlists = (data as RecursiveRequired<typeof data>) ?? []
const wishlist = wishlists[0]
return { wishlist }
}
export default getCustomerWishlist

View File

@ -1,53 +0,0 @@
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import { BigcommerceConfig, getConfig } from '..'
import { definitions } from '../definitions/store-content'
export type Page = definitions['page_Full']
export type GetPageResult<T extends { page?: any } = { page?: Page }> = T
export type PageVariables = {
id: number
}
async function getPage(opts: {
url?: string
variables: PageVariables
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetPageResult>
async function getPage<T extends { page?: any }, V = any>(opts: {
url: string
variables: V
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetPageResult<T>>
async function getPage({
url,
variables,
config,
preview,
}: {
url?: string
variables: PageVariables
config?: BigcommerceConfig
preview?: boolean
}): Promise<GetPageResult> {
config = getConfig(config)
// RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `url`
const { data } = await config.storeApiFetch<RecursivePartial<{ data: Page[] }>>(
url || `/v3/content/pages?id=${variables.id}&include=body`
)
const firstPage = data?.[0]
const page = firstPage as RecursiveRequired<typeof firstPage>
if (preview || page?.is_visible) {
return { page }
}
return {}
}
export default getPage

View File

@ -1,90 +0,0 @@
import type { GetProductQuery, GetProductQueryVariables } from '../../schema'
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import setProductLocaleMeta from '../utils/set-product-locale-meta'
import { productInfoFragment } from '../fragments/product'
import { BigcommerceConfig, getConfig } from '..'
export const getProductQuery = /* GraphQL */ `
query getProduct(
$hasLocale: Boolean = false
$locale: String = "null"
$path: String!
) {
site {
route(path: $path) {
node {
__typename
... on Product {
...productInfo
}
}
}
}
}
${productInfoFragment}
`
export type ProductNode = Extract<
GetProductQuery['site']['route']['node'],
{ __typename: 'Product' }
>
export type GetProductResult<
T extends { product?: any } = { product?: ProductNode }
> = T
export type ProductVariables = { locale?: string } & (
| { path: string; slug?: never }
| { path?: never; slug: string }
)
async function getProduct(opts: {
variables: ProductVariables
config?: BigcommerceConfig
}): Promise<GetProductResult>
async function getProduct<T extends { product?: any }, V = any>(opts: {
query: string
variables: V
config?: BigcommerceConfig
}): Promise<GetProductResult<T>>
async function getProduct({
query = getProductQuery,
variables: { slug, ...vars },
config,
}: {
query?: string
variables: ProductVariables
config?: BigcommerceConfig
}): Promise<GetProductResult> {
config = getConfig(config)
const locale = vars.locale || config.locale
const variables: GetProductQueryVariables = {
...vars,
locale,
hasLocale: !!locale,
path: slug ? `/${slug}/` : vars.path!,
}
const { data } = await config.fetch<RecursivePartial<GetProductQuery>>(
query,
{ variables }
)
const product = data.site?.route?.node
if (product?.__typename === 'Product') {
if (locale && config.applyLocale) {
setProductLocaleMeta(product)
}
return {
product: product as RecursiveRequired<typeof product>,
}
}
return {}
}
export default getProduct

View File

@ -1,103 +0,0 @@
import type { GetSiteInfoQuery, GetSiteInfoQueryVariables } from '../../schema'
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import filterEdges from '../utils/filter-edges'
import { BigcommerceConfig, getConfig } from '..'
import { categoryTreeItemFragment } from '../fragments/category-tree'
// Get 3 levels of categories
export const getSiteInfoQuery = /* GraphQL */ `
query getSiteInfo {
site {
categoryTree {
...categoryTreeItem
children {
...categoryTreeItem
children {
...categoryTreeItem
}
}
}
brands {
pageInfo {
startCursor
endCursor
}
edges {
cursor
node {
entityId
name
defaultImage {
urlOriginal
altText
}
pageTitle
metaDesc
metaKeywords
searchKeywords
path
}
}
}
}
}
${categoryTreeItemFragment}
`
export type CategoriesTree = NonNullable<
GetSiteInfoQuery['site']['categoryTree']
>
export type BrandEdge = NonNullable<
NonNullable<GetSiteInfoQuery['site']['brands']['edges']>[0]
>
export type Brands = BrandEdge[]
export type GetSiteInfoResult<
T extends { categories: any[]; brands: any[] } = {
categories: CategoriesTree
brands: Brands
}
> = T
async function getSiteInfo(opts?: {
variables?: GetSiteInfoQueryVariables
config?: BigcommerceConfig
}): Promise<GetSiteInfoResult>
async function getSiteInfo<
T extends { categories: any[]; brands: any[] },
V = any
>(opts: {
query: string
variables?: V
config?: BigcommerceConfig
}): Promise<GetSiteInfoResult<T>>
async function getSiteInfo({
query = getSiteInfoQuery,
variables,
config,
}: {
query?: string
variables?: GetSiteInfoQueryVariables
config?: BigcommerceConfig
} = {}): Promise<GetSiteInfoResult> {
config = getConfig(config)
// RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `query`
const { data } = await config.fetch<RecursivePartial<GetSiteInfoQuery>>(
query,
{ variables }
)
const categories = data.site?.categoryTree
const brands = data.site?.brands?.edges
return {
categories: (categories as RecursiveRequired<typeof categories>) ?? [],
brands: filterEdges(brands as RecursiveRequired<typeof brands>),
}
}
export default getSiteInfo

View File

@ -1,71 +0,0 @@
import type { ServerResponse } from 'http'
import type { LoginMutation, LoginMutationVariables } from '../../schema'
import type { RecursivePartial } from '../utils/types'
import concatHeader from '../utils/concat-cookie'
import { BigcommerceConfig, getConfig } from '..'
export const loginMutation = /* GraphQL */ `
mutation login($email: String!, $password: String!) {
login(email: $email, password: $password) {
result
}
}
`
export type LoginResult<T extends { result?: any } = { result?: string }> = T
export type LoginVariables = LoginMutationVariables
async function login(opts: {
variables: LoginVariables
config?: BigcommerceConfig
res: ServerResponse
}): Promise<LoginResult>
async function login<T extends { result?: any }, V = any>(opts: {
query: string
variables: V
res: ServerResponse
config?: BigcommerceConfig
}): Promise<LoginResult<T>>
async function login({
query = loginMutation,
variables,
res: response,
config,
}: {
query?: string
variables: LoginVariables
res: ServerResponse
config?: BigcommerceConfig
}): Promise<LoginResult> {
config = getConfig(config)
const { data, res } = await config.fetch<RecursivePartial<LoginMutation>>(
query,
{ variables }
)
// Bigcommerce returns a Set-Cookie header with the auth cookie
let cookie = res.headers.get('Set-Cookie')
if (cookie && typeof cookie === 'string') {
// In development, don't set a secure cookie or the browser will ignore it
if (process.env.NODE_ENV !== 'production') {
cookie = cookie.replace('; Secure', '')
// SameSite=none can't be set unless the cookie is Secure
cookie = cookie.replace('; SameSite=none', '; SameSite=lax')
}
response.setHeader(
'Set-Cookie',
concatHeader(response.getHeader('Set-Cookie'), cookie)!
)
}
return {
result: data.login?.result,
}
}
export default login

View File

@ -1,14 +0,0 @@
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

@ -1,58 +0,0 @@
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

@ -1,25 +0,0 @@
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

@ -1,38 +0,0 @@
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

@ -1,71 +0,0 @@
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

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

View File

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

View File

@ -1,20 +0,0 @@
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

@ -1,28 +0,0 @@
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

@ -1,13 +0,0 @@
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,
})

View File

@ -1,21 +0,0 @@
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

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

View File

@ -1,56 +0,0 @@
import type { WishlistHandlers } from '..'
import getCustomerId from '../../operations/get-customer-id'
import getCustomerWishlist from '../../operations/get-customer-wishlist'
import { parseWishlistItem } from '../../utils/parse-item'
// Returns the wishlist of the signed customer
const addItem: WishlistHandlers['addItem'] = async ({
res,
body: { customerToken, item },
config,
}) => {
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
const customerId =
customerToken && (await getCustomerId({ customerToken, config }))
if (!customerId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const { wishlist } = await getCustomerWishlist({
variables: { customerId },
config,
})
const options = {
method: 'POST',
body: JSON.stringify(
wishlist
? {
items: [parseWishlistItem(item)],
}
: {
name: 'Wishlist',
customer_id: customerId,
items: [parseWishlistItem(item)],
is_public: false,
}
),
}
const { data } = wishlist
? await config.storeApiFetch(`/v3/wishlists/${wishlist.id}/items`, options)
: await config.storeApiFetch('/v3/wishlists', options)
res.status(200).json({ data })
}
export default addItem

View File

@ -1,35 +0,0 @@
import getCustomerId from '../../operations/get-customer-id'
import type { Wishlist, WishlistHandlers } from '..'
import getCustomerWishlist from '../../operations/get-customer-wishlist'
// Return wishlist info
const getWishlist: WishlistHandlers['getWishlist'] = async ({
res,
body: { customerToken },
config,
}) => {
let result: { data?: Wishlist } = {}
if (customerToken) {
const customerId =
customerToken && (await getCustomerId({ customerToken, config }))
if (!customerId) {
// If the customerToken is invalid, then this request is too
return res.status(404).json({
data: null,
errors: [{ message: 'Wishlist not found' }],
})
}
const { wishlist } = await getCustomerWishlist({
variables: { customerId },
config,
})
result = { data: wishlist }
}
res.status(200).json({ data: result.data ?? null })
}
export default getWishlist

View File

@ -1,39 +0,0 @@
import getCustomerId from '../../operations/get-customer-id'
import getCustomerWishlist, {
Wishlist,
} from '../../operations/get-customer-wishlist'
import type { WishlistHandlers } from '..'
// Return current wishlist info
const removeItem: WishlistHandlers['removeItem'] = async ({
res,
body: { customerToken, itemId },
config,
}) => {
const customerId =
customerToken && (await getCustomerId({ customerToken, config }))
const { wishlist } =
(customerId &&
(await getCustomerWishlist({
variables: { customerId },
config,
}))) ||
{}
if (!wishlist || !itemId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const result = await config.storeApiFetch<{ data: Wishlist } | null>(
`/v3/wishlists/${wishlist.id}/items/${itemId}`,
{ method: 'DELETE' }
)
const data = result?.data ?? null
res.status(200).json({ data })
}
export default removeItem

View File

@ -1,94 +0,0 @@
import isAllowedMethod from '../utils/is-allowed-method'
import createApiHandler, {
BigcommerceApiHandler,
BigcommerceHandler,
} from '../utils/create-api-handler'
import { BigcommerceApiError } from '../utils/errors'
import getWishlist from './handlers/get-wishlist'
import addItem from './handlers/add-item'
import removeItem from './handlers/remove-item'
import { definitions } from '../definitions/wishlist'
export type ItemBody = {
productId: number
variantId: number
}
export type AddItemBody = { item: ItemBody }
export type RemoveItemBody = { itemId: string }
export type WishlistBody = {
customer_id: number
is_public: number
name: string
items: any[]
}
export type AddWishlistBody = { wishlist: WishlistBody }
export type Wishlist = definitions['wishlist_Full']
export type WishlistHandlers = {
getWishlist: BigcommerceHandler<Wishlist, { customerToken?: string }>
addItem: BigcommerceHandler<
Wishlist,
{ customerToken?: string } & Partial<AddItemBody>
>
removeItem: BigcommerceHandler<
Wishlist,
{ customerToken?: string } & Partial<RemoveItemBody>
>
}
const METHODS = ['GET', 'POST', 'DELETE']
// TODO: a complete implementation should have schema validation for `req.body`
const wishlistApi: BigcommerceApiHandler<Wishlist, WishlistHandlers> = async (
req,
res,
config,
handlers
) => {
if (!isAllowedMethod(req, res, METHODS)) return
const { cookies } = req
const customerToken = cookies[config.customerCookie]
try {
// Return current wishlist info
if (req.method === 'GET') {
const body = { customerToken }
return await handlers['getWishlist']({ req, res, config, body })
}
// Add an item to the wishlist
if (req.method === 'POST') {
const body = { ...req.body, customerToken }
return await handlers['addItem']({ req, res, config, body })
}
// Remove an item from the wishlist
if (req.method === 'DELETE') {
const body = { ...req.body, customerToken }
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 = {
getWishlist,
addItem,
removeItem,
}
export default createApiHandler(wishlistApi, handlers, {})

View File

@ -1,56 +0,0 @@
import { useCallback } from 'react'
import type { HookFetcher } from '../../commerce/utils/types'
import { CommerceError } from '../../commerce/utils/errors'
import useCartAddItem from '../../commerce/cart/use-add-item'
import type { ItemBody, AddItemBody } from '../api/cart'
import useCart, { Cart } from './use-cart'
const defaultOpts = {
url: '/api/bigcommerce/cart',
method: 'POST',
}
export type AddItemInput = ItemBody
export const fetcher: HookFetcher<Cart, AddItemBody> = (
options,
{ item },
fetch
) => {
if (
item.quantity &&
(!Number.isInteger(item.quantity) || item.quantity! < 1)
) {
throw new CommerceError({
message: 'The item quantity has to be a valid integer greater than 0',
})
}
return fetch({
...defaultOpts,
...options,
body: { item },
})
}
export function extendHook(customFetcher: typeof fetcher) {
const useAddItem = () => {
const { mutate } = useCart()
const fn = useCartAddItem(defaultOpts, customFetcher)
return useCallback(
async function addItem(input: AddItemInput) {
const data = await fn({ item: input })
await mutate(data, false)
return data
},
[fn, mutate]
)
}
useAddItem.extend = extendHook
return useAddItem
}
export default extendHook(fetcher)

View File

@ -1,13 +0,0 @@
import useAddItem from './use-add-item'
import useRemoveItem from './use-remove-item'
import useUpdateItem from './use-update-item'
// This hook is probably not going to be used, but it's here
// to show how a commerce should be structuring it
export default function useCartActions() {
const addItem = useAddItem()
const updateItem = useUpdateItem()
const removeItem = useRemoveItem()
return { addItem, updateItem, removeItem }
}

View File

@ -1,50 +0,0 @@
import type { HookFetcher } from '../../commerce/utils/types'
import type { SwrOptions } from '../../commerce/utils/use-data'
import useCommerceCart, { CartInput } from '../../commerce/cart/use-cart'
import type { Cart } from '../api/cart'
const defaultOpts = {
url: '/api/bigcommerce/cart',
method: 'GET',
}
export type { Cart }
export const fetcher: HookFetcher<Cart | null, CartInput> = (
options,
{ cartId },
fetch
) => {
return cartId ? fetch({ ...defaultOpts, ...options }) : null
}
export function extendHook(
customFetcher: typeof fetcher,
swrOptions?: SwrOptions<Cart | null, CartInput>
) {
const useCart = () => {
const response = useCommerceCart(defaultOpts, [], customFetcher, {
revalidateOnFocus: false,
...swrOptions,
})
// Uses a getter to only calculate the prop when required
// response.data is also a getter and it's better to not trigger it early
Object.defineProperty(response, 'isEmpty', {
get() {
return Object.values(response.data?.line_items ?? {}).every(
(items) => !items.length
)
},
set: (x) => x,
})
return response
}
useCart.extend = extendHook
return useCart
}
export default extendHook(fetcher)

View File

@ -1,51 +0,0 @@
import { useCallback } from 'react'
import { HookFetcher } from '../../commerce/utils/types'
import useCartRemoveItem from '../../commerce/cart/use-remove-item'
import type { RemoveItemBody } from '../api/cart'
import useCart, { Cart } from './use-cart'
const defaultOpts = {
url: '/api/bigcommerce/cart',
method: 'DELETE',
}
export type RemoveItemInput = {
id: string
}
export const fetcher: HookFetcher<Cart | null, RemoveItemBody> = (
options,
{ itemId },
fetch
) => {
return fetch({
...defaultOpts,
...options,
body: { itemId },
})
}
export function extendHook(customFetcher: typeof fetcher) {
const useRemoveItem = () => {
const { mutate } = useCart()
const fn = useCartRemoveItem<Cart | null, RemoveItemBody>(
defaultOpts,
customFetcher
)
return useCallback(
async function removeItem(input: RemoveItemInput) {
const data = await fn({ itemId: input.id })
await mutate(data, false)
return data
},
[fn, mutate]
)
}
useRemoveItem.extend = extendHook
return useRemoveItem
}
export default extendHook(fetcher)

View File

@ -1,70 +0,0 @@
import { useCallback } from 'react'
import debounce from 'lodash.debounce'
import type { HookFetcher } from '../../commerce/utils/types'
import { CommerceError } from '../../commerce/utils/errors'
import useCartUpdateItem from '../../commerce/cart/use-update-item'
import type { ItemBody, UpdateItemBody } from '../api/cart'
import { fetcher as removeFetcher } from './use-remove-item'
import useCart, { Cart } from './use-cart'
const defaultOpts = {
url: '/api/bigcommerce/cart',
method: 'PUT',
}
export type UpdateItemInput = Partial<{ id: string } & ItemBody>
export const fetcher: HookFetcher<Cart | null, UpdateItemBody> = (
options,
{ itemId, item },
fetch
) => {
if (Number.isInteger(item.quantity)) {
// Also allow the update hook to remove an item if the quantity is lower than 1
if (item.quantity! < 1) {
return removeFetcher(null, { itemId }, fetch)
}
} else if (item.quantity) {
throw new CommerceError({
message: 'The item quantity has to be a valid integer',
})
}
return fetch({
...defaultOpts,
...options,
body: { itemId, item },
})
}
function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
const useUpdateItem = (item?: any) => {
const { mutate } = useCart()
const fn = useCartUpdateItem<Cart | null, UpdateItemBody>(
defaultOpts,
customFetcher
)
return useCallback(
debounce(async (input: UpdateItemInput) => {
const data = await fn({
itemId: input.id ?? item?.id,
item: {
productId: input.productId ?? item?.product_id,
variantId: input.productId ?? item?.variant_id,
quantity: input.quantity,
},
})
await mutate(data, false)
return data
}, cfg?.wait ?? 500),
[fn, mutate]
)
}
useUpdateItem.extend = extendHook
return useUpdateItem
}
export default extendHook(fetcher)

View File

@ -1,60 +0,0 @@
import { ReactNode } from 'react'
import {
CommerceConfig,
CommerceProvider as CoreCommerceProvider,
useCommerce as useCoreCommerce,
} from '../commerce'
import { FetcherError } from '../commerce/utils/errors'
async function getText(res: Response) {
try {
return (await res.text()) || res.statusText
} catch (error) {
return res.statusText
}
}
async function getError(res: Response) {
if (res.headers.get('Content-Type')?.includes('application/json')) {
const data = await res.json()
return new FetcherError({ errors: data.errors, status: res.status })
}
return new FetcherError({ message: await getText(res), status: res.status })
}
export const bigcommerceConfig: CommerceConfig = {
locale: 'en-us',
cartCookie: 'bc_cartId',
async fetcher({ url, method = 'GET', variables, body: bodyObj }) {
const hasBody = Boolean(variables || bodyObj)
const body = hasBody
? JSON.stringify(variables ? { variables } : bodyObj)
: undefined
const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined
const res = await fetch(url!, { method, body, headers })
if (res.ok) {
const { data } = await res.json()
return data
}
throw await getError(res)
},
}
export type BigcommerceConfig = Partial<CommerceConfig>
export type BigcommerceProps = {
children?: ReactNode
locale: string
} & BigcommerceConfig
export function CommerceProvider({ children, ...config }: BigcommerceProps) {
return (
<CoreCommerceProvider config={{ ...bigcommerceConfig, ...config }}>
{children}
</CoreCommerceProvider>
)
}
export const useCommerce = () => useCoreCommerce()

View File

@ -1,64 +0,0 @@
import type { HookFetcher } from '../../commerce/utils/types'
import type { SwrOptions } from '../../commerce/utils/use-data'
import useCommerceSearch from '../../commerce/products/use-search'
import type { SearchProductsData } from '../api/catalog/products'
const defaultOpts = {
url: '/api/bigcommerce/catalog/products',
method: 'GET',
}
export type SearchProductsInput = {
search?: string
categoryId?: number
brandId?: number
sort?: string
}
export const fetcher: HookFetcher<SearchProductsData, SearchProductsInput> = (
options,
{ search, categoryId, brandId, sort },
fetch
) => {
// Use a dummy base as we only care about the relative path
const url = new URL(options?.url ?? defaultOpts.url, 'http://a')
if (search) url.searchParams.set('search', search)
if (Number.isInteger(categoryId))
url.searchParams.set('category', String(categoryId))
if (Number.isInteger(categoryId))
url.searchParams.set('brand', String(brandId))
if (sort) url.searchParams.set('sort', sort)
return fetch({
url: url.pathname + url.search,
method: options?.method ?? defaultOpts.method,
})
}
export function extendHook(
customFetcher: typeof fetcher,
swrOptions?: SwrOptions<SearchProductsData, SearchProductsInput>
) {
const useSearch = (input: SearchProductsInput = {}) => {
const response = useCommerceSearch(
defaultOpts,
[
['search', input.search],
['categoryId', input.categoryId],
['brandId', input.brandId],
['sort', input.sort],
],
customFetcher,
{ revalidateOnFocus: false, ...swrOptions }
)
return response
}
useSearch.extend = extendHook
return useSearch
}
export default extendHook(fetcher)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,49 +0,0 @@
/**
* Generates definitions for REST API endpoints that are being
* used by ../api using https://github.com/drwpow/swagger-to-ts
*/
const { readFileSync, promises } = require('fs')
const path = require('path')
const fetch = require('node-fetch')
const swaggerToTS = require('@manifoldco/swagger-to-ts').default
async function getSchema(filename) {
const url = `https://next-api.stoplight.io/projects/8433/files/${filename}`
const res = await fetch(url)
if (!res.ok) {
throw new Error(`Request failed with ${res.status}: ${res.statusText}`)
}
return res.json()
}
const schemas = Object.entries({
'../api/definitions/catalog.ts':
'BigCommerce_Catalog_API.oas2.yml?ref=version%2F20.930',
'../api/definitions/store-content.ts':
'BigCommerce_Store_Content_API.oas2.yml?ref=version%2F20.930',
'../api/definitions/wishlist.ts':
'BigCommerce_Wishlist_API.oas2.yml?ref=version%2F20.930',
// swagger-to-ts is not working for the schema of the cart API
// '../api/definitions/cart.ts':
// 'BigCommerce_Server_to_Server_Cart_API.oas2.yml',
})
async function writeDefinitions() {
const ops = schemas.map(async ([dest, filename]) => {
const destination = path.join(__dirname, dest)
const schema = await getSchema(filename)
const definition = swaggerToTS(schema.content, {
prettierConfig: 'package.json',
})
await promises.writeFile(destination, definition)
console.log(`✔️ Added definitions for: ${dest}`)
})
await Promise.all(ops)
}
writeDefinitions()

View File

@ -1,38 +0,0 @@
import type { HookFetcher } from '../commerce/utils/types'
import type { SwrOptions } from '../commerce/utils/use-data'
import useCommerceCustomer from '../commerce/use-customer'
import type { Customer, CustomerData } from './api/customers'
const defaultOpts = {
url: '/api/bigcommerce/customers',
method: 'GET',
}
export type { Customer }
export const fetcher: HookFetcher<Customer | null> = async (
options,
_,
fetch
) => {
const data = await fetch<CustomerData | null>({ ...defaultOpts, ...options })
return data?.customer ?? null
}
export function extendHook(
customFetcher: typeof fetcher,
swrOptions?: SwrOptions<Customer | null>
) {
const useCustomer = () => {
return useCommerceCustomer(defaultOpts, [], customFetcher, {
revalidateOnFocus: false,
...swrOptions,
})
}
useCustomer.extend = extendHook
return useCustomer
}
export default extendHook(fetcher)

View File

@ -1,54 +0,0 @@
import { useCallback } from 'react'
import type { HookFetcher } from '../commerce/utils/types'
import { CommerceError } from '../commerce/utils/errors'
import useCommerceLogin from '../commerce/use-login'
import type { LoginBody } from './api/customers/login'
import useCustomer from './use-customer'
const defaultOpts = {
url: '/api/bigcommerce/customers/login',
method: 'POST',
}
export type LoginInput = LoginBody
export const fetcher: HookFetcher<null, LoginBody> = (
options,
{ email, password },
fetch
) => {
if (!(email && password)) {
throw new CommerceError({
message:
'A first name, last name, email and password are required to login',
})
}
return fetch({
...defaultOpts,
...options,
body: { email, password },
})
}
export function extendHook(customFetcher: typeof fetcher) {
const useLogin = () => {
const { revalidate } = useCustomer()
const fn = useCommerceLogin<null, LoginInput>(defaultOpts, customFetcher)
return useCallback(
async function login(input: LoginInput) {
const data = await fn(input)
await revalidate()
return data
},
[fn]
)
}
useLogin.extend = extendHook
return useLogin
}
export default extendHook(fetcher)

View File

@ -1,38 +0,0 @@
import { useCallback } from 'react'
import type { HookFetcher } from '../commerce/utils/types'
import useCommerceLogout from '../commerce/use-logout'
import useCustomer from './use-customer'
const defaultOpts = {
url: '/api/bigcommerce/customers/logout',
method: 'GET',
}
export const fetcher: HookFetcher<null> = (options, _, fetch) => {
return fetch({
...defaultOpts,
...options,
})
}
export function extendHook(customFetcher: typeof fetcher) {
const useLogout = () => {
const { mutate } = useCustomer()
const fn = useCommerceLogout<null>(defaultOpts, customFetcher)
return useCallback(
async function login() {
const data = await fn(null)
await mutate(null, false)
return data
},
[fn]
)
}
useLogout.extend = extendHook
return useLogout
}
export default extendHook(fetcher)

View File

@ -1,2 +0,0 @@
export * from '../commerce/use-price'
export { default } from '../commerce/use-price'

View File

@ -1,54 +0,0 @@
import { useCallback } from 'react'
import type { HookFetcher } from '../commerce/utils/types'
import { CommerceError } from '../commerce/utils/errors'
import useCommerceSignup from '../commerce/use-signup'
import type { SignupBody } from './api/customers/signup'
import useCustomer from './use-customer'
const defaultOpts = {
url: '/api/bigcommerce/customers/signup',
method: 'POST',
}
export type SignupInput = SignupBody
export const fetcher: HookFetcher<null, SignupBody> = (
options,
{ firstName, lastName, email, password },
fetch
) => {
if (!(firstName && lastName && email && password)) {
throw new CommerceError({
message:
'A first name, last name, email and password are required to signup',
})
}
return fetch({
...defaultOpts,
...options,
body: { firstName, lastName, email, password },
})
}
export function extendHook(customFetcher: typeof fetcher) {
const useSignup = () => {
const { revalidate } = useCustomer()
const fn = useCommerceSignup<null, SignupInput>(defaultOpts, customFetcher)
return useCallback(
async function signup(input: SignupInput) {
const data = await fn(input)
await revalidate()
return data
},
[fn]
)
}
useSignup.extend = extendHook
return useSignup
}
export default extendHook(fetcher)

View File

@ -1,57 +0,0 @@
import { useCallback } from 'react'
import { HookFetcher } from '../../commerce/utils/types'
import { CommerceError } from '../../commerce/utils/errors'
import useWishlistAddItem from '../../commerce/wishlist/use-add-item'
import type { ItemBody, AddItemBody } from '../api/wishlist'
import useCustomer from '../use-customer'
import useWishlist, { Wishlist } from './use-wishlist'
const defaultOpts = {
url: '/api/bigcommerce/wishlist',
method: 'POST',
}
export type AddItemInput = ItemBody
export const fetcher: HookFetcher<Wishlist, AddItemBody> = (
options,
{ item },
fetch
) => {
// TODO: add validations before doing the fetch
return fetch({
...defaultOpts,
...options,
body: { item },
})
}
export function extendHook(customFetcher: typeof fetcher) {
const useAddItem = () => {
const { data: customer } = useCustomer()
const { mutate } = useWishlist()
const fn = useWishlistAddItem(defaultOpts, customFetcher)
return useCallback(
async function addItem(input: AddItemInput) {
if (!customer) {
// A signed customer is required in order to have a wishlist
throw new CommerceError({
message: 'Signed customer not found',
})
}
const data = await fn({ item: input })
await mutate(data, false)
return data
},
[fn, mutate, customer]
)
}
useAddItem.extend = extendHook
return useAddItem
}
export default extendHook(fetcher)

View File

@ -1,61 +0,0 @@
import { useCallback } from 'react'
import { HookFetcher } from '../../commerce/utils/types'
import { CommerceError } from '../../commerce/utils/errors'
import useWishlistRemoveItem from '../../commerce/wishlist/use-remove-item'
import type { RemoveItemBody } from '../api/wishlist'
import useCustomer from '../use-customer'
import useWishlist, { Wishlist } from './use-wishlist'
const defaultOpts = {
url: '/api/bigcommerce/wishlist',
method: 'DELETE',
}
export type RemoveItemInput = {
id: string | number
}
export const fetcher: HookFetcher<Wishlist | null, RemoveItemBody> = (
options,
{ itemId },
fetch
) => {
return fetch({
...defaultOpts,
...options,
body: { itemId },
})
}
export function extendHook(customFetcher: typeof fetcher) {
const useRemoveItem = () => {
const { data: customer } = useCustomer()
const { mutate } = useWishlist()
const fn = useWishlistRemoveItem<Wishlist | null, RemoveItemBody>(
defaultOpts,
customFetcher
)
return useCallback(
async function removeItem(input: RemoveItemInput) {
if (!customer) {
// A signed customer is required in order to have a wishlist
throw new CommerceError({
message: 'Signed customer not found',
})
}
const data = await fn({ itemId: String(input.id) })
await mutate(data, false)
return data
},
[fn, mutate, customer]
)
}
useRemoveItem.extend = extendHook
return useRemoveItem
}
export default extendHook(fetcher)

View File

@ -1,11 +0,0 @@
import useAddItem from './use-add-item'
import useRemoveItem from './use-remove-item'
// This hook is probably not going to be used, but it's here
// to show how a commerce should be structuring it
export default function useWishlistActions() {
const addItem = useAddItem()
const removeItem = useRemoveItem()
return { addItem, removeItem }
}

View File

@ -1,55 +0,0 @@
import { HookFetcher } from '../../commerce/utils/types'
import { SwrOptions } from '../../commerce/utils/use-data'
import useCommerceWishlist from '../../commerce/wishlist/use-wishlist'
import type { Wishlist } from '../api/wishlist'
import useCustomer from '../use-customer'
const defaultOpts = {
url: '/api/bigcommerce/wishlist',
method: 'GET',
}
export type { Wishlist }
export const fetcher: HookFetcher<Wishlist | null, { customerId?: number }> = (
options,
{ customerId },
fetch
) => {
return customerId ? fetch({ ...defaultOpts, ...options }) : null
}
export function extendHook(
customFetcher: typeof fetcher,
swrOptions?: SwrOptions<Wishlist | null, { customerId?: number }>
) {
const useWishlists = () => {
const { data: customer } = useCustomer()
const response = useCommerceWishlist(
defaultOpts,
[['customerId', customer?.entityId]],
customFetcher,
{
revalidateOnFocus: false,
...swrOptions,
}
)
// Uses a getter to only calculate the prop when required
// response.data is also a getter and it's better to not trigger it early
Object.defineProperty(response, 'isEmpty', {
get() {
return (response.data?.items?.length || 0) > 0
},
set: (x) => x,
})
return response
}
useWishlists.extend = extendHook
return useWishlists
}
export default extendHook(fetcher)

View File

@ -1,36 +0,0 @@
import type { RequestInit, Response } from '@vercel/fetch'
export interface CommerceAPIConfig {
locale?: string
commerceUrl: string
apiToken: string
cartCookie: string
cartCookieMaxAge: number
customerCookie: string
fetch<Data = any, Variables = any>(
query: string,
queryData?: CommerceAPIFetchOptions<Variables>,
fetchOptions?: RequestInit
): Promise<GraphQLFetcherResult<Data>>
}
export type GraphQLFetcher<
Data extends GraphQLFetcherResult = GraphQLFetcherResult,
Variables = any
> = (
query: string,
queryData?: CommerceAPIFetchOptions<Variables>,
fetchOptions?: RequestInit
) => Promise<Data>
export interface GraphQLFetcherResult<Data = any> {
data: Data
res: Response
}
export interface CommerceAPIFetchOptions<Variables> {
variables?: Variables
preview?: boolean
}
// TODO: define interfaces for all the available operations and API endpoints

View File

@ -1,5 +0,0 @@
import useAction from '../utils/use-action'
const useAddItem = useAction
export default useAddItem

View File

@ -1,17 +0,0 @@
import type { HookFetcher, HookFetcherOptions } from '../utils/types'
import useAddItem from './use-add-item'
import useRemoveItem from './use-remove-item'
import useUpdateItem from './use-update-item'
// This hook is probably not going to be used, but it's here
// to show how a commerce should be structuring it
export default function useCartActions<T, Input>(
options: HookFetcherOptions,
fetcher: HookFetcher<T, Input>
) {
const addItem = useAddItem<T, Input>(options, fetcher)
const updateItem = useUpdateItem<T, Input>(options, fetcher)
const removeItem = useRemoveItem<T, Input>(options, fetcher)
return { addItem, updateItem, removeItem }
}

View File

@ -1,31 +0,0 @@
import type { responseInterface } from 'swr'
import Cookies from 'js-cookie'
import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
import useData, { SwrOptions } from '../utils/use-data'
import { useCommerce } from '..'
export type CartResponse<Result> = responseInterface<Result, Error> & {
isEmpty: boolean
}
export type CartInput = {
cartId: string | undefined
}
export default function useCart<Result>(
options: HookFetcherOptions,
input: HookInput,
fetcherFn: HookFetcher<Result, CartInput>,
swrOptions?: SwrOptions<Result, CartInput>
) {
const { cartCookie } = useCommerce()
const fetcher: typeof fetcherFn = (options, input, fetch) => {
input.cartId = Cookies.get(cartCookie)
return fetcherFn(options, input, fetch)
}
const response = useData(options, input, fetcher, swrOptions)
return Object.assign(response, { isEmpty: true }) as CartResponse<Result>
}

View File

@ -1,5 +0,0 @@
import useAction from '../utils/use-action'
const useRemoveItem = useAction
export default useRemoveItem

View File

@ -1,5 +0,0 @@
import useAction from '../utils/use-action'
const useUpdateItem = useAction
export default useUpdateItem

View File

@ -1,51 +0,0 @@
import {
ReactNode,
MutableRefObject,
createContext,
useContext,
useMemo,
useRef,
} from 'react'
import { Fetcher } from './utils/types'
const Commerce = createContext<CommerceContextValue | {}>({})
export type CommerceProps = {
children?: ReactNode
config: CommerceConfig
}
export type CommerceConfig = { fetcher: Fetcher<any> } & Omit<
CommerceContextValue,
'fetcherRef'
>
export type CommerceContextValue = {
fetcherRef: MutableRefObject<Fetcher<any>>
locale: string
cartCookie: string
}
export function CommerceProvider({ children, config }: CommerceProps) {
if (!config) {
throw new Error('CommerceProvider requires a valid config object')
}
const fetcherRef = useRef(config.fetcher)
// Because the config is an object, if the parent re-renders this provider
// will re-render every consumer unless we memoize the config
const cfg = useMemo(
() => ({
fetcherRef,
locale: config.locale,
cartCookie: config.cartCookie,
}),
[config.locale, config.cartCookie]
)
return <Commerce.Provider value={cfg}>{children}</Commerce.Provider>
}
export function useCommerce<T extends CommerceContextValue>() {
return useContext(Commerce) as T
}

View File

@ -1,5 +0,0 @@
import useData from '../utils/use-data'
const useSearch = useData
export default useSearch

View File

@ -1,5 +0,0 @@
import useData from './utils/use-data'
const useCustomer = useData
export default useCustomer

View File

@ -1,5 +0,0 @@
import useAction from './utils/use-action'
const useLogin = useAction
export default useLogin

View File

@ -1,5 +0,0 @@
import useAction from './utils/use-action'
const useLogout = useAction
export default useLogout

View File

@ -1,64 +0,0 @@
import { useMemo } from 'react'
import { useCommerce } from '.'
export function formatPrice({
amount,
currencyCode,
locale,
}: {
amount: number
currencyCode: string
locale: string
}) {
const formatCurrency = new Intl.NumberFormat(locale, {
style: 'currency',
currency: currencyCode,
})
return formatCurrency.format(amount)
}
export function formatVariantPrice({
amount,
baseAmount,
currencyCode,
locale,
}: {
baseAmount: number
amount: number
currencyCode: string
locale: string
}) {
const hasDiscount = baseAmount > amount
const formatDiscount = new Intl.NumberFormat(locale, { style: 'percent' })
const discount = hasDiscount
? formatDiscount.format((baseAmount - amount) / baseAmount)
: null
const price = formatPrice({ amount, currencyCode, locale })
const basePrice = hasDiscount
? formatPrice({ amount: baseAmount, currencyCode, locale })
: null
return { price, basePrice, discount }
}
export default function usePrice(
data?: {
amount: number
baseAmount?: number
currencyCode: string
} | null
) {
const { amount, baseAmount, currencyCode } = data ?? {}
const { locale } = useCommerce()
const value = useMemo(() => {
if (typeof amount !== 'number' || !currencyCode) return ''
return baseAmount
? formatVariantPrice({ amount, baseAmount, currencyCode, locale })
: formatPrice({ amount, currencyCode, locale })
}, [amount, baseAmount, currencyCode])
return typeof value === 'string' ? { price: value } : value
}

View File

@ -1,5 +0,0 @@
import useAction from './utils/use-action'
const useSignup = useAction
export default useSignup

View File

@ -1,40 +0,0 @@
export type ErrorData = {
message: string
code?: string
}
export type ErrorProps = {
code?: string
} & (
| { message: string; errors?: never }
| { message?: never; errors: ErrorData[] }
)
export class CommerceError extends Error {
code?: string
errors: ErrorData[]
constructor({ message, code, errors }: ErrorProps) {
const error: ErrorData = message
? { message, ...(code ? { code } : {}) }
: errors![0]
super(error.message)
this.errors = message ? [error] : errors!
if (error.code) this.code = error.code
}
}
export class FetcherError extends CommerceError {
status: number
constructor(
options: {
status: number
} & ErrorProps
) {
super(options)
this.status = options.status
}
}

View File

@ -1,24 +0,0 @@
// Core fetcher added by CommerceProvider
export type Fetcher<T> = (options: FetcherOptions) => T | Promise<T>
export type FetcherOptions = {
url?: string
query?: string
method?: string
variables?: any
body?: any
}
export type HookFetcher<Result, Input = null> = (
options: HookFetcherOptions | null,
input: Input,
fetch: <T = Result>(options: FetcherOptions) => Promise<T>
) => Result | Promise<Result>
export type HookFetcherOptions = {
query?: string
url?: string
method?: string
}
export type HookInput = [string, string | number | undefined][]

View File

@ -1,15 +0,0 @@
import { useCallback } from 'react'
import type { HookFetcher, HookFetcherOptions } from './types'
import { useCommerce } from '..'
export default function useAction<T, Input = null>(
options: HookFetcherOptions,
fetcher: HookFetcher<T, Input>
) {
const { fetcherRef } = useCommerce()
return useCallback(
(input: Input) => fetcher(options, input, fetcherRef.current),
[fetcher]
)
}

View File

@ -1,60 +0,0 @@
import useSWR, { ConfigInterface, responseInterface } from 'swr'
import type { HookInput, HookFetcher, HookFetcherOptions } from './types'
import { CommerceError } from './errors'
import { useCommerce } from '..'
export type SwrOptions<Result, Input = null> = ConfigInterface<
Result,
CommerceError,
HookFetcher<Result, Input>
>
export type UseData = <Result = any, Input = null>(
options: HookFetcherOptions | (() => HookFetcherOptions | null),
input: HookInput,
fetcherFn: HookFetcher<Result, Input>,
swrOptions?: SwrOptions<Result, Input>
) => responseInterface<Result, CommerceError>
const useData: UseData = (options, input, fetcherFn, swrOptions) => {
const { fetcherRef } = useCommerce()
const fetcher = async (
url?: string,
query?: string,
method?: string,
...args: any[]
) => {
try {
return await fetcherFn(
{ url, query, method },
// Transform the input array into an object
args.reduce((obj, val, i) => {
obj[input[i][0]!] = val
return obj
}, {}),
fetcherRef.current
)
} catch (error) {
// SWR will not log errors, but any error that's not an instance
// of CommerceError is not welcomed by this hook
if (!(error instanceof CommerceError)) {
console.error(error)
}
throw error
}
}
const response = useSWR(
() => {
const opts = typeof options === 'function' ? options() : options
return opts
? [opts.url, opts.query, opts.method, ...input.map((e) => e[1])]
: null
},
fetcher,
swrOptions
)
return response
}
export default useData

View File

@ -1,5 +0,0 @@
import useAction from '../utils/use-action'
const useAddItem = useAction
export default useAddItem

View File

@ -1,5 +0,0 @@
import useAction from '../utils/use-action'
const useRemoveItem = useAction
export default useRemoveItem

View File

@ -1,17 +0,0 @@
import type { responseInterface } from 'swr'
import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
import useData, { SwrOptions } from '../utils/use-data'
export type WishlistResponse<Result> = responseInterface<Result, Error> & {
isEmpty: boolean
}
export default function useWishlist<Result, Input = null>(
options: HookFetcherOptions,
input: HookInput,
fetcherFn: HookFetcher<Result, Input>,
swrOptions?: SwrOptions<Result, Input>
) {
const response = useData(options, input, fetcherFn, swrOptions)
return Object.assign(response, { isEmpty: true }) as WishlistResponse<Result>
}