mirror of
https://github.com/vercel/commerce.git
synced 2025-06-18 21:21:21 +00:00
Add operations codegen, tidy up
This commit is contained in:
parent
8d48bc98ac
commit
3319968ade
@ -3,9 +3,6 @@ import s from './LoadingDots.module.css'
|
|||||||
const LoadingDots: React.FC = () => {
|
const LoadingDots: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<span className={s.root}>
|
<span className={s.root}>
|
||||||
<span />
|
|
||||||
<span />
|
|
||||||
<span />
|
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
2
framework/types.d.ts
vendored
2
framework/types.d.ts
vendored
@ -131,7 +131,7 @@ interface Cart extends Entity {
|
|||||||
id: string | undefined
|
id: string | undefined
|
||||||
currency: { code: string }
|
currency: { code: string }
|
||||||
taxIncluded?: boolean
|
taxIncluded?: boolean
|
||||||
items: Pick<Product, 'id' | 'name' | 'prices'> & CartItem[]
|
items: (Pick<Product, 'id' | 'name' | 'prices'> & CartItem)[]
|
||||||
subTotal: number | string
|
subTotal: number | string
|
||||||
total: number | string
|
total: number | string
|
||||||
customerId: Customer['id']
|
customerId: Customer['id']
|
||||||
|
@ -44,11 +44,7 @@ yarn add storefront-data-hooks
|
|||||||
After install, the first thing you do is: <b>set your environment variables</b> in `.env.local`
|
After install, the first thing you do is: <b>set your environment variables</b> in `.env.local`
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
BIGCOMMERCE_STOREFRONT_API_URL=<>
|
VENDURE_SHOP_API_URL=<>
|
||||||
BIGCOMMERCE_STOREFRONT_API_TOKEN=<>
|
|
||||||
BIGCOMMERCE_STORE_API_URL=<>
|
|
||||||
BIGCOMMERCE_STORE_API_TOKEN=<>
|
|
||||||
BIGCOMMERCE_STORE_API_CLIENT_ID=<>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## General Usage
|
## General Usage
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
import { parseCartItem } from '../../utils/parse-item'
|
|
||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
|
||||||
import type { CartHandlers } from '..'
|
|
||||||
|
|
||||||
const addItem: CartHandlers['addItem'] = async ({
|
|
||||||
res,
|
|
||||||
body: { cartId, item },
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
if (!item) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Missing item' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (!item.quantity) item.quantity = 1
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
line_items: [parseCartItem(item)],
|
|
||||||
...(!cartId && config.storeChannelId
|
|
||||||
? { channel_id: config.storeChannelId }
|
|
||||||
: {}),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
const { data } = cartId
|
|
||||||
? await config.storeApiFetch(`/v3/carts/${cartId}/items`, 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
|
|
@ -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
|
|
@ -1,33 +0,0 @@
|
|||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
|
||||||
import type { CartHandlers } from '..'
|
|
||||||
|
|
||||||
const removeItem: CartHandlers['removeItem'] = async ({
|
|
||||||
res,
|
|
||||||
body: { cartId, itemId },
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
if (!cartId || !itemId) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Invalid request' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await config.storeApiFetch<{ data: any } | null>(
|
|
||||||
`/v3/carts/${cartId}/items/${itemId}`,
|
|
||||||
{ 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
|
|
@ -1,35 +0,0 @@
|
|||||||
import { parseCartItem } from '../../utils/parse-item'
|
|
||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
|
||||||
import type { CartHandlers } from '..'
|
|
||||||
|
|
||||||
const updateItem: CartHandlers['updateItem'] = async ({
|
|
||||||
res,
|
|
||||||
body: { cartId, itemId, item },
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
if (!cartId || !itemId || !item) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Invalid request' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await config.storeApiFetch(
|
|
||||||
`/v3/carts/${cartId}/items/${itemId}`,
|
|
||||||
{
|
|
||||||
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
|
|
@ -1,116 +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'
|
|
||||||
|
|
||||||
type OptionSelections = {
|
|
||||||
option_id: Number
|
|
||||||
option_value: Number|String
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ItemBody = {
|
|
||||||
productId: number
|
|
||||||
variantId: number
|
|
||||||
quantity?: number
|
|
||||||
optionSelections?: OptionSelections
|
|
||||||
}
|
|
||||||
|
|
||||||
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, {})
|
|
@ -1,79 +0,0 @@
|
|||||||
import { Product } from 'framework/types'
|
|
||||||
import getAllProducts, { ProductEdge } from '../../../product/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]: Product
|
|
||||||
}>((prods, p) => {
|
|
||||||
prods[p.id] = p
|
|
||||||
return prods
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
const products: Product[] = 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
|
|
@ -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 getProducts from './handlers/get-products'
|
|
||||||
import { Product } from 'framework/types'
|
|
||||||
|
|
||||||
export type SearchProductsData = {
|
|
||||||
products: Product[]
|
|
||||||
found: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ProductsHandlers = {
|
|
||||||
getProducts: BigcommerceHandler<
|
|
||||||
SearchProductsData,
|
|
||||||
{ search?: 'string'; category?: string; brand?: string; sort?: string }
|
|
||||||
>
|
|
||||||
}
|
|
||||||
|
|
||||||
const METHODS = ['GET']
|
|
||||||
|
|
||||||
// TODO(lf): 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, {})
|
|
@ -1,77 +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) {
|
|
||||||
res.redirect('/cart')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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, {}, {})
|
|
@ -1,58 +0,0 @@
|
|||||||
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<any>
|
|
||||||
|
|
||||||
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
|
|
@ -1,49 +0,0 @@
|
|||||||
import { FetcherError } from '@commerce/utils/errors'
|
|
||||||
import login from '../../../auth/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
|
|
@ -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
|
|
@ -1,62 +0,0 @@
|
|||||||
import { BigcommerceApiError } from '../../utils/errors'
|
|
||||||
import login from '../../../auth/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
|
|
@ -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, {})
|
|
@ -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, {})
|
|
@ -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, {})
|
|
@ -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
@ -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 customer’s 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 customer’s 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 BigCommerce’s 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 post’s `<meta/>` element.
|
|
||||||
*/
|
|
||||||
meta_description?: string
|
|
||||||
/**
|
|
||||||
* Keywords for this blog post’s `<meta/>` element.
|
|
||||||
*/
|
|
||||||
meta_keywords?: string
|
|
||||||
/**
|
|
||||||
* Name of the blog post’s 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 customer’s 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 page’s 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`: customer’s phone number, as submitted on the form; `companyname`: customer’s submitted company name; `orderno`: customer’s submitted order number; `rma`: customer’s submitted RMA (Return Merchandise Authorization) number.
|
|
||||||
*/
|
|
||||||
contact_fields?: string
|
|
||||||
/**
|
|
||||||
* Where the page’s 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 page’s `<meta/>` element.
|
|
||||||
*/
|
|
||||||
meta_description?: string
|
|
||||||
/**
|
|
||||||
* HTML or variable that populates this page’s `<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 storefront’s navigation menu.
|
|
||||||
*/
|
|
||||||
is_visible?: boolean
|
|
||||||
/**
|
|
||||||
* If true, this page is the storefront’s home page.
|
|
||||||
*/
|
|
||||||
is_homepage?: boolean
|
|
||||||
/**
|
|
||||||
* Text specified for this page’s `<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 page’s `<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'
|
|
||||||
}
|
|
||||||
}
|
|
@ -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'] }
|
|
||||||
}
|
|
@ -8,6 +8,9 @@ export const cartFragment = /* GraphQL */ `
|
|||||||
total
|
total
|
||||||
totalWithTax
|
totalWithTax
|
||||||
currencyCode
|
currencyCode
|
||||||
|
customer {
|
||||||
|
id
|
||||||
|
}
|
||||||
lines {
|
lines {
|
||||||
id
|
id
|
||||||
quantity
|
quantity
|
||||||
@ -16,6 +19,7 @@ export const cartFragment = /* GraphQL */ `
|
|||||||
preview
|
preview
|
||||||
}
|
}
|
||||||
productVariant {
|
productVariant {
|
||||||
|
id
|
||||||
name
|
name
|
||||||
product {
|
product {
|
||||||
slug
|
slug
|
||||||
|
19
framework/vendure/api/fragments/search-result.ts
Normal file
19
framework/vendure/api/fragments/search-result.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export const searchResultFragment = /* GraphQL */ `
|
||||||
|
fragment SearchResult on SearchResult {
|
||||||
|
productId
|
||||||
|
productName
|
||||||
|
description
|
||||||
|
description
|
||||||
|
slug
|
||||||
|
sku
|
||||||
|
currencyCode
|
||||||
|
productAsset {
|
||||||
|
id
|
||||||
|
preview
|
||||||
|
}
|
||||||
|
priceWithTax {
|
||||||
|
... on SinglePrice { value }
|
||||||
|
... on PriceRange { min max }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
@ -1,7 +1,5 @@
|
|||||||
import type { RequestInit } from '@vercel/fetch'
|
|
||||||
import type { CommerceAPIConfig } from '@commerce/api'
|
import type { CommerceAPIConfig } from '@commerce/api'
|
||||||
import fetchGraphqlApi from './utils/fetch-graphql-api'
|
import fetchGraphqlApi from './utils/fetch-graphql-api'
|
||||||
import fetchStoreApi from './utils/fetch-store-api'
|
|
||||||
|
|
||||||
export interface VendureConfig extends CommerceAPIConfig {}
|
export interface VendureConfig extends CommerceAPIConfig {}
|
||||||
|
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
import { VendureConfig, getConfig } from '..'
|
|
||||||
|
|
||||||
export type BigcommerceApiHandler<
|
|
||||||
T = any,
|
|
||||||
H extends BigcommerceHandlers = {},
|
|
||||||
Options extends {} = {}
|
|
||||||
> = (
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<BigcommerceApiResponse<T>>,
|
|
||||||
config: VendureConfig,
|
|
||||||
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: VendureConfig
|
|
||||||
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?: VendureConfig
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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'
|
|
||||||
}
|
|
||||||
}
|
|
@ -27,7 +27,7 @@ const fetchGraphqlApi: GraphQLFetcher = async (
|
|||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
if (json.errors) {
|
if (json.errors) {
|
||||||
throw new FetcherError({
|
throw new FetcherError({
|
||||||
errors: json.errors ?? [{ message: 'Failed to fetch Bigcommerce API' }],
|
errors: json.errors ?? [{ message: 'Failed to fetch Vendure API' }],
|
||||||
status: res.status,
|
status: res.status,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
export default function filterEdges<T>(
|
|
||||||
edges: (T | null | undefined)[] | null | undefined
|
|
||||||
) {
|
|
||||||
return edges?.filter((edge): edge is T => !!edge) ?? []
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -1,14 +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,
|
|
||||||
option_selections: item.optionSelections
|
|
||||||
})
|
|
@ -1,21 +0,0 @@
|
|||||||
import type { ProductNode } from '../../product/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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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]>
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
import type { WishlistHandlers } from '..'
|
|
||||||
import getCustomerId from '../../../customer/get-customer-id'
|
|
||||||
import getCustomerWishlist from '../../../customer/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
|
|
@ -1,37 +0,0 @@
|
|||||||
import getCustomerId from '../../../customer/get-customer-id'
|
|
||||||
import getCustomerWishlist from '../../../customer/get-customer-wishlist'
|
|
||||||
import type { Wishlist, WishlistHandlers } from '..'
|
|
||||||
|
|
||||||
// Return wishlist info
|
|
||||||
const getWishlist: WishlistHandlers['getWishlist'] = async ({
|
|
||||||
res,
|
|
||||||
body: { customerToken, includeProducts },
|
|
||||||
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 },
|
|
||||||
includeProducts,
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
|
|
||||||
result = { data: wishlist }
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json({ data: result.data ?? null })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getWishlist
|
|
@ -1,39 +0,0 @@
|
|||||||
import getCustomerId from '../../../customer/get-customer-id'
|
|
||||||
import getCustomerWishlist, {
|
|
||||||
Wishlist,
|
|
||||||
} from '../../../customer/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
|
|
@ -1,103 +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 {
|
|
||||||
Wishlist,
|
|
||||||
WishlistItem,
|
|
||||||
} from '../../customer/get-customer-wishlist'
|
|
||||||
import getWishlist from './handlers/get-wishlist'
|
|
||||||
import addItem from './handlers/add-item'
|
|
||||||
import removeItem from './handlers/remove-item'
|
|
||||||
|
|
||||||
export type { Wishlist, WishlistItem }
|
|
||||||
|
|
||||||
export type ItemBody = {
|
|
||||||
productId: Product['id']
|
|
||||||
variantId: ProductVariant['id']
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AddItemBody = { item: ItemBody }
|
|
||||||
|
|
||||||
export type RemoveItemBody = { itemId: Product['id'] }
|
|
||||||
|
|
||||||
export type WishlistBody = {
|
|
||||||
customer_id: Customer['id']
|
|
||||||
is_public: number
|
|
||||||
name: string
|
|
||||||
items: any[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AddWishlistBody = { wishlist: WishlistBody }
|
|
||||||
|
|
||||||
export type WishlistHandlers = {
|
|
||||||
getWishlist: BigcommerceHandler<
|
|
||||||
Wishlist,
|
|
||||||
{ customerToken?: string; includeProducts?: boolean }
|
|
||||||
>
|
|
||||||
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,
|
|
||||||
includeProducts: req.query.products === '1',
|
|
||||||
}
|
|
||||||
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, {})
|
|
@ -6,8 +6,10 @@ import { VendureConfig, getConfig } from '../api'
|
|||||||
|
|
||||||
export const loginMutation = /* GraphQL */ `
|
export const loginMutation = /* GraphQL */ `
|
||||||
mutation login($email: String!, $password: String!) {
|
mutation login($email: String!, $password: String!) {
|
||||||
login(email: $email, password: $password) {
|
login(username: $email, password: $password) {
|
||||||
result
|
...on CurrentUser {
|
||||||
|
id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
@ -5,6 +5,7 @@ import useCartAddItem from '@commerce/cart/use-add-item'
|
|||||||
import useCart from './use-cart'
|
import useCart from './use-cart'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { cartFragment } from '../api/fragments/cart'
|
import { cartFragment } from '../api/fragments/cart'
|
||||||
|
import { AddItemToOrderMutation, AddItemToOrderMutationVariables } from '@framework/schema'
|
||||||
|
|
||||||
export const addItemToOrderMutation = /* GraphQL */ `
|
export const addItemToOrderMutation = /* GraphQL */ `
|
||||||
mutation addItemToOrder($variantId: ID!, $quantity: Int!) {
|
mutation addItemToOrder($variantId: ID!, $quantity: Int!) {
|
||||||
@ -17,7 +18,7 @@ export const addItemToOrderMutation = /* GraphQL */ `
|
|||||||
|
|
||||||
export type AddItemInput = { productId?: number; variantId: number; quantity?: number; };
|
export type AddItemInput = { productId?: number; variantId: number; quantity?: number; };
|
||||||
|
|
||||||
export const fetcher: HookFetcher<Cart, AddItemInput> = (
|
export const fetcher: HookFetcher<AddItemToOrderMutation, AddItemToOrderMutationVariables> = (
|
||||||
options,
|
options,
|
||||||
{ variantId, quantity },
|
{ variantId, quantity },
|
||||||
fetch
|
fetch
|
||||||
@ -48,7 +49,7 @@ export function extendHook(customFetcher: typeof fetcher) {
|
|||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async function addItem(input: AddItemInput) {
|
async function addItem(input: AddItemInput) {
|
||||||
const data = await fn({ quantity: input.quantity, variantId: input.variantId })
|
const data = await fn({ quantity: input.quantity || 1, variantId: input.variantId })
|
||||||
await mutate(data, false)
|
await mutate(data, false)
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
import fetchGraphqlApi from '@framework/api/utils/fetch-graphql-api'
|
|
||||||
import { HookFetcher } from '@commerce/utils/types'
|
import { HookFetcher } from '@commerce/utils/types'
|
||||||
import useData, { SwrOptions } from '@commerce/utils/use-data'
|
import useData, { SwrOptions } from '@commerce/utils/use-data'
|
||||||
import useCommerceCart, { CartInput } from '@commerce/cart/use-cart'
|
|
||||||
import useResponse from '@commerce/utils/use-response'
|
import useResponse from '@commerce/utils/use-response'
|
||||||
import useAction from '@commerce/utils/use-action'
|
|
||||||
import { useCallback } from 'react'
|
|
||||||
import { normalizeCart } from '../../bigcommerce/lib/normalize'
|
|
||||||
import { cartFragment } from '../api/fragments/cart'
|
import { cartFragment } from '../api/fragments/cart'
|
||||||
|
import { CartFragment } from '../schema'
|
||||||
|
import { normalizeCart } from '@framework/lib/normalize'
|
||||||
|
|
||||||
export const getCartQuery = /* GraphQL */ `
|
export const getCartQuery = /* GraphQL */ `
|
||||||
query activeOrder {
|
query activeOrder {
|
||||||
@ -17,7 +14,7 @@ export const getCartQuery = /* GraphQL */ `
|
|||||||
${cartFragment}
|
${cartFragment}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const fetcher: HookFetcher<any | null> = (
|
export const fetcher: HookFetcher<any, null> = (
|
||||||
options,
|
options,
|
||||||
input,
|
input,
|
||||||
fetch
|
fetch
|
||||||
@ -25,30 +22,23 @@ export const fetcher: HookFetcher<any | null> = (
|
|||||||
return fetch({ ...options, query: getCartQuery })
|
return fetch({ ...options, query: getCartQuery })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CartResult = {
|
||||||
|
activeOrder?: CartFragment;
|
||||||
|
addItemToOrder?: CartFragment;
|
||||||
|
adjustOrderLine?: CartFragment;
|
||||||
|
removeOrderLine?: CartFragment;
|
||||||
|
}
|
||||||
|
|
||||||
export function extendHook(
|
export function extendHook(
|
||||||
customFetcher: typeof fetcher,
|
customFetcher: typeof fetcher,
|
||||||
swrOptions?: SwrOptions<any | null>
|
swrOptions?: SwrOptions<any | null>
|
||||||
) {
|
) {
|
||||||
const useCart = () => {
|
const useCart = () => {
|
||||||
const response = useData({ query: getCartQuery }, [], customFetcher, swrOptions)
|
const response = useData<CartResult>({ query: getCartQuery }, [], customFetcher, swrOptions)
|
||||||
const res = useResponse(response, {
|
const res = useResponse(response, {
|
||||||
normalizer: (data => {
|
normalizer: (data => {
|
||||||
const order = data?.activeOrder || data?.addItemToOrder || data?.adjustOrderLine || data?.removeOrderLine;
|
const order = data?.activeOrder || data?.addItemToOrder || data?.adjustOrderLine || data?.removeOrderLine;
|
||||||
return (order ? {
|
return (order ? normalizeCart(order) : null)
|
||||||
id: order.id,
|
|
||||||
currency: { code: order.currencyCode },
|
|
||||||
subTotal: order.subTotalWithTax / 100,
|
|
||||||
total: order.totalWithTax / 100,
|
|
||||||
items: order.lines?.map(l => ({
|
|
||||||
id: l.id,
|
|
||||||
name: l.productVariant.name,
|
|
||||||
quantity: l.quantity,
|
|
||||||
url: l.productVariant.product.slug,
|
|
||||||
variantId: l.productVariant.id,
|
|
||||||
productId: l.productVariant.productId,
|
|
||||||
images: [{ url: l.featuredAsset?.preview }]
|
|
||||||
}))
|
|
||||||
} : null)
|
|
||||||
}),
|
}),
|
||||||
descriptors: {
|
descriptors: {
|
||||||
isEmpty: {
|
isEmpty: {
|
||||||
|
@ -1,43 +1,47 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { HookFetcher } from '@commerce/utils/types'
|
import { HookFetcher } from '@commerce/utils/types'
|
||||||
import useCartRemoveItem from '@commerce/cart/use-remove-item'
|
import useCartRemoveItem from '@commerce/cart/use-remove-item'
|
||||||
import useCart, { Cart } from './use-cart'
|
import useCart from './use-cart'
|
||||||
import { cartFragment } from '@framework/api/fragments/cart'
|
import { cartFragment } from '@framework/api/fragments/cart'
|
||||||
|
import { RemoveOrderLineMutation, RemoveOrderLineMutationVariables } from '@framework/schema'
|
||||||
|
|
||||||
export const removeOrderLineMutation = /* GraphQL */ `
|
export const removeOrderLineMutation = /* GraphQL */ `
|
||||||
mutation removeOrderLine($orderLineId: ID!) {
|
mutation removeOrderLine($orderLineId: ID!) {
|
||||||
removeOrderLine(orderLineId: $orderLineId) {
|
removeOrderLine(orderLineId: $orderLineId) {
|
||||||
|
__typename
|
||||||
...Cart
|
...Cart
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${cartFragment}
|
${cartFragment}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const fetcher: HookFetcher<Cart | null, any> = (
|
export const fetcher: HookFetcher<RemoveOrderLineMutation, RemoveOrderLineMutationVariables> = (
|
||||||
options,
|
options,
|
||||||
{ lineId },
|
{ orderLineId },
|
||||||
fetch
|
fetch
|
||||||
) => {
|
) => {
|
||||||
return fetch({
|
return fetch({
|
||||||
...options,
|
...options,
|
||||||
query: removeOrderLineMutation,
|
query: removeOrderLineMutation,
|
||||||
variables: { orderLineId: lineId },
|
variables: { orderLineId },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extendHook(customFetcher: typeof fetcher) {
|
export function extendHook(customFetcher: typeof fetcher) {
|
||||||
const useRemoveItem = (item?: any) => {
|
const useRemoveItem = (item?: any) => {
|
||||||
const { mutate } = useCart()
|
const { mutate } = useCart()
|
||||||
const fn = useCartRemoveItem<Cart | null, any>(
|
const fn = useCartRemoveItem<RemoveOrderLineMutation, RemoveOrderLineMutationVariables>(
|
||||||
{},
|
{},
|
||||||
customFetcher
|
customFetcher
|
||||||
)
|
)
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async function removeItem(input: any) {
|
async function removeItem(input: any) {
|
||||||
const data = await fn({ lineId: input.id })
|
const { removeOrderLine } = await fn({ orderLineId: input.id })
|
||||||
await mutate(data, false)
|
if (removeOrderLine.__typename === 'Order') {
|
||||||
return data
|
await mutate({ removeOrderLine }, false)
|
||||||
|
}
|
||||||
|
return { removeOrderLine }
|
||||||
},
|
},
|
||||||
[fn, mutate]
|
[fn, mutate]
|
||||||
)
|
)
|
||||||
|
@ -2,8 +2,9 @@ import { useCallback } from 'react'
|
|||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
import type { HookFetcher } from '@commerce/utils/types'
|
import type { HookFetcher } from '@commerce/utils/types'
|
||||||
import useCartUpdateItem from '@commerce/cart/use-update-item'
|
import useCartUpdateItem from '@commerce/cart/use-update-item'
|
||||||
import useCart, { Cart } from './use-cart'
|
import useCart from './use-cart'
|
||||||
import { cartFragment } from '@framework/api/fragments/cart'
|
import { cartFragment } from '@framework/api/fragments/cart'
|
||||||
|
import { AdjustOrderLineMutation, AdjustOrderLineMutationVariables } from '@framework/schema'
|
||||||
|
|
||||||
export const adjustOrderLineMutation = /* GraphQL */ `
|
export const adjustOrderLineMutation = /* GraphQL */ `
|
||||||
mutation adjustOrderLine($orderLineId: ID!, $quantity: Int!) {
|
mutation adjustOrderLine($orderLineId: ID!, $quantity: Int!) {
|
||||||
@ -13,34 +14,36 @@ export const adjustOrderLineMutation = /* GraphQL */ `
|
|||||||
}
|
}
|
||||||
${cartFragment}
|
${cartFragment}
|
||||||
`
|
`
|
||||||
export const fetcher: HookFetcher<Cart | null, any> = (
|
export const fetcher: HookFetcher<AdjustOrderLineMutation, AdjustOrderLineMutationVariables> = (
|
||||||
options,
|
options,
|
||||||
{ lineId, quantity },
|
{ orderLineId, quantity },
|
||||||
fetch
|
fetch
|
||||||
) => {
|
) => {
|
||||||
return fetch({
|
return fetch({
|
||||||
...options,
|
...options,
|
||||||
query: adjustOrderLineMutation,
|
query: adjustOrderLineMutation,
|
||||||
variables: { orderLineId: lineId, quantity },
|
variables: { orderLineId, quantity },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
|
function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
|
||||||
const useUpdateItem = (item?: any) => {
|
const useUpdateItem = (item?: any) => {
|
||||||
const { mutate } = useCart()
|
const { mutate } = useCart()
|
||||||
const fn = useCartUpdateItem<Cart | null, any>(
|
const fn = useCartUpdateItem<AdjustOrderLineMutation, AdjustOrderLineMutationVariables>(
|
||||||
{},
|
{},
|
||||||
customFetcher
|
customFetcher
|
||||||
)
|
)
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
debounce(async (input: any) => {
|
debounce(async (input: any) => {
|
||||||
const data = await fn({
|
const { adjustOrderLine } = await fn({
|
||||||
lineId: item.id,
|
orderLineId: item.id,
|
||||||
quantity: input.quantity,
|
quantity: input.quantity,
|
||||||
})
|
})
|
||||||
await mutate(data, false)
|
if (adjustOrderLine.__typename === 'Order') {
|
||||||
return data
|
await mutate({ adjustOrderLine }, false)
|
||||||
|
}
|
||||||
|
return { adjustOrderLine }
|
||||||
}, cfg?.wait ?? 500),
|
}, cfg?.wait ?? 500),
|
||||||
[fn, mutate]
|
[fn, mutate]
|
||||||
)
|
)
|
||||||
|
@ -2,11 +2,24 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"http://localhost:3001/shop-api": {}
|
"http://localhost:3001/shop-api": {}
|
||||||
},
|
},
|
||||||
|
"documents": [
|
||||||
|
{
|
||||||
|
"./framework/vendure/**/*.{ts,tsx}": {
|
||||||
|
"noRequire": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"generates": {
|
"generates": {
|
||||||
"./framework/vendure/schema.d.ts": {
|
"./framework/vendure/schema.d.ts": {
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"typescript"
|
"typescript",
|
||||||
]
|
"typescript-operations"
|
||||||
|
],
|
||||||
|
"config": {
|
||||||
|
"scalars": {
|
||||||
|
"ID": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"./framework/vendure/schema.graphql": {
|
"./framework/vendure/schema.graphql": {
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
import { getConfig, VendureConfig } from '../api'
|
||||||
import { VendureConfig, getConfig } from '../api'
|
|
||||||
import { definitions } from '../api/definitions/store-content'
|
|
||||||
|
|
||||||
export type Page = definitions['page_Full']
|
export type Page = any;
|
||||||
|
|
||||||
export type GetAllPagesResult<
|
export type GetAllPagesResult<
|
||||||
T extends { pages: any[] } = { pages: Page[] }
|
T extends { pages: any[] } = { pages: Page[] }
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
|
||||||
import { VendureConfig, getConfig } from '../api'
|
import { VendureConfig, getConfig } from '../api'
|
||||||
import { definitions } from '../api/definitions/store-content'
|
|
||||||
|
|
||||||
export type Page = definitions['page_Full']
|
export type Page = any;
|
||||||
|
|
||||||
export type GetPageResult<T extends { page?: any } = { page?: Page }> = T
|
export type GetPageResult<T extends { page?: any } = { page?: Page }> = T
|
||||||
|
|
||||||
@ -36,17 +34,6 @@ async function getPage({
|
|||||||
preview?: boolean
|
preview?: boolean
|
||||||
}): Promise<GetPageResult> {
|
}): Promise<GetPageResult> {
|
||||||
config = getConfig(config)
|
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 {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
|
||||||
import filterEdges from '../api/utils/filter-edges'
|
|
||||||
import { VendureConfig, getConfig } from '../api'
|
import { VendureConfig, getConfig } from '../api'
|
||||||
|
import { GetCollectionsQuery } from '@framework/schema'
|
||||||
|
import { arrayToTree } from '@framework/lib/array-to-tree';
|
||||||
|
|
||||||
export const getCollectionsQuery = /* GraphQL */ `
|
export const getCollectionsQuery = /* GraphQL */ `
|
||||||
query getCollections {
|
query getCollections {
|
||||||
@ -37,81 +37,23 @@ async function getSiteInfo({
|
|||||||
config = getConfig(config)
|
config = getConfig(config)
|
||||||
// RecursivePartial forces the method to check for every prop in the data, which is
|
// RecursivePartial forces the method to check for every prop in the data, which is
|
||||||
// required in case there's a custom `query`
|
// required in case there's a custom `query`
|
||||||
const { data } = await config.fetch<any>(
|
const { data } = await config.fetch<GetCollectionsQuery>(
|
||||||
query,
|
query,
|
||||||
{ variables }
|
{ variables }
|
||||||
)
|
)
|
||||||
const categories = arrayToTree(data.collections?.items.map(i => ({
|
const categories = arrayToTree(data.collections?.items.map(i => ({
|
||||||
...i,
|
...i,
|
||||||
entityId: i.id,
|
entityId: i.id,
|
||||||
name: i.name,
|
|
||||||
path: i.slug,
|
path: i.slug,
|
||||||
description: i.description,
|
|
||||||
productCount: i.productVariants.totalItems,
|
productCount: i.productVariants.totalItems,
|
||||||
}))).children;
|
}))).children;
|
||||||
const brands = data.site?.brands?.edges
|
const brands = [] as any[];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categories: (categories as RecursiveRequired<typeof categories>) ?? [],
|
categories: categories ?? [],
|
||||||
brands: [],
|
brands,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default getSiteInfo
|
export default getSiteInfo
|
||||||
|
|
||||||
export type HasParent = { id: string; parent?: { id: string } | null };
|
|
||||||
export type TreeNode<T extends HasParent> = T & { children: Array<TreeNode<T>>; expanded: boolean };
|
|
||||||
export type RootNode<T extends HasParent> = { id?: string; children: Array<TreeNode<T>> };
|
|
||||||
|
|
||||||
export function arrayToTree<T extends HasParent>(nodes: T[], currentState?: RootNode<T>): RootNode<T> {
|
|
||||||
const topLevelNodes: Array<TreeNode<T>> = [];
|
|
||||||
const mappedArr: { [id: string]: TreeNode<T> } = {};
|
|
||||||
const currentStateMap = treeToMap(currentState);
|
|
||||||
|
|
||||||
// First map the nodes of the array to an object -> create a hash table.
|
|
||||||
for (const node of nodes) {
|
|
||||||
mappedArr[node.id] = { ...(node as any), children: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const id of nodes.map(n => n.id)) {
|
|
||||||
if (mappedArr.hasOwnProperty(id)) {
|
|
||||||
const mappedElem = mappedArr[id];
|
|
||||||
mappedElem.expanded = currentStateMap.get(id)?.expanded ?? false;
|
|
||||||
const parent = mappedElem.parent;
|
|
||||||
if (!parent) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// If the element is not at the root level, add it to its parent array of children.
|
|
||||||
const parentIsRoot = !mappedArr[parent.id];
|
|
||||||
if (!parentIsRoot) {
|
|
||||||
if (mappedArr[parent.id]) {
|
|
||||||
mappedArr[parent.id].children.push(mappedElem);
|
|
||||||
} else {
|
|
||||||
mappedArr[parent.id] = { children: [mappedElem] } as any;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
topLevelNodes.push(mappedElem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// tslint:disable-next-line:no-non-null-assertion
|
|
||||||
const rootId = topLevelNodes.length ? topLevelNodes[0].parent!.id : undefined;
|
|
||||||
return { id: rootId, children: topLevelNodes };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts an existing tree (as generated by the arrayToTree function) into a flat
|
|
||||||
* Map. This is used to persist certain states (e.g. `expanded`) when re-building the
|
|
||||||
* tree.
|
|
||||||
*/
|
|
||||||
function treeToMap<T extends HasParent>(tree?: RootNode<T>): Map<string, TreeNode<T>> {
|
|
||||||
const nodeMap = new Map<string, TreeNode<T>>();
|
|
||||||
function visit(node: TreeNode<T>) {
|
|
||||||
nodeMap.set(node.id, node);
|
|
||||||
node.children.forEach(visit);
|
|
||||||
}
|
|
||||||
if (tree) {
|
|
||||||
visit(tree as TreeNode<T>);
|
|
||||||
}
|
|
||||||
return nodeMap;
|
|
||||||
}
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { GetCustomerIdQuery } from '../schema'
|
import { GetCustomerIdQuery } from '../schema'
|
||||||
import { VendureConfig, getConfig } from '../api'
|
import { VendureConfig, getConfig } from '../api'
|
||||||
|
|
||||||
export const getCustomerIdQuery = /* GraphQL */ `
|
export const getCustomerIdQuery = /* */ `
|
||||||
query getCustomerId {
|
query getCustomerId {
|
||||||
customer {
|
customer {
|
||||||
entityId
|
entityId
|
||||||
|
56
framework/vendure/lib/array-to-tree.ts
Normal file
56
framework/vendure/lib/array-to-tree.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
export type HasParent = { id: number; parent?: { id: number } | null };
|
||||||
|
export type TreeNode<T extends HasParent> = T & { children: Array<TreeNode<T>>; expanded: boolean };
|
||||||
|
export type RootNode<T extends HasParent> = { id?: number; children: Array<TreeNode<T>> };
|
||||||
|
|
||||||
|
export function arrayToTree<T extends HasParent>(nodes: T[], currentState?: RootNode<T>): RootNode<T> {
|
||||||
|
const topLevelNodes: Array<TreeNode<T>> = [];
|
||||||
|
const mappedArr: { [id: string]: TreeNode<T> } = {};
|
||||||
|
const currentStateMap = treeToMap(currentState);
|
||||||
|
|
||||||
|
// First map the nodes of the array to an object -> create a hash table.
|
||||||
|
for (const node of nodes) {
|
||||||
|
mappedArr[node.id] = { ...(node as any), children: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of nodes.map(n => n.id)) {
|
||||||
|
if (mappedArr.hasOwnProperty(id)) {
|
||||||
|
const mappedElem = mappedArr[id];
|
||||||
|
mappedElem.expanded = currentStateMap.get(id)?.expanded ?? false;
|
||||||
|
const parent = mappedElem.parent;
|
||||||
|
if (!parent) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// If the element is not at the root level, add it to its parent array of children.
|
||||||
|
const parentIsRoot = !mappedArr[parent.id];
|
||||||
|
if (!parentIsRoot) {
|
||||||
|
if (mappedArr[parent.id]) {
|
||||||
|
mappedArr[parent.id].children.push(mappedElem);
|
||||||
|
} else {
|
||||||
|
mappedArr[parent.id] = { children: [mappedElem] } as any;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
topLevelNodes.push(mappedElem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// tslint:disable-next-line:no-non-null-assertion
|
||||||
|
const rootId = topLevelNodes.length ? topLevelNodes[0].parent!.id : undefined;
|
||||||
|
return { id: rootId, children: topLevelNodes };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an existing tree (as generated by the arrayToTree function) into a flat
|
||||||
|
* Map. This is used to persist certain states (e.g. `expanded`) when re-building the
|
||||||
|
* tree.
|
||||||
|
*/
|
||||||
|
function treeToMap<T extends HasParent>(tree?: RootNode<T>): Map<number, TreeNode<T>> {
|
||||||
|
const nodeMap = new Map<number, TreeNode<T>>();
|
||||||
|
function visit(node: TreeNode<T>) {
|
||||||
|
nodeMap.set(node.id, node);
|
||||||
|
node.children.forEach(visit);
|
||||||
|
}
|
||||||
|
if (tree) {
|
||||||
|
visit(tree as TreeNode<T>);
|
||||||
|
}
|
||||||
|
return nodeMap;
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import update from '@framework/lib/immutability'
|
import update from '@framework/lib/immutability'
|
||||||
|
import { CartFragment, SearchResultFragment } from '@framework/schema'
|
||||||
|
|
||||||
function normalizeProductOption(productOption: any) {
|
function normalizeProductOption(productOption: any) {
|
||||||
const {
|
const {
|
||||||
@ -60,59 +61,47 @@ export function normalizeProduct(productNode: any): Product {
|
|||||||
price: {
|
price: {
|
||||||
$set: {
|
$set: {
|
||||||
value: prices?.price.value,
|
value: prices?.price.value,
|
||||||
currencyCode: prices?.price.currencyCode,
|
currencyCode: prices?.price.currencyCode
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
$unset: ['entityId']
|
||||||
$unset: ['entityId'],
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeCart(data: any): Cart {
|
export function normalizeSearchResult(item: SearchResultFragment): Product {
|
||||||
return update(data, {
|
return {
|
||||||
$auto: {
|
id: item.productId,
|
||||||
items: { $set: data?.line_items?.physical_items?.map(itemsToProducts) },
|
name: item.productName,
|
||||||
subTotal: { $set: data?.base_amount },
|
description: item.description,
|
||||||
total: { $set: data?.cart_amount },
|
slug: item.slug,
|
||||||
|
path: item.slug,
|
||||||
|
images: [{ url: item.productAsset?.preview || '' }],
|
||||||
|
variants: [],
|
||||||
|
price: {
|
||||||
|
value: ((item.priceWithTax as any).min / 100),
|
||||||
|
currencyCode: item.currencyCode
|
||||||
},
|
},
|
||||||
$unset: ['created_time', 'coupons', 'line_items', 'email'],
|
options: [],
|
||||||
})
|
sku: item.sku
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function itemsToProducts(item: any): CartItem {
|
export function normalizeCart(order: CartFragment): Cart {
|
||||||
const {
|
return {
|
||||||
id,
|
id: order.id.toString(),
|
||||||
name,
|
currency: { code: order.currencyCode },
|
||||||
quantity,
|
subTotal: order.subTotalWithTax / 100,
|
||||||
product_id,
|
total: order.totalWithTax / 100,
|
||||||
variant_id,
|
customerId: order.customer?.id as number,
|
||||||
image_url,
|
items: order.lines?.map(l => ({
|
||||||
list_price,
|
id: l.id,
|
||||||
sale_price,
|
name: l.productVariant.name,
|
||||||
extended_list_price,
|
quantity: l.quantity,
|
||||||
extended_sale_price,
|
url: l.productVariant.product.slug,
|
||||||
...rest
|
variantId: l.productVariant.id,
|
||||||
} = item
|
productId: l.productVariant.productId,
|
||||||
|
images: [{ url: l.featuredAsset?.preview || '' }],
|
||||||
return update(item, {
|
prices: []
|
||||||
$auto: {
|
}))
|
||||||
prices: {
|
}
|
||||||
$auto: {
|
|
||||||
listPrice: { $set: list_price },
|
|
||||||
salePrice: { $set: sale_price },
|
|
||||||
extendedListPrice: { $set: extended_list_price },
|
|
||||||
extendedSalePrice: { $set: extended_sale_price },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
images: {
|
|
||||||
$set: [
|
|
||||||
{
|
|
||||||
alt: name,
|
|
||||||
url: image_url,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
productId: { $set: product_id },
|
|
||||||
variantId: { $set: variant_id },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,6 @@ import type {
|
|||||||
GetAllProductPathsQuery,
|
GetAllProductPathsQuery,
|
||||||
GetAllProductPathsQueryVariables,
|
GetAllProductPathsQueryVariables,
|
||||||
} from '../schema'
|
} from '../schema'
|
||||||
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
|
||||||
import filterEdges from '../api/utils/filter-edges'
|
|
||||||
import { VendureConfig, getConfig } from '../api'
|
import { VendureConfig, getConfig } from '../api'
|
||||||
|
|
||||||
export const getAllProductPathsQuery = /* GraphQL */ `
|
export const getAllProductPathsQuery = /* GraphQL */ `
|
||||||
@ -16,17 +14,7 @@ export const getAllProductPathsQuery = /* GraphQL */ `
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export type ProductPath = NonNullable<
|
export type GetAllProductPathsResult = { products: Array<{ node: { path: string; } }> };
|
||||||
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?: {
|
async function getAllProductPaths(opts?: {
|
||||||
variables?: GetAllProductPathsQueryVariables
|
variables?: GetAllProductPathsQueryVariables
|
||||||
@ -40,7 +28,7 @@ async function getAllProductPaths<
|
|||||||
query: string
|
query: string
|
||||||
variables?: V
|
variables?: V
|
||||||
config?: VendureConfig
|
config?: VendureConfig
|
||||||
}): Promise<GetAllProductPathsResult<T>>
|
}): Promise<GetAllProductPathsResult>
|
||||||
|
|
||||||
async function getAllProductPaths({
|
async function getAllProductPaths({
|
||||||
query = getAllProductPathsQuery,
|
query = getAllProductPathsQuery,
|
||||||
@ -54,9 +42,7 @@ async function getAllProductPaths({
|
|||||||
config = getConfig(config)
|
config = getConfig(config)
|
||||||
// RecursivePartial forces the method to check for every prop in the data, which is
|
// RecursivePartial forces the method to check for every prop in the data, which is
|
||||||
// required in case there's a custom `query`
|
// required in case there's a custom `query`
|
||||||
const { data } = await config.fetch<
|
const { data } = await config.fetch<GetAllProductPathsQuery>(query, { variables })
|
||||||
RecursivePartial<GetAllProductPathsQuery>
|
|
||||||
>(query, { variables })
|
|
||||||
const products = data.products.items
|
const products = data.products.items
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import type { RecursivePartial, RecursiveRequired } from '../api/utils/types'
|
import { getConfig, VendureConfig } from '../api'
|
||||||
import filterEdges from '../api/utils/filter-edges'
|
import { searchResultFragment } from '@framework/api/fragments/search-result'
|
||||||
import setProductLocaleMeta from '../api/utils/set-product-locale-meta'
|
import { GetAllProductsQuery } from '@framework/schema'
|
||||||
import { productConnectionFragment } from '../api/fragments/product'
|
import { normalizeSearchResult } from '@framework/lib/normalize'
|
||||||
import { VendureConfig, getConfig } from '../api'
|
|
||||||
import { normalizeProduct } from '../lib/normalize'
|
|
||||||
|
|
||||||
export const getAllProductsQuery = /* GraphQL */ `
|
export const getAllProductsQuery = /* GraphQL */ `
|
||||||
query getAllProducts(
|
query getAllProducts(
|
||||||
@ -11,24 +9,11 @@ export const getAllProductsQuery = /* GraphQL */ `
|
|||||||
) {
|
) {
|
||||||
search(input: $input) {
|
search(input: $input) {
|
||||||
items {
|
items {
|
||||||
productId
|
...SearchResult
|
||||||
productName
|
|
||||||
description
|
|
||||||
description
|
|
||||||
slug
|
|
||||||
sku
|
|
||||||
currencyCode
|
|
||||||
productAsset {
|
|
||||||
id
|
|
||||||
preview
|
|
||||||
}
|
|
||||||
priceWithTax {
|
|
||||||
... on SinglePrice { value }
|
|
||||||
... on PriceRange { min max }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
${searchResultFragment}
|
||||||
`
|
`
|
||||||
|
|
||||||
export type ProductVariables = { first?: number; }
|
export type ProductVariables = { first?: number; }
|
||||||
@ -53,31 +38,17 @@ async function getAllProducts({
|
|||||||
const variables = {
|
const variables = {
|
||||||
input: {
|
input: {
|
||||||
take: vars.first,
|
take: vars.first,
|
||||||
groupByProduct: true,
|
groupByProduct: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { data } = await config.fetch(
|
const { data } = await config.fetch<GetAllProductsQuery>(
|
||||||
query,
|
query,
|
||||||
{ variables }
|
{ variables }
|
||||||
)
|
)
|
||||||
|
|
||||||
return { products: data.search.items.map((item: any) => {
|
|
||||||
return {
|
return {
|
||||||
id: item.productId,
|
products: data.search.items.map(item => normalizeSearchResult(item))
|
||||||
name: item.productName,
|
|
||||||
description: item.description,
|
|
||||||
slug: item.slug,
|
|
||||||
path: item.slug,
|
|
||||||
images: [{ url: item.productAsset?.preview }],
|
|
||||||
variants: [],
|
|
||||||
price: {
|
|
||||||
value: (item.priceWithTax.min / 100),
|
|
||||||
currencyCode: item.currencyCode,
|
|
||||||
},
|
|
||||||
options: [],
|
|
||||||
sku: item.sku,
|
|
||||||
}
|
}
|
||||||
}) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default getAllProducts
|
export default getAllProducts
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
import setProductLocaleMeta from '../api/utils/set-product-locale-meta'
|
import { getConfig, VendureConfig } from '../api'
|
||||||
import { productInfoFragment } from '../api/fragments/product'
|
|
||||||
import { VendureConfig, getConfig } from '../api'
|
|
||||||
import { normalizeProduct } from '@framework/lib/normalize'
|
|
||||||
|
|
||||||
export const getProductQuery = /* GraphQL */ `
|
export const getProductQuery = /* GraphQL */ `
|
||||||
query getProduct($slug: String!) {
|
query getProduct($slug: String!) {
|
||||||
|
@ -1,33 +1,21 @@
|
|||||||
import type { HookFetcher } from '@commerce/utils/types'
|
import type { HookFetcher } from '@commerce/utils/types'
|
||||||
import type { SwrOptions } from '@commerce/utils/use-data'
|
import type { SwrOptions } from '@commerce/utils/use-data'
|
||||||
import useCommerceSearch from '@commerce/products/use-search'
|
import useCommerceSearch from '@commerce/products/use-search'
|
||||||
import type { SearchProductsData } from '../api/catalog/products'
|
|
||||||
import useResponse from '@commerce/utils/use-response'
|
import useResponse from '@commerce/utils/use-response'
|
||||||
|
import { searchResultFragment } from '@framework/api/fragments/search-result'
|
||||||
|
import { SearchQuery } from '@framework/schema'
|
||||||
|
import { normalizeSearchResult } from '@framework/lib/normalize'
|
||||||
|
|
||||||
export const searchQuery = /* GraphQL */ `
|
export const searchQuery = /* GraphQL */ `
|
||||||
query search($input: SearchInput!) {
|
query search($input: SearchInput!) {
|
||||||
search(input: $input) {
|
search(input: $input) {
|
||||||
items {
|
items {
|
||||||
productId
|
...SearchResult
|
||||||
currencyCode
|
|
||||||
productName
|
|
||||||
description
|
|
||||||
priceWithTax {
|
|
||||||
...on SinglePrice {
|
|
||||||
value
|
|
||||||
}
|
|
||||||
...on PriceRange {
|
|
||||||
min max
|
|
||||||
}
|
|
||||||
}
|
|
||||||
productAsset {
|
|
||||||
preview
|
|
||||||
}
|
|
||||||
slug
|
|
||||||
}
|
}
|
||||||
totalItems
|
totalItems
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
${searchResultFragment}
|
||||||
`
|
`
|
||||||
|
|
||||||
export type SearchProductsInput = {
|
export type SearchProductsInput = {
|
||||||
@ -37,7 +25,7 @@ export type SearchProductsInput = {
|
|||||||
sort?: string
|
sort?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetcher: HookFetcher<SearchProductsData, SearchProductsInput> = (
|
export const fetcher: HookFetcher<SearchQuery, SearchProductsInput> = (
|
||||||
options,
|
options,
|
||||||
{ search, categoryId, brandId, sort },
|
{ search, categoryId, brandId, sort },
|
||||||
fetch
|
fetch
|
||||||
@ -59,7 +47,7 @@ export function extendHook(
|
|||||||
swrOptions?: SwrOptions<any, SearchProductsInput>
|
swrOptions?: SwrOptions<any, SearchProductsInput>
|
||||||
) {
|
) {
|
||||||
const useSearch = (input: SearchProductsInput = {}) => {
|
const useSearch = (input: SearchProductsInput = {}) => {
|
||||||
const response = useCommerceSearch(
|
const response = useCommerceSearch<SearchQuery, SearchProductsInput>(
|
||||||
{},
|
{},
|
||||||
[
|
[
|
||||||
['search', input.search],
|
['search', input.search],
|
||||||
@ -74,22 +62,8 @@ export function extendHook(
|
|||||||
return useResponse(response, {
|
return useResponse(response, {
|
||||||
normalizer: data => {
|
normalizer: data => {
|
||||||
return {
|
return {
|
||||||
found: data?.search.totalItems > 0,
|
found: data?.search.totalItems && data?.search.totalItems > 0,
|
||||||
products: data?.search.items.map((item: any) => ({
|
products: data?.search.items.map(item => normalizeSearchResult(item)) ?? []
|
||||||
id: item.productId,
|
|
||||||
name: item.productName,
|
|
||||||
description: item.description,
|
|
||||||
slug: item.slug,
|
|
||||||
path: item.slug,
|
|
||||||
images: [{ url: item.productAsset?.preview }],
|
|
||||||
variants: [],
|
|
||||||
price: {
|
|
||||||
value: (item.priceWithTax.min / 100),
|
|
||||||
currencyCode: item.currencyCode
|
|
||||||
},
|
|
||||||
options: [],
|
|
||||||
sku: item.sku
|
|
||||||
})) ?? [],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
192
framework/vendure/schema.d.ts
vendored
192
framework/vendure/schema.d.ts
vendored
@ -8,7 +8,7 @@ export type MakeMaybe<T, K extends keyof T> = Omit<T, K> &
|
|||||||
{ [SubKey in K]: Maybe<T[SubKey]> }
|
{ [SubKey in K]: Maybe<T[SubKey]> }
|
||||||
/** All built-in and custom scalars, mapped to their actual values */
|
/** All built-in and custom scalars, mapped to their actual values */
|
||||||
export type Scalars = {
|
export type Scalars = {
|
||||||
ID: string
|
ID: number
|
||||||
String: string
|
String: string
|
||||||
Boolean: boolean
|
Boolean: boolean
|
||||||
Int: number
|
Int: number
|
||||||
@ -2795,3 +2795,193 @@ export type NativeAuthInput = {
|
|||||||
username: Scalars['String']
|
username: Scalars['String']
|
||||||
password: Scalars['String']
|
password: Scalars['String']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CartFragment = { __typename?: 'Order' } & Pick<
|
||||||
|
Order,
|
||||||
|
| 'id'
|
||||||
|
| 'code'
|
||||||
|
| 'totalQuantity'
|
||||||
|
| 'subTotal'
|
||||||
|
| 'subTotalWithTax'
|
||||||
|
| 'total'
|
||||||
|
| 'totalWithTax'
|
||||||
|
| 'currencyCode'
|
||||||
|
> & {
|
||||||
|
customer?: Maybe<{ __typename?: 'Customer' } & Pick<Customer, 'id'>>
|
||||||
|
lines: Array<
|
||||||
|
{ __typename?: 'OrderLine' } & Pick<OrderLine, 'id' | 'quantity'> & {
|
||||||
|
featuredAsset?: Maybe<
|
||||||
|
{ __typename?: 'Asset' } & Pick<Asset, 'id' | 'preview'>
|
||||||
|
>
|
||||||
|
productVariant: { __typename?: 'ProductVariant' } & Pick<
|
||||||
|
ProductVariant,
|
||||||
|
'id' | 'name' | 'productId'
|
||||||
|
> & { product: { __typename?: 'Product' } & Pick<Product, 'slug'> }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SearchResultFragment = { __typename?: 'SearchResult' } & Pick<
|
||||||
|
SearchResult,
|
||||||
|
'productId' | 'productName' | 'description' | 'slug' | 'sku' | 'currencyCode'
|
||||||
|
> & {
|
||||||
|
productAsset?: Maybe<
|
||||||
|
{ __typename?: 'SearchResultAsset' } & Pick<
|
||||||
|
SearchResultAsset,
|
||||||
|
'id' | 'preview'
|
||||||
|
>
|
||||||
|
>
|
||||||
|
priceWithTax:
|
||||||
|
| ({ __typename?: 'PriceRange' } & Pick<PriceRange, 'min' | 'max'>)
|
||||||
|
| ({ __typename?: 'SinglePrice' } & Pick<SinglePrice, 'value'>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LoginMutationVariables = Exact<{
|
||||||
|
email: Scalars['String']
|
||||||
|
password: Scalars['String']
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type LoginMutation = { __typename?: 'Mutation' } & {
|
||||||
|
login:
|
||||||
|
| ({ __typename?: 'CurrentUser' } & Pick<CurrentUser, 'id'>)
|
||||||
|
| { __typename?: 'InvalidCredentialsError' }
|
||||||
|
| { __typename?: 'NotVerifiedError' }
|
||||||
|
| { __typename?: 'NativeAuthStrategyError' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AddItemToOrderMutationVariables = Exact<{
|
||||||
|
variantId: Scalars['ID']
|
||||||
|
quantity: Scalars['Int']
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type AddItemToOrderMutation = { __typename?: 'Mutation' } & {
|
||||||
|
addItemToOrder:
|
||||||
|
| ({ __typename?: 'Order' } & CartFragment)
|
||||||
|
| { __typename?: 'OrderModificationError' }
|
||||||
|
| { __typename?: 'OrderLimitError' }
|
||||||
|
| { __typename?: 'NegativeQuantityError' }
|
||||||
|
| { __typename?: 'InsufficientStockError' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ActiveOrderQueryVariables = Exact<{ [key: string]: never }>
|
||||||
|
|
||||||
|
export type ActiveOrderQuery = { __typename?: 'Query' } & {
|
||||||
|
activeOrder?: Maybe<{ __typename?: 'Order' } & CartFragment>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RemoveOrderLineMutationVariables = Exact<{
|
||||||
|
orderLineId: Scalars['ID']
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type RemoveOrderLineMutation = { __typename?: 'Mutation' } & {
|
||||||
|
removeOrderLine:
|
||||||
|
| ({ __typename: 'Order' } & CartFragment)
|
||||||
|
| { __typename: 'OrderModificationError' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AdjustOrderLineMutationVariables = Exact<{
|
||||||
|
orderLineId: Scalars['ID']
|
||||||
|
quantity: Scalars['Int']
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type AdjustOrderLineMutation = { __typename?: 'Mutation' } & {
|
||||||
|
adjustOrderLine:
|
||||||
|
| ({ __typename?: 'Order' } & CartFragment)
|
||||||
|
| { __typename?: 'OrderModificationError' }
|
||||||
|
| { __typename?: 'OrderLimitError' }
|
||||||
|
| { __typename?: 'NegativeQuantityError' }
|
||||||
|
| { __typename?: 'InsufficientStockError' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetCollectionsQueryVariables = Exact<{ [key: string]: never }>
|
||||||
|
|
||||||
|
export type GetCollectionsQuery = { __typename?: 'Query' } & {
|
||||||
|
collections: { __typename?: 'CollectionList' } & {
|
||||||
|
items: Array<
|
||||||
|
{ __typename?: 'Collection' } & Pick<
|
||||||
|
Collection,
|
||||||
|
'id' | 'name' | 'description' | 'slug'
|
||||||
|
> & {
|
||||||
|
productVariants: { __typename?: 'ProductVariantList' } & Pick<
|
||||||
|
ProductVariantList,
|
||||||
|
'totalItems'
|
||||||
|
>
|
||||||
|
parent?: Maybe<{ __typename?: 'Collection' } & Pick<Collection, 'id'>>
|
||||||
|
children?: Maybe<
|
||||||
|
Array<{ __typename?: 'Collection' } & Pick<Collection, 'id'>>
|
||||||
|
>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetAllProductPathsQueryVariables = Exact<{
|
||||||
|
first?: Maybe<Scalars['Int']>
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type GetAllProductPathsQuery = { __typename?: 'Query' } & {
|
||||||
|
products: { __typename?: 'ProductList' } & {
|
||||||
|
items: Array<{ __typename?: 'Product' } & Pick<Product, 'slug'>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetAllProductsQueryVariables = Exact<{
|
||||||
|
input: SearchInput
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type GetAllProductsQuery = { __typename?: 'Query' } & {
|
||||||
|
search: { __typename?: 'SearchResponse' } & {
|
||||||
|
items: Array<{ __typename?: 'SearchResult' } & SearchResultFragment>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetProductQueryVariables = Exact<{
|
||||||
|
slug: Scalars['String']
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type GetProductQuery = { __typename?: 'Query' } & {
|
||||||
|
product?: Maybe<
|
||||||
|
{ __typename?: 'Product' } & Pick<
|
||||||
|
Product,
|
||||||
|
'id' | 'name' | 'slug' | 'description'
|
||||||
|
> & {
|
||||||
|
assets: Array<
|
||||||
|
{ __typename?: 'Asset' } & Pick<Asset, 'id' | 'preview' | 'name'>
|
||||||
|
>
|
||||||
|
variants: Array<
|
||||||
|
{ __typename?: 'ProductVariant' } & Pick<
|
||||||
|
ProductVariant,
|
||||||
|
'id' | 'priceWithTax' | 'currencyCode'
|
||||||
|
> & {
|
||||||
|
options: Array<
|
||||||
|
{ __typename?: 'ProductOption' } & Pick<
|
||||||
|
ProductOption,
|
||||||
|
'id' | 'name' | 'code' | 'groupId'
|
||||||
|
>
|
||||||
|
>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
optionGroups: Array<
|
||||||
|
{ __typename?: 'ProductOptionGroup' } & Pick<
|
||||||
|
ProductOptionGroup,
|
||||||
|
'code' | 'name'
|
||||||
|
> & {
|
||||||
|
options: Array<
|
||||||
|
{ __typename?: 'ProductOption' } & Pick<ProductOption, 'name'>
|
||||||
|
>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SearchQueryVariables = Exact<{
|
||||||
|
input: SearchInput
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type SearchQuery = { __typename?: 'Query' } & {
|
||||||
|
search: { __typename?: 'SearchResponse' } & Pick<
|
||||||
|
SearchResponse,
|
||||||
|
'totalItems'
|
||||||
|
> & { items: Array<{ __typename?: 'SearchResult' } & SearchResultFragment> }
|
||||||
|
}
|
||||||
|
@ -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 = `http://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()
|
|
Loading…
x
Reference in New Issue
Block a user