forked from crowetic/commerce
Dynamic API routes (#836)
* Add dynamic API endpoints * Add missing dependency * Update api handlers * Updates * Fix build errors * Update package.json * Add checkout endpoint parser & update errors * Update tsconfig.json * Update cart.ts * Update parser * Update errors.ts * Update errors.ts * Move to Edge runtime * Revert to local * Fix switchable runtimes * Make nodejs default runtime * Update pnpm-lock.yaml * Update handlers * Fix build errors * Change headers
This commit is contained in:
parent
a5b367a747
commit
c75b0fc001
@ -47,14 +47,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@cfworker/uuid": "^1.12.4",
|
||||||
|
"@tsndr/cloudflare-worker-jwt": "^2.1.0",
|
||||||
"@vercel/commerce": "workspace:*",
|
"@vercel/commerce": "workspace:*",
|
||||||
"@vercel/fetch": "^6.2.0",
|
|
||||||
"cookie": "^0.4.1",
|
"cookie": "^0.4.1",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
|
"js-cookie": "^3.0.1",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"uuidv4": "^6.2.12",
|
"uuidv4": "^6.2.13"
|
||||||
"node-fetch": "^2.6.7"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"next": "^12",
|
"next": "^12",
|
||||||
@ -69,8 +70,8 @@
|
|||||||
"@types/jsonwebtoken": "^8.5.7",
|
"@types/jsonwebtoken": "^8.5.7",
|
||||||
"@types/lodash.debounce": "^4.0.6",
|
"@types/lodash.debounce": "^4.0.6",
|
||||||
"@types/node": "^17.0.8",
|
"@types/node": "^17.0.8",
|
||||||
"@types/react": "^18.0.14",
|
|
||||||
"@types/node-fetch": "^2.6.2",
|
"@types/node-fetch": "^2.6.2",
|
||||||
|
"@types/react": "^18.0.14",
|
||||||
"lint-staged": "^12.1.7",
|
"lint-staged": "^12.1.7",
|
||||||
"next": "^12.0.8",
|
"next": "^12.0.8",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
|
@ -1,22 +1,14 @@
|
|||||||
// @ts-nocheck
|
import type { CartEndpoint } from '.'
|
||||||
|
import type { BigcommerceCart } from '../../../types'
|
||||||
|
|
||||||
import { normalizeCart } from '../../../lib/normalize'
|
import { normalizeCart } from '../../../lib/normalize'
|
||||||
import { parseCartItem } from '../../utils/parse-item'
|
import { parseCartItem } from '../../utils/parse-item'
|
||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
import getCartCookie from '../../utils/get-cart-cookie'
|
||||||
import type { CartEndpoint } from '.'
|
|
||||||
|
|
||||||
const addItem: CartEndpoint['handlers']['addItem'] = async ({
|
const addItem: CartEndpoint['handlers']['addItem'] = async ({
|
||||||
res,
|
|
||||||
body: { cartId, item },
|
body: { cartId, item },
|
||||||
config,
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
if (!item) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Missing item' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (!item.quantity) item.quantity = 1
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@ -26,22 +18,27 @@ const addItem: CartEndpoint['handlers']['addItem'] = async ({
|
|||||||
: {}),
|
: {}),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data } = cartId
|
const { data } = cartId
|
||||||
? await config.storeApiFetch(
|
? await config.storeApiFetch<{ data: BigcommerceCart }>(
|
||||||
`/v3/carts/${cartId}/items?include=line_items.physical_items.options,line_items.digital_items.options`,
|
`/v3/carts/${cartId}/items?include=line_items.physical_items.options,line_items.digital_items.options`,
|
||||||
options
|
options
|
||||||
)
|
)
|
||||||
: await config.storeApiFetch(
|
: await config.storeApiFetch<{ data: BigcommerceCart }>(
|
||||||
'/v3/carts?include=line_items.physical_items.options,line_items.digital_items.options',
|
'/v3/carts?include=line_items.physical_items.options,line_items.digital_items.options',
|
||||||
options
|
options
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create or update the cart cookie
|
return {
|
||||||
res.setHeader(
|
data: normalizeCart(data),
|
||||||
'Set-Cookie',
|
headers: {
|
||||||
getCartCookie(config.cartCookie, data.id, config.cartCookieMaxAge)
|
'Set-Cookie': getCartCookie(
|
||||||
)
|
config.cartCookie,
|
||||||
res.status(200).json({ data: normalizeCart(data) })
|
data.id,
|
||||||
|
config.cartCookieMaxAge
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default addItem
|
export default addItem
|
||||||
|
@ -1,36 +1,41 @@
|
|||||||
// @ts-nocheck
|
import type { CartEndpoint } from '.'
|
||||||
|
import type { BigcommerceCart } from '../../../types'
|
||||||
|
|
||||||
|
import getCartCookie from '../../utils/get-cart-cookie'
|
||||||
|
|
||||||
import { normalizeCart } from '../../../lib/normalize'
|
import { normalizeCart } from '../../../lib/normalize'
|
||||||
import { BigcommerceApiError } from '../../utils/errors'
|
import { BigcommerceApiError } from '../../utils/errors'
|
||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
|
||||||
import type { BigcommerceCart } from '../../../types'
|
|
||||||
import type { CartEndpoint } from '.'
|
|
||||||
|
|
||||||
// Return current cart info
|
// Return current cart info
|
||||||
const getCart: CartEndpoint['handlers']['getCart'] = async ({
|
const getCart: CartEndpoint['handlers']['getCart'] = async ({
|
||||||
res,
|
|
||||||
body: { cartId },
|
body: { cartId },
|
||||||
config,
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
let result: { data?: BigcommerceCart } = {}
|
|
||||||
|
|
||||||
if (cartId) {
|
if (cartId) {
|
||||||
try {
|
try {
|
||||||
result = await config.storeApiFetch(
|
const result = await config.storeApiFetch<{
|
||||||
|
data?: BigcommerceCart
|
||||||
|
} | null>(
|
||||||
`/v3/carts/${cartId}?include=line_items.physical_items.options,line_items.digital_items.options`
|
`/v3/carts/${cartId}?include=line_items.physical_items.options,line_items.digital_items.options`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: result?.data ? normalizeCart(result.data) : null,
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof BigcommerceApiError && error.status === 404) {
|
if (error instanceof BigcommerceApiError && error.status === 404) {
|
||||||
// Remove the cookie if it exists but the cart wasn't found
|
return {
|
||||||
res.setHeader('Set-Cookie', getCartCookie(config.cartCookie))
|
headers: { 'Set-Cookie': getCartCookie(config.cartCookie) },
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({
|
return {
|
||||||
data: result.data ? normalizeCart(result.data) : null,
|
data: null,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default getCart
|
export default getCart
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
import { type GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
||||||
import cartEndpoint from '@vercel/commerce/api/endpoints/cart'
|
import cartEndpoint from '@vercel/commerce/api/endpoints/cart'
|
||||||
import type { CartSchema } from '@vercel/commerce/types/cart'
|
import type { CartSchema } from '@vercel/commerce/types/cart'
|
||||||
import type { BigcommerceAPI } from '../..'
|
import type { BigcommerceAPI } from '../..'
|
||||||
|
@ -1,34 +1,26 @@
|
|||||||
import { normalizeCart } from '../../../lib/normalize'
|
|
||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
|
||||||
import type { CartEndpoint } from '.'
|
import type { CartEndpoint } from '.'
|
||||||
|
|
||||||
|
import { normalizeCart } from '../../../lib/normalize'
|
||||||
|
import getCartCookie from '../../utils/get-cart-cookie'
|
||||||
|
|
||||||
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
|
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
|
||||||
res,
|
|
||||||
body: { cartId, itemId },
|
body: { cartId, itemId },
|
||||||
config,
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
if (!cartId || !itemId) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Invalid request' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await config.storeApiFetch<{ data: any } | null>(
|
const result = await config.storeApiFetch<{ data: any } | null>(
|
||||||
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
|
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
|
||||||
{ method: 'DELETE' }
|
{ method: 'DELETE' }
|
||||||
)
|
)
|
||||||
const data = result?.data ?? null
|
return {
|
||||||
|
data: result?.data ? normalizeCart(result.data) : null,
|
||||||
res.setHeader(
|
headers: {
|
||||||
'Set-Cookie',
|
'Set-Cookie': result?.data
|
||||||
data
|
|
||||||
? // Update the cart cookie
|
? // Update the cart cookie
|
||||||
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
|
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
|
||||||
: // Remove the cart cookie if the cart was removed (empty items)
|
: // Remove the cart cookie if the cart was removed (empty items)
|
||||||
getCartCookie(config.cartCookie)
|
getCartCookie(config.cartCookie),
|
||||||
)
|
},
|
||||||
res.status(200).json({ data: data && normalizeCart(data) })
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default removeItem
|
export default removeItem
|
||||||
|
@ -1,21 +1,15 @@
|
|||||||
|
import type { CartEndpoint } from '.'
|
||||||
|
import type { BigcommerceCart } from '../../../types'
|
||||||
|
|
||||||
import { normalizeCart } from '../../../lib/normalize'
|
import { normalizeCart } from '../../../lib/normalize'
|
||||||
import { parseCartItem } from '../../utils/parse-item'
|
import { parseCartItem } from '../../utils/parse-item'
|
||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
import getCartCookie from '../../utils/get-cart-cookie'
|
||||||
import type { CartEndpoint } from '.'
|
|
||||||
|
|
||||||
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
|
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
|
||||||
res,
|
|
||||||
body: { cartId, itemId, item },
|
body: { cartId, itemId, item },
|
||||||
config,
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
if (!cartId || !itemId || !item) {
|
const { data } = await config.storeApiFetch<{ data: BigcommerceCart }>(
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Invalid request' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await config.storeApiFetch(
|
|
||||||
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
|
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
|
||||||
{
|
{
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@ -25,12 +19,16 @@ const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update the cart cookie
|
return {
|
||||||
res.setHeader(
|
data: normalizeCart(data),
|
||||||
'Set-Cookie',
|
headers: {
|
||||||
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
|
'Set-Cookie': getCartCookie(
|
||||||
)
|
config.cartCookie,
|
||||||
res.status(200).json({ data: normalizeCart(data) })
|
cartId,
|
||||||
|
config.cartCookieMaxAge
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default updateItem
|
export default updateItem
|
||||||
|
@ -11,7 +11,6 @@ const LIMIT = 12
|
|||||||
|
|
||||||
// Return current cart info
|
// Return current cart info
|
||||||
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
|
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
|
||||||
res,
|
|
||||||
body: { search, categoryId, brandId, sort },
|
body: { search, categoryId, brandId, sort },
|
||||||
config,
|
config,
|
||||||
commerce,
|
commerce,
|
||||||
@ -73,7 +72,7 @@ const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
|
|||||||
if (product) products.push(product)
|
if (product) products.push(product)
|
||||||
})
|
})
|
||||||
|
|
||||||
res.status(200).json({ data: { products, found } })
|
return { data: { products, found } }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default getProducts
|
export default getProducts
|
||||||
|
@ -1,38 +1,47 @@
|
|||||||
import type { CheckoutEndpoint } from '.'
|
import type { CheckoutEndpoint } from '.'
|
||||||
import getCustomerId from '../../utils/get-customer-id'
|
import getCustomerId from '../../utils/get-customer-id'
|
||||||
import jwt from 'jsonwebtoken'
|
|
||||||
import { uuid } from 'uuidv4'
|
|
||||||
|
|
||||||
const fullCheckout = true
|
const fullCheckout = true
|
||||||
|
|
||||||
const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
|
const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
|
||||||
req,
|
req,
|
||||||
res,
|
|
||||||
config,
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
const { cookies } = req
|
const { cookies } = req
|
||||||
const cartId = cookies[config.cartCookie]
|
const cartId = cookies.get(config.cartCookie)
|
||||||
const customerToken = cookies[config.customerCookie]
|
const customerToken = cookies.get(config.customerCookie)
|
||||||
|
|
||||||
if (!cartId) {
|
if (!cartId) {
|
||||||
res.redirect('/cart')
|
return { redirectTo: '/cart' }
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const { data } = await config.storeApiFetch(
|
|
||||||
|
const { data } = await config.storeApiFetch<any>(
|
||||||
`/v3/carts/${cartId}/redirect_urls`,
|
`/v3/carts/${cartId}/redirect_urls`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const customerId =
|
const customerId =
|
||||||
customerToken && (await getCustomerId({ customerToken, config }))
|
customerToken && (await getCustomerId({ customerToken, config }))
|
||||||
|
|
||||||
//if there is a customer create a jwt token
|
//if there is a customer create a jwt token
|
||||||
if (!customerId) {
|
if (!customerId) {
|
||||||
if (fullCheckout) {
|
if (fullCheckout) {
|
||||||
res.redirect(data.checkout_url)
|
return { redirectTo: data.checkout_url }
|
||||||
return
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Dynamically import uuid & jsonwebtoken based on the runtime
|
||||||
|
const { uuid } =
|
||||||
|
process.env.NEXT_RUNTIME === 'edge'
|
||||||
|
? await import('@cfworker/uuid')
|
||||||
|
: await import('uuidv4')
|
||||||
|
|
||||||
|
const jwt =
|
||||||
|
process.env.NEXT_RUNTIME === 'edge'
|
||||||
|
? await import('@tsndr/cloudflare-worker-jwt')
|
||||||
|
: await import('jsonwebtoken')
|
||||||
|
|
||||||
const dateCreated = Math.round(new Date().getTime() / 1000)
|
const dateCreated = Math.round(new Date().getTime() / 1000)
|
||||||
const payload = {
|
const payload = {
|
||||||
iss: config.storeApiClientId,
|
iss: config.storeApiClientId,
|
||||||
@ -42,49 +51,51 @@ const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
|
|||||||
store_hash: config.storeHash,
|
store_hash: config.storeHash,
|
||||||
customer_id: customerId,
|
customer_id: customerId,
|
||||||
channel_id: config.storeChannelId,
|
channel_id: config.storeChannelId,
|
||||||
redirect_to: data.checkout_url.replace(config.storeUrl, ""),
|
redirect_to: data.checkout_url.replace(config.storeUrl, ''),
|
||||||
}
|
}
|
||||||
let token = jwt.sign(payload, config.storeApiClientSecret!, {
|
let token = jwt.sign(payload, config.storeApiClientSecret!, {
|
||||||
algorithm: 'HS256',
|
algorithm: 'HS256',
|
||||||
})
|
})
|
||||||
let checkouturl = `${config.storeUrl}/login/token/${token}`
|
let checkouturl = `${config.storeUrl}/login/token/${token}`
|
||||||
console.log('checkouturl', checkouturl)
|
|
||||||
if (fullCheckout) {
|
if (fullCheckout) {
|
||||||
res.redirect(checkouturl)
|
return { redirectTo: checkouturl }
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make the embedded checkout work too!
|
// TODO: make the embedded checkout work too!
|
||||||
const html = `
|
// const html = `
|
||||||
<!DOCTYPE html>
|
// <!DOCTYPE html>
|
||||||
<html lang="en">
|
// <html lang="en">
|
||||||
<head>
|
// <head>
|
||||||
<meta charset="UTF-8">
|
// <meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
// <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Checkout</title>
|
// <title>Checkout</title>
|
||||||
<script src="https://checkout-sdk.bigcommerce.com/v1/loader.js"></script>
|
// <script src="https://checkout-sdk.bigcommerce.com/v1/loader.js"></script>
|
||||||
<script>
|
// <script>
|
||||||
window.onload = function() {
|
// window.onload = function() {
|
||||||
checkoutKitLoader.load('checkout-sdk').then(function (service) {
|
// checkoutKitLoader.load('checkout-sdk').then(function (service) {
|
||||||
service.embedCheckout({
|
// service.embedCheckout({
|
||||||
containerId: 'checkout',
|
// containerId: 'checkout',
|
||||||
url: '${data.embedded_checkout_url}'
|
// url: '${data.embedded_checkout_url}'
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
</script>
|
// </script>
|
||||||
</head>
|
// </head>
|
||||||
<body>
|
// <body>
|
||||||
<div id="checkout"></div>
|
// <div id="checkout"></div>
|
||||||
</body>
|
// </body>
|
||||||
</html>
|
// </html>
|
||||||
`
|
// `
|
||||||
|
|
||||||
res.status(200)
|
// return new Response(html, {
|
||||||
res.setHeader('Content-Type', 'text/html')
|
// headers: {
|
||||||
res.write(html)
|
// 'Content-Type': 'text/html',
|
||||||
res.end()
|
// },
|
||||||
|
// })
|
||||||
|
|
||||||
|
return { data: null }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default getCheckout
|
export default getCheckout
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export default function noopApi(...args: any[]): void {}
|
|
@ -1 +0,0 @@
|
|||||||
export default function noopApi(...args: any[]): void {}
|
|
@ -1,5 +1,6 @@
|
|||||||
import type { GetLoggedInCustomerQuery } from '../../../../schema'
|
import type { GetLoggedInCustomerQuery } from '../../../../schema'
|
||||||
import type { CustomerEndpoint } from '.'
|
import type { CustomerEndpoint } from '.'
|
||||||
|
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
|
||||||
|
|
||||||
export const getLoggedInCustomerQuery = /* GraphQL */ `
|
export const getLoggedInCustomerQuery = /* GraphQL */ `
|
||||||
query getLoggedInCustomer {
|
query getLoggedInCustomer {
|
||||||
@ -25,29 +26,26 @@ export const getLoggedInCustomerQuery = /* GraphQL */ `
|
|||||||
export type Customer = NonNullable<GetLoggedInCustomerQuery['customer']>
|
export type Customer = NonNullable<GetLoggedInCustomerQuery['customer']>
|
||||||
|
|
||||||
const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] =
|
const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] =
|
||||||
async ({ req, res, config }) => {
|
async ({ req, config }) => {
|
||||||
const token = req.cookies[config.customerCookie]
|
const token = req.cookies.get(config.customerCookie)
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
const { data } = await config.fetch<GetLoggedInCustomerQuery>(
|
const { data } = await config.fetch<GetLoggedInCustomerQuery>(
|
||||||
getLoggedInCustomerQuery,
|
getLoggedInCustomerQuery,
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
headers: {
|
'Set-Cookie': `${config.customerCookie}=${token}`,
|
||||||
cookie: `${config.customerCookie}=${token}`,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
const { customer } = data
|
const { customer } = data
|
||||||
|
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
return res.status(400).json({
|
throw new CommerceAPIError('Customer not found', {
|
||||||
data: null,
|
status: 404,
|
||||||
errors: [{ message: 'Customer not found', code: 'not_found' }],
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json({
|
return {
|
||||||
data: {
|
data: {
|
||||||
customer: {
|
customer: {
|
||||||
id: String(customer.entityId),
|
id: String(customer.entityId),
|
||||||
@ -59,10 +57,12 @@ const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] =
|
|||||||
notes: customer.notes,
|
notes: customer.notes,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({ data: null })
|
return {
|
||||||
|
data: null,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default getLoggedInCustomer
|
export default getLoggedInCustomer
|
||||||
|
27
packages/bigcommerce/src/api/endpoints/index.ts
Normal file
27
packages/bigcommerce/src/api/endpoints/index.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import type { BigcommerceAPI, Provider } from '..'
|
||||||
|
|
||||||
|
import createEndpoints from '@vercel/commerce/api/endpoints'
|
||||||
|
|
||||||
|
import cart from './cart'
|
||||||
|
import login from './login'
|
||||||
|
import logout from './logout'
|
||||||
|
import signup from './signup'
|
||||||
|
import checkout from './checkout'
|
||||||
|
import customer from './customer'
|
||||||
|
import wishlist from './wishlist'
|
||||||
|
import products from './catalog/products'
|
||||||
|
|
||||||
|
const endpoints = {
|
||||||
|
cart,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
signup,
|
||||||
|
checkout,
|
||||||
|
wishlist,
|
||||||
|
customer,
|
||||||
|
'catalog/products': products,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function bigcommerceAPI(commerce: BigcommerceAPI) {
|
||||||
|
return createEndpoints<Provider>(commerce, endpoints)
|
||||||
|
}
|
@ -1,49 +1,35 @@
|
|||||||
import { FetcherError } from '@vercel/commerce/utils/errors'
|
|
||||||
import type { LoginEndpoint } from '.'
|
import type { LoginEndpoint } from '.'
|
||||||
|
|
||||||
|
import { FetcherError } from '@vercel/commerce/utils/errors'
|
||||||
|
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
|
||||||
|
|
||||||
const invalidCredentials = /invalid credentials/i
|
const invalidCredentials = /invalid credentials/i
|
||||||
|
|
||||||
const login: LoginEndpoint['handlers']['login'] = async ({
|
const login: LoginEndpoint['handlers']['login'] = async ({
|
||||||
res,
|
|
||||||
body: { email, password },
|
body: { email, password },
|
||||||
config,
|
config,
|
||||||
commerce,
|
commerce,
|
||||||
}) => {
|
}) => {
|
||||||
// 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 {
|
try {
|
||||||
|
const res = new Response()
|
||||||
await commerce.login({ variables: { email, password }, config, res })
|
await commerce.login({ variables: { email, password }, config, res })
|
||||||
|
return {
|
||||||
|
status: res.status,
|
||||||
|
headers: res.headers,
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Check if the email and password didn't match an existing account
|
// Check if the email and password didn't match an existing account
|
||||||
if (
|
if (error instanceof FetcherError) {
|
||||||
error instanceof FetcherError &&
|
throw new CommerceAPIError(
|
||||||
invalidCredentials.test(error.message)
|
invalidCredentials.test(error.message)
|
||||||
) {
|
? 'Cannot find an account that matches the provided credentials'
|
||||||
return res.status(401).json({
|
: error.message,
|
||||||
data: null,
|
{ status: error.status || 401 }
|
||||||
errors: [
|
)
|
||||||
{
|
} else {
|
||||||
message:
|
|
||||||
'Cannot find an account that matches the provided credentials',
|
|
||||||
code: 'invalid_credentials',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
}
|
||||||
res.status(200).json({ data: null })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default login
|
export default login
|
||||||
|
@ -2,21 +2,23 @@ import { serialize } from 'cookie'
|
|||||||
import type { LogoutEndpoint } from '.'
|
import type { LogoutEndpoint } from '.'
|
||||||
|
|
||||||
const logout: LogoutEndpoint['handlers']['logout'] = async ({
|
const logout: LogoutEndpoint['handlers']['logout'] = async ({
|
||||||
res,
|
|
||||||
body: { redirectTo },
|
body: { redirectTo },
|
||||||
config,
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
// Remove the cookie
|
const headers = {
|
||||||
res.setHeader(
|
'Set-Cookie': serialize(config.customerCookie, '', {
|
||||||
'Set-Cookie',
|
maxAge: -1,
|
||||||
serialize(config.customerCookie, '', { maxAge: -1, path: '/' })
|
path: '/',
|
||||||
)
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
// Only allow redirects to a relative URL
|
return redirectTo
|
||||||
if (redirectTo?.startsWith('/')) {
|
? {
|
||||||
res.redirect(redirectTo)
|
redirectTo,
|
||||||
} else {
|
headers,
|
||||||
res.status(200).json({ data: null })
|
}
|
||||||
|
: {
|
||||||
|
headers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,23 +1,13 @@
|
|||||||
import { BigcommerceApiError } from '../../utils/errors'
|
|
||||||
import type { SignupEndpoint } from '.'
|
import type { SignupEndpoint } from '.'
|
||||||
|
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
|
||||||
|
|
||||||
|
import { BigcommerceApiError } from '../../utils/errors'
|
||||||
|
|
||||||
const signup: SignupEndpoint['handlers']['signup'] = async ({
|
const signup: SignupEndpoint['handlers']['signup'] = async ({
|
||||||
res,
|
|
||||||
body: { firstName, lastName, email, password },
|
body: { firstName, lastName, email, password },
|
||||||
config,
|
config,
|
||||||
commerce,
|
commerce,
|
||||||
}) => {
|
}) => {
|
||||||
// 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 {
|
try {
|
||||||
await config.storeApiFetch('/v3/customers', {
|
await config.storeApiFetch('/v3/customers', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -35,28 +25,26 @@ const signup: SignupEndpoint['handlers']['signup'] = async ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof BigcommerceApiError && error.status === 422) {
|
if (error instanceof BigcommerceApiError && error.status === 422) {
|
||||||
const hasEmailError = '0.email' in error.data?.errors
|
const hasEmailError = '0.email' in error.data?.errors
|
||||||
|
|
||||||
// If there's an error with the email, it most likely means it's duplicated
|
// If there's an error with the email, it most likely means it's duplicated
|
||||||
if (hasEmailError) {
|
if (hasEmailError) {
|
||||||
return res.status(400).json({
|
throw new CommerceAPIError('Email already in use', {
|
||||||
data: null,
|
status: 400,
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
message: 'The email is already in use',
|
|
||||||
code: 'duplicated_email',
|
code: 'duplicated_email',
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error
|
const res = new Response()
|
||||||
}
|
|
||||||
|
|
||||||
// Login the customer right after creating it
|
// Login the customer right after creating it
|
||||||
await commerce.login({ variables: { email, password }, res, config })
|
await commerce.login({ variables: { email, password }, res, config })
|
||||||
|
|
||||||
res.status(200).json({ data: null })
|
return {
|
||||||
|
headers: res.headers,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default signup
|
export default signup
|
||||||
|
@ -1,22 +1,12 @@
|
|||||||
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
|
||||||
import { parseWishlistItem } from '../../utils/parse-item'
|
import { parseWishlistItem } from '../../utils/parse-item'
|
||||||
import getCustomerId from '../../utils/get-customer-id'
|
import getCustomerId from '../../utils/get-customer-id'
|
||||||
import type { WishlistEndpoint } from '.'
|
import type { WishlistEndpoint } from '.'
|
||||||
|
|
||||||
const addItem: WishlistEndpoint['handlers']['addItem'] = async ({
|
const addItem: WishlistEndpoint['handlers']['addItem'] = async ({
|
||||||
res,
|
|
||||||
body: { customerToken, item },
|
body: { customerToken, item },
|
||||||
config,
|
config,
|
||||||
commerce,
|
commerce,
|
||||||
}) => {
|
}) => {
|
||||||
if (!item) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Missing item' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const customerId =
|
const customerId =
|
||||||
customerToken && (await getCustomerId({ customerToken, config }))
|
customerToken && (await getCustomerId({ customerToken, config }))
|
||||||
|
|
||||||
@ -31,7 +21,7 @@ const addItem: WishlistEndpoint['handlers']['addItem'] = async ({
|
|||||||
|
|
||||||
if (!wishlist) {
|
if (!wishlist) {
|
||||||
// If user has no wishlist, then let's create one with new item
|
// If user has no wishlist, then let's create one with new item
|
||||||
const { data } = await config.storeApiFetch('/v3/wishlists', {
|
const { data } = await config.storeApiFetch<any>('/v3/wishlists', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: 'Next.js Commerce Wishlist',
|
name: 'Next.js Commerce Wishlist',
|
||||||
@ -40,11 +30,13 @@ const addItem: WishlistEndpoint['handlers']['addItem'] = async ({
|
|||||||
items: [parseWishlistItem(item)],
|
items: [parseWishlistItem(item)],
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
return res.status(200).json(data)
|
return {
|
||||||
|
data,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Existing Wishlist, let's add Item to Wishlist
|
// Existing Wishlist, let's add Item to Wishlist
|
||||||
const { data } = await config.storeApiFetch(
|
const { data } = await config.storeApiFetch<any>(
|
||||||
`/v3/wishlists/${wishlist.id}/items`,
|
`/v3/wishlists/${wishlist.id}/items`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -55,13 +47,7 @@ const addItem: WishlistEndpoint['handlers']['addItem'] = async ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Returns Wishlist
|
// Returns Wishlist
|
||||||
return res.status(200).json(data)
|
return { data }
|
||||||
} catch (err: any) {
|
|
||||||
res.status(500).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: err.message }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default addItem
|
export default addItem
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
|
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
|
||||||
import type { Wishlist } from '@vercel/commerce/types/wishlist'
|
import type { Wishlist } from '@vercel/commerce/types/wishlist'
|
||||||
import type { WishlistEndpoint } from '.'
|
import type { WishlistEndpoint } from '.'
|
||||||
import getCustomerId from '../../utils/get-customer-id'
|
import getCustomerId from '../../utils/get-customer-id'
|
||||||
|
|
||||||
// Return wishlist info
|
// Return wishlist info
|
||||||
const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({
|
const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({
|
||||||
res,
|
|
||||||
body: { customerToken, includeProducts },
|
body: { customerToken, includeProducts },
|
||||||
config,
|
config,
|
||||||
commerce,
|
commerce,
|
||||||
@ -16,11 +16,7 @@ const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({
|
|||||||
customerToken && (await getCustomerId({ customerToken, config }))
|
customerToken && (await getCustomerId({ customerToken, config }))
|
||||||
|
|
||||||
if (!customerId) {
|
if (!customerId) {
|
||||||
// If the customerToken is invalid, then this request is too
|
throw new CommerceAPIError('Wishlist not found', { status: 404 })
|
||||||
return res.status(404).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Wishlist not found' }],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { wishlist } = await commerce.getCustomerWishlist({
|
const { wishlist } = await commerce.getCustomerWishlist({
|
||||||
@ -32,7 +28,7 @@ const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({
|
|||||||
result = { data: wishlist }
|
result = { data: wishlist }
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({ data: result.data ?? null })
|
return { data: result.data ?? null }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default getWishlist
|
export default getWishlist
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import type { Wishlist } from '@vercel/commerce/types/wishlist'
|
import type { Wishlist } from '@vercel/commerce/types/wishlist'
|
||||||
import getCustomerId from '../../utils/get-customer-id'
|
import getCustomerId from '../../utils/get-customer-id'
|
||||||
import type { WishlistEndpoint } from '.'
|
import type { WishlistEndpoint } from '.'
|
||||||
|
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
|
||||||
|
|
||||||
// Return wishlist info
|
// Return wishlist info
|
||||||
const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({
|
const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({
|
||||||
res,
|
|
||||||
body: { customerToken, itemId },
|
body: { customerToken, itemId },
|
||||||
config,
|
config,
|
||||||
commerce,
|
commerce,
|
||||||
@ -20,10 +20,7 @@ const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({
|
|||||||
{}
|
{}
|
||||||
|
|
||||||
if (!wishlist || !itemId) {
|
if (!wishlist || !itemId) {
|
||||||
return res.status(400).json({
|
throw new CommerceAPIError('Wishlist not found', { status: 400 })
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Invalid request' }],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await config.storeApiFetch<{ data: Wishlist } | null>(
|
const result = await config.storeApiFetch<{ data: Wishlist } | null>(
|
||||||
@ -32,7 +29,7 @@ const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({
|
|||||||
)
|
)
|
||||||
const data = result?.data ?? null
|
const data = result?.data ?? null
|
||||||
|
|
||||||
res.status(200).json({ data })
|
return { data }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default removeItem
|
export default removeItem
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import type { RequestInit } from '@vercel/fetch'
|
|
||||||
import {
|
import {
|
||||||
CommerceAPI,
|
CommerceAPI,
|
||||||
CommerceAPIConfig,
|
CommerceAPIConfig,
|
||||||
@ -35,7 +34,14 @@ export interface BigcommerceConfig extends CommerceAPIConfig {
|
|||||||
storeUrl?: string
|
storeUrl?: string
|
||||||
storeApiClientSecret?: string
|
storeApiClientSecret?: string
|
||||||
storeHash?: string
|
storeHash?: string
|
||||||
storeApiFetch<T>(endpoint: string, options?: RequestInit): Promise<T>
|
storeApiFetch<T>(
|
||||||
|
endpoint: string,
|
||||||
|
options?: {
|
||||||
|
method?: string
|
||||||
|
body?: any
|
||||||
|
headers?: HeadersInit
|
||||||
|
}
|
||||||
|
): Promise<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
const API_URL = process.env.BIGCOMMERCE_STOREFRONT_API_URL // GraphAPI
|
const API_URL = process.env.BIGCOMMERCE_STOREFRONT_API_URL // GraphAPI
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import type { ServerResponse } from 'http'
|
|
||||||
import type {
|
import type {
|
||||||
OperationContext,
|
OperationContext,
|
||||||
OperationOptions,
|
OperationOptions,
|
||||||
@ -23,14 +22,14 @@ export default function loginOperation({
|
|||||||
async function login<T extends LoginOperation>(opts: {
|
async function login<T extends LoginOperation>(opts: {
|
||||||
variables: T['variables']
|
variables: T['variables']
|
||||||
config?: BigcommerceConfig
|
config?: BigcommerceConfig
|
||||||
res: ServerResponse
|
res: Response
|
||||||
}): Promise<T['data']>
|
}): Promise<T['data']>
|
||||||
|
|
||||||
async function login<T extends LoginOperation>(
|
async function login<T extends LoginOperation>(
|
||||||
opts: {
|
opts: {
|
||||||
variables: T['variables']
|
variables: T['variables']
|
||||||
config?: BigcommerceConfig
|
config?: BigcommerceConfig
|
||||||
res: ServerResponse
|
res: Response
|
||||||
} & OperationOptions
|
} & OperationOptions
|
||||||
): Promise<T['data']>
|
): Promise<T['data']>
|
||||||
|
|
||||||
@ -42,7 +41,7 @@ export default function loginOperation({
|
|||||||
}: {
|
}: {
|
||||||
query?: string
|
query?: string
|
||||||
variables: T['variables']
|
variables: T['variables']
|
||||||
res: ServerResponse
|
res: Response
|
||||||
config?: BigcommerceConfig
|
config?: BigcommerceConfig
|
||||||
}): Promise<T['data']> {
|
}): Promise<T['data']> {
|
||||||
config = commerce.getConfig(config)
|
config = commerce.getConfig(config)
|
||||||
@ -64,11 +63,16 @@ export default function loginOperation({
|
|||||||
cookie = cookie.replace(/; SameSite=none/gi, '; SameSite=lax')
|
cookie = cookie.replace(/; SameSite=none/gi, '; SameSite=lax')
|
||||||
}
|
}
|
||||||
|
|
||||||
response.setHeader(
|
const prevCookie = response.headers.get('Set-Cookie')
|
||||||
|
const newCookie = concatHeader(prevCookie, cookie)
|
||||||
|
|
||||||
|
if (newCookie) {
|
||||||
|
res.headers.set(
|
||||||
'Set-Cookie',
|
'Set-Cookie',
|
||||||
concatHeader(response.getHeader('Set-Cookie'), cookie)!
|
String(Array.isArray(newCookie) ? newCookie.join(',') : newCookie)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result: data.login?.result,
|
result: data.login?.result,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
type Header = string | number | string[] | undefined
|
type Header = string | number | string[] | undefined | null
|
||||||
|
|
||||||
export default function concatHeader(prev: Header, val: Header) {
|
export default function concatHeader(prev: Header, val: Header) {
|
||||||
if (!val) return prev
|
if (!val) return prev
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import type { Response } from '@vercel/fetch'
|
|
||||||
|
|
||||||
// Used for GraphQL errors
|
// Used for GraphQL errors
|
||||||
export class BigcommerceGraphQLError extends Error {}
|
export class BigcommerceGraphQLError extends Error {}
|
||||||
|
|
||||||
|
@ -1,19 +1,22 @@
|
|||||||
import { FetcherError } from '@vercel/commerce/utils/errors'
|
import { FetcherError } from '@vercel/commerce/utils/errors'
|
||||||
import type { GraphQLFetcher } from '@vercel/commerce/api'
|
import type { GraphQLFetcher } from '@vercel/commerce/api'
|
||||||
import type { BigcommerceConfig } from '../index'
|
import type { BigcommerceConfig } from '../index'
|
||||||
import fetch from './fetch'
|
|
||||||
|
|
||||||
const fetchGraphqlApi: (getConfig: () => BigcommerceConfig) => GraphQLFetcher =
|
const fetchGraphqlApi: (getConfig: () => BigcommerceConfig) => GraphQLFetcher =
|
||||||
(getConfig) =>
|
(getConfig) =>
|
||||||
async (query: string, { variables, preview } = {}, fetchOptions) => {
|
async (
|
||||||
|
query: string,
|
||||||
|
{ variables, preview } = {},
|
||||||
|
options: { headers?: HeadersInit } = {}
|
||||||
|
): Promise<any> => {
|
||||||
// log.warn(query)
|
// log.warn(query)
|
||||||
const config = getConfig()
|
const config = getConfig()
|
||||||
|
|
||||||
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
|
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
|
||||||
...fetchOptions,
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${config.apiToken}`,
|
Authorization: `Bearer ${config.apiToken}`,
|
||||||
...fetchOptions?.headers,
|
...options.headers,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
import type { FetchOptions, Response } from '@vercel/fetch'
|
|
||||||
import type { BigcommerceConfig } from '../index'
|
import type { BigcommerceConfig } from '../index'
|
||||||
import { BigcommerceApiError, BigcommerceNetworkError } from './errors'
|
import { BigcommerceApiError, BigcommerceNetworkError } from './errors'
|
||||||
import fetch from './fetch'
|
|
||||||
|
|
||||||
const fetchStoreApi =
|
const fetchStoreApi =
|
||||||
<T>(getConfig: () => BigcommerceConfig) =>
|
<T>(getConfig: () => BigcommerceConfig) =>
|
||||||
async (endpoint: string, options?: FetchOptions): Promise<T> => {
|
async (
|
||||||
|
endpoint: string,
|
||||||
|
options?: {
|
||||||
|
method?: string
|
||||||
|
body?: any
|
||||||
|
headers?: HeadersInit
|
||||||
|
}
|
||||||
|
): Promise<T> => {
|
||||||
const config = getConfig()
|
const config = getConfig()
|
||||||
let res: Response
|
let res: Response
|
||||||
|
|
||||||
|
@ -20,9 +20,7 @@ async function getCustomerId({
|
|||||||
getCustomerIdQuery,
|
getCustomerIdQuery,
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
headers: {
|
'Set-Cookie': `${config.customerCookie}=${customerToken}`,
|
||||||
cookie: `${config.customerCookie}=${customerToken}`,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ export default useLogin as UseLogin<typeof handler>
|
|||||||
|
|
||||||
export const handler: MutationHook<LoginHook> = {
|
export const handler: MutationHook<LoginHook> = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
url: '/api/login',
|
url: '/api/commerce/login',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
},
|
},
|
||||||
async fetcher({ input: { email, password }, options, fetch }) {
|
async fetcher({ input: { email, password }, options, fetch }) {
|
||||||
|
@ -8,7 +8,7 @@ export default useLogout as UseLogout<typeof handler>
|
|||||||
|
|
||||||
export const handler: MutationHook<LogoutHook> = {
|
export const handler: MutationHook<LogoutHook> = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
url: '/api/logout',
|
url: '/api/commerce/logout',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
},
|
},
|
||||||
useHook:
|
useHook:
|
||||||
|
@ -9,7 +9,7 @@ export default useSignup as UseSignup<typeof handler>
|
|||||||
|
|
||||||
export const handler: MutationHook<SignupHook> = {
|
export const handler: MutationHook<SignupHook> = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
url: '/api/signup',
|
url: '/api/commerce/signup',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
},
|
},
|
||||||
async fetcher({
|
async fetcher({
|
||||||
|
@ -9,7 +9,7 @@ export default useAddItem as UseAddItem<typeof handler>
|
|||||||
|
|
||||||
export const handler: MutationHook<AddItemHook> = {
|
export const handler: MutationHook<AddItemHook> = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
url: '/api/cart',
|
url: '/api/commerce/cart',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
},
|
},
|
||||||
async fetcher({ input: item, options, fetch }) {
|
async fetcher({ input: item, options, fetch }) {
|
||||||
@ -33,7 +33,6 @@ export const handler: MutationHook<AddItemHook> = {
|
|||||||
({ fetch }) =>
|
({ fetch }) =>
|
||||||
() => {
|
() => {
|
||||||
const { mutate } = useCart()
|
const { mutate } = useCart()
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async function addItem(input) {
|
async function addItem(input) {
|
||||||
const data = await fetch({ input })
|
const data = await fetch({ input })
|
||||||
|
@ -7,7 +7,7 @@ export default useCart as UseCart<typeof handler>
|
|||||||
|
|
||||||
export const handler: SWRHook<GetCartHook> = {
|
export const handler: SWRHook<GetCartHook> = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
url: '/api/cart',
|
url: '/api/commerce/cart',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
},
|
},
|
||||||
useHook:
|
useHook:
|
||||||
|
@ -26,7 +26,7 @@ export default useRemoveItem as UseRemoveItem<typeof handler>
|
|||||||
|
|
||||||
export const handler = {
|
export const handler = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
url: '/api/cart',
|
url: '/api/commerce/cart',
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
},
|
},
|
||||||
async fetcher({
|
async fetcher({
|
||||||
|
@ -5,7 +5,9 @@ import type {
|
|||||||
HookFetcherContext,
|
HookFetcherContext,
|
||||||
} from '@vercel/commerce/utils/types'
|
} from '@vercel/commerce/utils/types'
|
||||||
import { ValidationError } from '@vercel/commerce/utils/errors'
|
import { ValidationError } from '@vercel/commerce/utils/errors'
|
||||||
import useUpdateItem, { UseUpdateItem } from '@vercel/commerce/cart/use-update-item'
|
import useUpdateItem, {
|
||||||
|
UseUpdateItem,
|
||||||
|
} from '@vercel/commerce/cart/use-update-item'
|
||||||
import type { LineItem, UpdateItemHook } from '@vercel/commerce/types/cart'
|
import type { LineItem, UpdateItemHook } from '@vercel/commerce/types/cart'
|
||||||
import { handler as removeItemHandler } from './use-remove-item'
|
import { handler as removeItemHandler } from './use-remove-item'
|
||||||
import useCart from './use-cart'
|
import useCart from './use-cart'
|
||||||
@ -18,7 +20,7 @@ export default useUpdateItem as UseUpdateItem<typeof handler>
|
|||||||
|
|
||||||
export const handler = {
|
export const handler = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
url: '/api/cart',
|
url: '/api/commerce/cart',
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
},
|
},
|
||||||
async fetcher({
|
async fetcher({
|
||||||
|
@ -8,7 +8,7 @@ export default useCustomer as UseCustomer<typeof handler>
|
|||||||
|
|
||||||
export const handler: SWRHook<CustomerHook> = {
|
export const handler: SWRHook<CustomerHook> = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
url: '/api/customer',
|
url: '/api/commerce/customer',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
},
|
},
|
||||||
async fetcher({ options, fetch }) {
|
async fetcher({ options, fetch }) {
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { getCommerceProvider, useCommerce as useCoreCommerce } from '@vercel/commerce'
|
import {
|
||||||
|
getCommerceProvider,
|
||||||
|
useCommerce as useCoreCommerce,
|
||||||
|
} from '@vercel/commerce'
|
||||||
import { bigcommerceProvider, BigcommerceProvider } from './provider'
|
import { bigcommerceProvider, BigcommerceProvider } from './provider'
|
||||||
|
|
||||||
export { bigcommerceProvider }
|
export { bigcommerceProvider }
|
||||||
|
@ -3,9 +3,9 @@ import type { Product } from '@vercel/commerce/types/product'
|
|||||||
import type { Cart, LineItem } from '@vercel/commerce/types/cart'
|
import type { Cart, LineItem } from '@vercel/commerce/types/cart'
|
||||||
import type { Category, Brand } from '@vercel/commerce/types/site'
|
import type { Category, Brand } from '@vercel/commerce/types/site'
|
||||||
import type { BigcommerceCart, BCCategory, BCBrand } from '../types'
|
import type { BigcommerceCart, BCCategory, BCBrand } from '../types'
|
||||||
|
import type { ProductNode } from '../api/operations/get-all-products'
|
||||||
|
import type { definitions } from '../api/definitions/store-content'
|
||||||
|
|
||||||
import { definitions } from '../api/definitions/store-content'
|
|
||||||
import update from './immutability'
|
|
||||||
import getSlug from './get-slug'
|
import getSlug from './get-slug'
|
||||||
|
|
||||||
function normalizeProductOption(productOption: any) {
|
function normalizeProductOption(productOption: any) {
|
||||||
@ -20,55 +20,44 @@ function normalizeProductOption(productOption: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeProduct(productNode: any): Product {
|
export function normalizeProduct(productNode: ProductNode): Product {
|
||||||
const {
|
const {
|
||||||
entityId: id,
|
entityId: id,
|
||||||
productOptions,
|
productOptions,
|
||||||
prices,
|
prices,
|
||||||
path,
|
path,
|
||||||
id: _,
|
images,
|
||||||
options: _0,
|
variants,
|
||||||
} = productNode
|
} = productNode
|
||||||
|
|
||||||
return update(productNode, {
|
return {
|
||||||
id: { $set: String(id) },
|
id: String(id),
|
||||||
images: {
|
name: productNode.name,
|
||||||
$apply: ({ edges }: any) =>
|
description: productNode.description,
|
||||||
edges?.map(({ node: { urlOriginal, altText, ...rest } }: any) => ({
|
images:
|
||||||
|
images.edges?.map(({ node: { urlOriginal, altText, ...rest } }: any) => ({
|
||||||
url: urlOriginal,
|
url: urlOriginal,
|
||||||
alt: altText,
|
alt: altText,
|
||||||
...rest,
|
...rest,
|
||||||
})),
|
})) || [],
|
||||||
},
|
path: `/${getSlug(path)}`,
|
||||||
variants: {
|
variants:
|
||||||
$apply: ({ edges }: any) =>
|
variants.edges?.map(
|
||||||
edges?.map(({ node: { entityId, productOptions, ...rest } }: any) => ({
|
({ node: { entityId, productOptions, ...rest } }: any) => ({
|
||||||
id: String(entityId),
|
id: String(entityId),
|
||||||
options: productOptions?.edges
|
options: productOptions?.edges
|
||||||
? productOptions.edges.map(normalizeProductOption)
|
? productOptions.edges.map(normalizeProductOption)
|
||||||
: [],
|
: [],
|
||||||
...rest,
|
...rest,
|
||||||
})),
|
})
|
||||||
},
|
) || [],
|
||||||
options: {
|
options: productOptions?.edges?.map(normalizeProductOption) || [],
|
||||||
$set: productOptions.edges
|
slug: path?.replace(/^\/+|\/+$/g, ''),
|
||||||
? productOptions?.edges.map(normalizeProductOption)
|
|
||||||
: [],
|
|
||||||
},
|
|
||||||
brand: {
|
|
||||||
$apply: (brand: any) => (brand?.id ? brand.id : null),
|
|
||||||
},
|
|
||||||
slug: {
|
|
||||||
$set: path?.replace(/^\/+|\/+$/g, ''),
|
|
||||||
},
|
|
||||||
price: {
|
price: {
|
||||||
$set: {
|
|
||||||
value: prices?.price.value,
|
value: prices?.price.value,
|
||||||
currencyCode: prices?.price.currencyCode,
|
currencyCode: prices?.price.currencyCode,
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
$unset: ['entityId'],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizePage(page: definitions['page_Full']): Page {
|
export function normalizePage(page: definitions['page_Full']): Page {
|
||||||
@ -122,7 +111,7 @@ function normalizeLineItem(item: any): LineItem {
|
|||||||
listPrice: item.list_price,
|
listPrice: item.list_price,
|
||||||
},
|
},
|
||||||
options: item.options,
|
options: item.options,
|
||||||
path: item.url.split('/')[3],
|
path: `/${item.url.split('/')[3]}`,
|
||||||
discounts: item.discounts.map((discount: any) => ({
|
discounts: item.discounts.map((discount: any) => ({
|
||||||
value: discount.discounted_amount,
|
value: discount.discounted_amount,
|
||||||
})),
|
})),
|
||||||
|
@ -14,7 +14,7 @@ export type SearchProductsInput = {
|
|||||||
|
|
||||||
export const handler: SWRHook<SearchProductsHook> = {
|
export const handler: SWRHook<SearchProductsHook> = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
url: '/api/catalog/products',
|
url: '/api/commerce/catalog/products',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
},
|
},
|
||||||
fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) {
|
fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) {
|
||||||
|
@ -12,7 +12,7 @@ export default useAddItem as UseAddItem<typeof handler>
|
|||||||
|
|
||||||
export const handler: MutationHook<AddItemHook> = {
|
export const handler: MutationHook<AddItemHook> = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
url: '/api/wishlist',
|
url: '/api/commerce/wishlist',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
},
|
},
|
||||||
useHook:
|
useHook:
|
||||||
|
@ -12,7 +12,7 @@ export default useRemoveItem as UseRemoveItem<typeof handler>
|
|||||||
|
|
||||||
export const handler: MutationHook<RemoveItemHook> = {
|
export const handler: MutationHook<RemoveItemHook> = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
url: '/api/wishlist',
|
url: '/api/commerce/wishlist',
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
},
|
},
|
||||||
useHook:
|
useHook:
|
||||||
|
@ -10,7 +10,7 @@ import type { GetWishlistHook } from '@vercel/commerce/types/wishlist'
|
|||||||
export default useWishlist as UseWishlist<typeof handler>
|
export default useWishlist as UseWishlist<typeof handler>
|
||||||
export const handler: SWRHook<GetWishlistHook> = {
|
export const handler: SWRHook<GetWishlistHook> = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
url: '/api/wishlist',
|
url: '/api/commerce/wishlist',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
},
|
},
|
||||||
async fetcher({ input: { customerId, includeProducts }, options, fetch }) {
|
async fetcher({ input: { customerId, includeProducts }, options, fetch }) {
|
||||||
|
@ -69,7 +69,10 @@ Then, open [/site/.env.template](/site/.env.template) and add the provider name
|
|||||||
Using BigCommerce as an example. The first thing to do is export a `CommerceProvider` component that includes a `provider` object with all the handlers that can be used for hooks:
|
Using BigCommerce as an example. The first thing to do is export a `CommerceProvider` component that includes a `provider` object with all the handlers that can be used for hooks:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { getCommerceProvider, useCommerce as useCoreCommerce } from '@vercel/commerce'
|
import {
|
||||||
|
getCommerceProvider,
|
||||||
|
useCommerce as useCoreCommerce,
|
||||||
|
} from '@vercel/commerce'
|
||||||
import { bigcommerceProvider, BigcommerceProvider } from './provider'
|
import { bigcommerceProvider, BigcommerceProvider } from './provider'
|
||||||
|
|
||||||
export { bigcommerceProvider }
|
export { bigcommerceProvider }
|
||||||
@ -135,7 +138,7 @@ export default useCart as UseCart<typeof handler>
|
|||||||
|
|
||||||
export const handler: SWRHook<GetCartHook> = {
|
export const handler: SWRHook<GetCartHook> = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
url: '/api/cart',
|
url: '/api/commerce/cart',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
},
|
},
|
||||||
useHook:
|
useHook:
|
||||||
@ -175,7 +178,7 @@ export default useAddItem as UseAddItem<typeof handler>
|
|||||||
|
|
||||||
export const handler: MutationHook<AddItemHook> = {
|
export const handler: MutationHook<AddItemHook> = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
url: '/api/cart',
|
url: '/api/commerce/cart',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
},
|
},
|
||||||
async fetcher({ input: item, options, fetch }) {
|
async fetcher({ input: item, options, fetch }) {
|
||||||
@ -213,25 +216,26 @@ export const handler: MutationHook<AddItemHook> = {
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Showing progress and features
|
## Showing progress and features
|
||||||
|
|
||||||
When creating a PR for a new provider, include this list in the PR description and mark the progress as you push so we can organize the code review. Not all points are required (but advised) so make sure to keep the list up to date.
|
When creating a PR for a new provider, include this list in the PR description and mark the progress as you push so we can organize the code review. Not all points are required (but advised) so make sure to keep the list up to date.
|
||||||
|
|
||||||
**Status**
|
**Status**
|
||||||
|
|
||||||
* [ ] CommerceProvider
|
- [ ] CommerceProvider
|
||||||
* [ ] Schema & TS types
|
- [ ] Schema & TS types
|
||||||
* [ ] API Operations - Get all collections
|
- [ ] API Operations - Get all collections
|
||||||
* [ ] API Operations - Get all pages
|
- [ ] API Operations - Get all pages
|
||||||
* [ ] API Operations - Get all products
|
- [ ] API Operations - Get all products
|
||||||
* [ ] API Operations - Get page
|
- [ ] API Operations - Get page
|
||||||
* [ ] API Operations - Get product
|
- [ ] API Operations - Get product
|
||||||
* [ ] API Operations - Get Shop Info (categories and vendors working — `vendors` query still a WIP PR on Reaction)
|
- [ ] API Operations - Get Shop Info (categories and vendors working — `vendors` query still a WIP PR on Reaction)
|
||||||
* [ ] Hook - Add Item
|
- [ ] Hook - Add Item
|
||||||
* [ ] Hook - Remove Item
|
- [ ] Hook - Remove Item
|
||||||
* [ ] Hook - Update Item
|
- [ ] Hook - Update Item
|
||||||
* [ ] Hook - Get Cart (account-tied carts working, anonymous carts working, cart reconciliation working)
|
- [ ] Hook - Get Cart (account-tied carts working, anonymous carts working, cart reconciliation working)
|
||||||
* [ ] Auth (based on a WIP PR on Reaction - still need to implement refresh tokens)
|
- [ ] Auth (based on a WIP PR on Reaction - still need to implement refresh tokens)
|
||||||
* [ ] Customer information
|
- [ ] Customer information
|
||||||
* [ ] Product attributes - Size, Colors
|
- [ ] Product attributes - Size, Colors
|
||||||
* [ ] Custom checkout
|
- [ ] Custom checkout
|
||||||
* [ ] Typing (in progress)
|
- [ ] Typing (in progress)
|
||||||
* [ ] Tests
|
- [ ] Tests
|
||||||
|
@ -47,13 +47,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vercel/fetch": "^6.2.0",
|
"@vercel/edge": "^0.0.4",
|
||||||
"deepmerge": "^4.2.2",
|
"deepmerge": "^4.2.2",
|
||||||
"import-cwd": "^3.0.0",
|
"import-cwd": "^3.0.0",
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"swr": "^1.3.0",
|
"swr": "^1.3.0",
|
||||||
"node-fetch": "^2.6.7",
|
"zod": "^3.19.1"
|
||||||
"zod": "^3.19.0"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"next": "^12",
|
"next": "^12",
|
||||||
@ -66,8 +65,8 @@
|
|||||||
"@taskr/watch": "^1.1.0",
|
"@taskr/watch": "^1.1.0",
|
||||||
"@types/js-cookie": "^3.0.1",
|
"@types/js-cookie": "^3.0.1",
|
||||||
"@types/node": "^17.0.8",
|
"@types/node": "^17.0.8",
|
||||||
"@types/react": "^18.0.14",
|
|
||||||
"@types/node-fetch": "2.6.2",
|
"@types/node-fetch": "2.6.2",
|
||||||
|
"@types/react": "^18.0.14",
|
||||||
"lint-staged": "^12.1.7",
|
"lint-staged": "^12.1.7",
|
||||||
"next": "^12.0.8",
|
"next": "^12.0.8",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
|
@ -1,62 +1,64 @@
|
|||||||
import type { CartSchema } from '../../types/cart'
|
|
||||||
import { CommerceAPIError } from '../utils/errors'
|
|
||||||
import isAllowedOperation from '../utils/is-allowed-operation'
|
|
||||||
import type { GetAPISchema } from '..'
|
import type { GetAPISchema } from '..'
|
||||||
|
import type { CartSchema } from '../../types/cart'
|
||||||
|
|
||||||
|
import { parse, getInput } from '../utils'
|
||||||
|
import validateHandlers from '../utils/validate-handlers'
|
||||||
|
|
||||||
|
import {
|
||||||
|
getCartBodySchema,
|
||||||
|
addItemBodySchema,
|
||||||
|
updateItemBodySchema,
|
||||||
|
removeItemBodySchema,
|
||||||
|
cartSchema,
|
||||||
|
} from '../../schemas/cart'
|
||||||
|
|
||||||
const cartEndpoint: GetAPISchema<
|
const cartEndpoint: GetAPISchema<
|
||||||
any,
|
any,
|
||||||
CartSchema
|
CartSchema
|
||||||
>['endpoint']['handler'] = async (ctx) => {
|
>['endpoint']['handler'] = async (ctx) => {
|
||||||
const { req, res, handlers, config } = ctx
|
const { req, handlers, config } = ctx
|
||||||
|
|
||||||
if (
|
validateHandlers(req, {
|
||||||
!isAllowedOperation(req, res, {
|
|
||||||
GET: handlers['getCart'],
|
GET: handlers['getCart'],
|
||||||
POST: handlers['addItem'],
|
POST: handlers['addItem'],
|
||||||
PUT: handlers['updateItem'],
|
PUT: handlers['updateItem'],
|
||||||
DELETE: handlers['removeItem'],
|
DELETE: handlers['removeItem'],
|
||||||
})
|
})
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const input = await getInput(req)
|
||||||
|
|
||||||
|
let output
|
||||||
const { cookies } = req
|
const { cookies } = req
|
||||||
const cartId = cookies[config.cartCookie]
|
const cartId = cookies.get(config.cartCookie)
|
||||||
|
|
||||||
try {
|
|
||||||
// Return current cart info
|
// Return current cart info
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
const body = { cartId }
|
const body = getCartBodySchema.parse({ cartId })
|
||||||
return await handlers['getCart']({ ...ctx, body })
|
output = await handlers['getCart']({ ...ctx, body })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create or add an item to the cart
|
// Create or add an item to the cart
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const body = { ...req.body, cartId }
|
const body = addItemBodySchema.parse({ ...input, cartId })
|
||||||
return await handlers['addItem']({ ...ctx, body })
|
if (!body.item.quantity) {
|
||||||
|
body.item.quantity = 1
|
||||||
|
}
|
||||||
|
output = await handlers['addItem']({ ...ctx, body })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update item in cart
|
// Update item in cart
|
||||||
if (req.method === 'PUT') {
|
if (req.method === 'PUT') {
|
||||||
const body = { ...req.body, cartId }
|
const body = updateItemBodySchema.parse({ ...input, cartId })
|
||||||
return await handlers['updateItem']({ ...ctx, body })
|
output = await handlers['updateItem']({ ...ctx, body })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove an item from the cart
|
// Remove an item from the cart
|
||||||
if (req.method === 'DELETE') {
|
if (req.method === 'DELETE') {
|
||||||
const body = { ...req.body, cartId }
|
const body = removeItemBodySchema.parse({ ...input, cartId })
|
||||||
return await handlers['removeItem']({ ...ctx, body })
|
return await handlers['removeItem']({ ...ctx, body })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
const message =
|
return output ? parse(output, cartSchema.nullish()) : { status: 405 }
|
||||||
error instanceof CommerceAPIError
|
|
||||||
? 'An unexpected error ocurred with the Commerce API'
|
|
||||||
: 'An unexpected error ocurred'
|
|
||||||
|
|
||||||
res.status(500).json({ data: null, errors: [{ message }] })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default cartEndpoint
|
export default cartEndpoint
|
||||||
|
@ -1,31 +1,37 @@
|
|||||||
import type { ProductsSchema } from '../../../types/product'
|
|
||||||
import { CommerceAPIError } from '../../utils/errors'
|
|
||||||
import isAllowedOperation from '../../utils/is-allowed-operation'
|
|
||||||
import type { GetAPISchema } from '../..'
|
import type { GetAPISchema } from '../..'
|
||||||
|
import type { ProductsSchema } from '../../../types/product'
|
||||||
|
|
||||||
|
import validateHandlers from '../../utils/validate-handlers'
|
||||||
|
import {
|
||||||
|
searchProductBodySchema,
|
||||||
|
searchProductsSchema,
|
||||||
|
} from '../../../schemas/product'
|
||||||
|
import { parse } from '../../utils'
|
||||||
|
|
||||||
const productsEndpoint: GetAPISchema<
|
const productsEndpoint: GetAPISchema<
|
||||||
any,
|
any,
|
||||||
ProductsSchema
|
ProductsSchema
|
||||||
>['endpoint']['handler'] = async (ctx) => {
|
>['endpoint']['handler'] = async (ctx) => {
|
||||||
const { req, res, handlers } = ctx
|
const { req, handlers } = ctx
|
||||||
|
|
||||||
if (!isAllowedOperation(req, res, { GET: handlers['getProducts'] })) {
|
validateHandlers(req, { GET: handlers['getProducts'] })
|
||||||
return
|
const { searchParams } = new URL(req.url)
|
||||||
|
|
||||||
|
const body = searchProductBodySchema.parse({
|
||||||
|
search: searchParams.get('search') ?? undefined,
|
||||||
|
categoryId: searchParams.get('categoryId') ?? undefined,
|
||||||
|
brandId: searchParams.get('brandId') ?? undefined,
|
||||||
|
sort: searchParams.get('sort') ?? undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await handlers['getProducts']({ ...ctx, body })
|
||||||
|
|
||||||
|
res.headers = {
|
||||||
|
'Cache-Control': 'max-age=0, s-maxage=3600, stale-while-revalidate, public',
|
||||||
|
...res.headers,
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return parse(res, searchProductsSchema)
|
||||||
const body = req.query
|
|
||||||
return await handlers['getProducts']({ ...ctx, body })
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
const message =
|
|
||||||
error instanceof CommerceAPIError
|
|
||||||
? 'An unexpected error ocurred with the Commerce API'
|
|
||||||
: 'An unexpected error ocurred'
|
|
||||||
|
|
||||||
res.status(500).json({ data: null, errors: [{ message }] })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default productsEndpoint
|
export default productsEndpoint
|
||||||
|
@ -1,49 +1,45 @@
|
|||||||
import type { CheckoutSchema } from '../../types/checkout'
|
|
||||||
import type { GetAPISchema } from '..'
|
import type { GetAPISchema } from '..'
|
||||||
|
import type { CheckoutSchema } from '../../types/checkout'
|
||||||
|
|
||||||
import { CommerceAPIError } from '../utils/errors'
|
import {
|
||||||
import isAllowedOperation from '../utils/is-allowed-operation'
|
checkoutSchema,
|
||||||
|
getCheckoutBodySchema,
|
||||||
|
submitCheckoutBodySchema,
|
||||||
|
} from '../../schemas/checkout'
|
||||||
|
|
||||||
|
import { parse, getInput } from '../utils'
|
||||||
|
import validateHandlers from '../utils/validate-handlers'
|
||||||
|
|
||||||
const checkoutEndpoint: GetAPISchema<
|
const checkoutEndpoint: GetAPISchema<
|
||||||
any,
|
any,
|
||||||
CheckoutSchema
|
CheckoutSchema
|
||||||
>['endpoint']['handler'] = async (ctx) => {
|
>['endpoint']['handler'] = async (ctx) => {
|
||||||
const { req, res, handlers, config } = ctx
|
const { req, handlers, config } = ctx
|
||||||
|
|
||||||
if (
|
validateHandlers(req, {
|
||||||
!isAllowedOperation(req, res, {
|
|
||||||
GET: handlers['getCheckout'],
|
GET: handlers['getCheckout'],
|
||||||
POST: handlers['submitCheckout'],
|
POST: handlers['submitCheckout'],
|
||||||
})
|
})
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { cookies } = req
|
const { cookies } = req
|
||||||
const cartId = cookies[config.cartCookie]
|
const cartId = cookies.get(config.cartCookie)!
|
||||||
|
const input = await getInput(req)
|
||||||
|
|
||||||
try {
|
// Get checkout
|
||||||
// Create checkout
|
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
const body = { ...req.body, cartId }
|
const body = getCheckoutBodySchema.parse({ ...input, cartId })
|
||||||
return await handlers['getCheckout']({ ...ctx, body })
|
const res = await handlers['getCheckout']({ ...ctx, body })
|
||||||
|
return parse(res, checkoutSchema.optional())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create checkout
|
// Create checkout
|
||||||
if (req.method === 'POST' && handlers['submitCheckout']) {
|
if (req.method === 'POST' && handlers['submitCheckout']) {
|
||||||
const body = { ...req.body, cartId }
|
const body = submitCheckoutBodySchema.parse({ ...input, cartId })
|
||||||
return await handlers['submitCheckout']({ ...ctx, body })
|
const res = await handlers['submitCheckout']({ ...ctx, body })
|
||||||
|
return parse(res, checkoutSchema.optional())
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
const message =
|
return { status: 405 }
|
||||||
error instanceof CommerceAPIError
|
|
||||||
? 'An unexpected error ocurred with the Commerce API'
|
|
||||||
: 'An unexpected error ocurred'
|
|
||||||
|
|
||||||
res.status(500).json({ data: null, errors: [{ message }] })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default checkoutEndpoint
|
export default checkoutEndpoint
|
||||||
|
@ -1,65 +1,68 @@
|
|||||||
import type { CustomerAddressSchema } from '../../../types/customer/address'
|
import type { CustomerAddressSchema } from '../../../types/customer/address'
|
||||||
import type { GetAPISchema } from '../..'
|
import type { GetAPISchema } from '../..'
|
||||||
|
|
||||||
import { CommerceAPIError } from '../../utils/errors'
|
import validateHandlers from '../../utils/validate-handlers'
|
||||||
import isAllowedOperation from '../../utils/is-allowed-operation'
|
|
||||||
|
import {
|
||||||
|
addAddressBodySchema,
|
||||||
|
addressSchema,
|
||||||
|
deleteAddressBodySchema,
|
||||||
|
updateAddressBodySchema,
|
||||||
|
} from '../../../schemas/customer'
|
||||||
|
|
||||||
|
import { parse, getInput } from '../../utils'
|
||||||
|
import { getCartBodySchema } from '../../../schemas/cart'
|
||||||
|
|
||||||
|
// create a function that returns a function
|
||||||
|
|
||||||
const customerShippingEndpoint: GetAPISchema<
|
const customerShippingEndpoint: GetAPISchema<
|
||||||
any,
|
any,
|
||||||
CustomerAddressSchema
|
CustomerAddressSchema
|
||||||
>['endpoint']['handler'] = async (ctx) => {
|
>['endpoint']['handler'] = async (ctx) => {
|
||||||
const { req, res, handlers, config } = ctx
|
const { req, handlers, config } = ctx
|
||||||
|
|
||||||
if (
|
validateHandlers(req, {
|
||||||
!isAllowedOperation(req, res, {
|
|
||||||
GET: handlers['getAddresses'],
|
GET: handlers['getAddresses'],
|
||||||
POST: handlers['addItem'],
|
POST: handlers['addItem'],
|
||||||
PUT: handlers['updateItem'],
|
PUT: handlers['updateItem'],
|
||||||
DELETE: handlers['removeItem'],
|
DELETE: handlers['removeItem'],
|
||||||
})
|
})
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let output
|
||||||
|
const input = await getInput(req)
|
||||||
const { cookies } = req
|
const { cookies } = req
|
||||||
|
|
||||||
// Cart id might be usefull for anonymous shopping
|
// Cart id might be usefull for anonymous shopping
|
||||||
const cartId = cookies[config.cartCookie]
|
const cartId = cookies.get(config.cartCookie)
|
||||||
|
|
||||||
try {
|
|
||||||
// Return customer addresses
|
// Return customer addresses
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
const body = { cartId }
|
const body = getCartBodySchema.parse({ cartId })
|
||||||
return await handlers['getAddresses']({ ...ctx, body })
|
return parse(
|
||||||
|
await handlers['getAddresses']({ ...ctx, body }),
|
||||||
|
addressSchema
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create or add an item to customer addresses list
|
// Create or add an item to customer addresses list
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const body = { ...req.body, cartId }
|
const body = addAddressBodySchema.parse({ ...input, cartId })
|
||||||
return await handlers['addItem']({ ...ctx, body })
|
output = await handlers['addItem']({ ...ctx, body })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update item in customer addresses list
|
// Update item in customer addresses list
|
||||||
if (req.method === 'PUT') {
|
if (req.method === 'PUT') {
|
||||||
const body = { ...req.body, cartId }
|
const body = updateAddressBodySchema.parse({ ...input, cartId })
|
||||||
return await handlers['updateItem']({ ...ctx, body })
|
output = await handlers['updateItem']({ ...ctx, body })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove an item from customer addresses list
|
// Remove an item from customer addresses list
|
||||||
if (req.method === 'DELETE') {
|
if (req.method === 'DELETE') {
|
||||||
const body = { ...req.body, cartId }
|
const body = deleteAddressBodySchema.parse({ ...input, cartId })
|
||||||
return await handlers['removeItem']({ ...ctx, body })
|
return await handlers['removeItem']({ ...ctx, body })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
const message =
|
return output ? parse(output, addressSchema) : { status: 405 }
|
||||||
error instanceof CommerceAPIError
|
|
||||||
? 'An unexpected error ocurred with the Commerce API'
|
|
||||||
: 'An unexpected error ocurred'
|
|
||||||
|
|
||||||
res.status(500).json({ data: null, errors: [{ message }] })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default customerShippingEndpoint
|
export default customerShippingEndpoint
|
||||||
|
@ -1,65 +1,67 @@
|
|||||||
import type { CustomerCardSchema } from '../../../types/customer/card'
|
import type { CustomerCardSchema } from '../../../types/customer/card'
|
||||||
import type { GetAPISchema } from '../..'
|
import type { GetAPISchema } from '../..'
|
||||||
|
|
||||||
import { CommerceAPIError } from '../../utils/errors'
|
import { z } from 'zod'
|
||||||
import isAllowedOperation from '../../utils/is-allowed-operation'
|
|
||||||
|
import {
|
||||||
|
cardSchema,
|
||||||
|
addCardBodySchema,
|
||||||
|
deleteCardBodySchema,
|
||||||
|
updateCardBodySchema,
|
||||||
|
} from '../../../schemas/customer'
|
||||||
|
import { parse, getInput } from '../../utils'
|
||||||
|
|
||||||
|
import validateHandlers from '../../utils/validate-handlers'
|
||||||
|
|
||||||
const customerCardEndpoint: GetAPISchema<
|
const customerCardEndpoint: GetAPISchema<
|
||||||
any,
|
any,
|
||||||
CustomerCardSchema
|
CustomerCardSchema
|
||||||
>['endpoint']['handler'] = async (ctx) => {
|
>['endpoint']['handler'] = async (ctx) => {
|
||||||
const { req, res, handlers, config } = ctx
|
const { req, handlers, config } = ctx
|
||||||
|
|
||||||
if (
|
validateHandlers(req, {
|
||||||
!isAllowedOperation(req, res, {
|
|
||||||
GET: handlers['getCards'],
|
GET: handlers['getCards'],
|
||||||
POST: handlers['addItem'],
|
POST: handlers['addItem'],
|
||||||
PUT: handlers['updateItem'],
|
PUT: handlers['updateItem'],
|
||||||
DELETE: handlers['removeItem'],
|
DELETE: handlers['removeItem'],
|
||||||
})
|
})
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let output
|
||||||
|
const input = await getInput(req)
|
||||||
const { cookies } = req
|
const { cookies } = req
|
||||||
|
|
||||||
// Cart id might be usefull for anonymous shopping
|
// Cart id might be usefull for anonymous shopping
|
||||||
const cartId = cookies[config.cartCookie]
|
const cartId = cookies.get(config.cartCookie)
|
||||||
|
|
||||||
try {
|
|
||||||
// Create or add a card
|
// Create or add a card
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
const body = { ...req.body }
|
const body = { ...input }
|
||||||
return await handlers['getCards']({ ...ctx, body })
|
return parse(
|
||||||
|
await handlers['getCards']({ ...ctx, body }),
|
||||||
|
z.array(cardSchema).optional()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create or add an item to customer cards
|
// Create or add an item to customer cards
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const body = { ...req.body, cartId }
|
const body = addCardBodySchema.parse({ ...input, cartId })
|
||||||
return await handlers['addItem']({ ...ctx, body })
|
output = await handlers['addItem']({ ...ctx, body })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update item in customer cards
|
// Update item in customer cards
|
||||||
if (req.method === 'PUT') {
|
if (req.method === 'PUT') {
|
||||||
const body = { ...req.body, cartId }
|
const body = updateCardBodySchema.parse({ ...input, cartId })
|
||||||
return await handlers['updateItem']({ ...ctx, body })
|
output = await handlers['updateItem']({ ...ctx, body })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove an item from customer cards
|
// Remove an item from customer cards
|
||||||
if (req.method === 'DELETE') {
|
if (req.method === 'DELETE') {
|
||||||
const body = { ...req.body, cartId }
|
const body = deleteCardBodySchema.parse({ ...input, cartId })
|
||||||
|
|
||||||
return await handlers['removeItem']({ ...ctx, body })
|
return await handlers['removeItem']({ ...ctx, body })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
const message =
|
return output ? parse(output, cardSchema.nullish()) : { status: 405 }
|
||||||
error instanceof CommerceAPIError
|
|
||||||
? 'An unexpected error ocurred with the Commerce API'
|
|
||||||
: 'An unexpected error ocurred'
|
|
||||||
|
|
||||||
res.status(500).json({ data: null, errors: [{ message }] })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default customerCardEndpoint
|
export default customerCardEndpoint
|
||||||
|
@ -1,36 +1,25 @@
|
|||||||
import type { CustomerSchema } from '../../../types/customer'
|
import type { CustomerSchema } from '../../../types/customer'
|
||||||
import type { GetAPISchema } from '../..'
|
import type { GetAPISchema } from '../..'
|
||||||
|
|
||||||
import { CommerceAPIError } from '../../utils/errors'
|
import { parse } from '../../utils'
|
||||||
import isAllowedOperation from '../../utils/is-allowed-operation'
|
import validateHandlers from '../../utils/validate-handlers'
|
||||||
|
|
||||||
|
import { customerSchema } from '../../../schemas/customer'
|
||||||
|
|
||||||
const customerEndpoint: GetAPISchema<
|
const customerEndpoint: GetAPISchema<
|
||||||
any,
|
any,
|
||||||
CustomerSchema
|
CustomerSchema
|
||||||
>['endpoint']['handler'] = async (ctx) => {
|
>['endpoint']['handler'] = async (ctx) => {
|
||||||
const { req, res, handlers } = ctx
|
const { req, handlers } = ctx
|
||||||
|
|
||||||
if (
|
validateHandlers(req, {
|
||||||
!isAllowedOperation(req, res, {
|
|
||||||
GET: handlers['getLoggedInCustomer'],
|
GET: handlers['getLoggedInCustomer'],
|
||||||
})
|
})
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const body = null
|
const body = null
|
||||||
return await handlers['getLoggedInCustomer']({ ...ctx, body })
|
const output = await handlers['getLoggedInCustomer']({ ...ctx, body })
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
const message =
|
return output ? parse(output, customerSchema) : { status: 204 }
|
||||||
error instanceof CommerceAPIError
|
|
||||||
? 'An unexpected error ocurred with the Commerce API'
|
|
||||||
: 'An unexpected error ocurred'
|
|
||||||
|
|
||||||
res.status(500).json({ data: null, errors: [{ message }] })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default customerEndpoint
|
export default customerEndpoint
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
import edgeHandler from '../utils/edge-handler'
|
||||||
|
import nodeHandler from '../utils/node-handler'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Next.js Commerce API endpoints handler. Based on the path, it will call the corresponding endpoint handler,
|
||||||
|
* exported from the `endpoints` folder of the provider.
|
||||||
|
* @param {CommerceAPI} commerce The Commerce API instance.
|
||||||
|
* @param endpoints An object containing the handlers for each endpoint.
|
||||||
|
*/
|
||||||
|
export default process.env.NEXT_RUNTIME === 'edge' ? edgeHandler : nodeHandler
|
@ -1,36 +1,25 @@
|
|||||||
import type { LoginSchema } from '../../types/login'
|
|
||||||
import { CommerceAPIError } from '../utils/errors'
|
|
||||||
import isAllowedOperation from '../utils/is-allowed-operation'
|
|
||||||
import type { GetAPISchema } from '..'
|
import type { GetAPISchema } from '..'
|
||||||
|
import type { LoginSchema } from '../../types/login'
|
||||||
|
|
||||||
|
import { getInput } from '../utils'
|
||||||
|
import validateHandlers from '../utils/validate-handlers'
|
||||||
|
|
||||||
|
import { loginBodySchema } from '../../schemas/auth'
|
||||||
|
|
||||||
const loginEndpoint: GetAPISchema<
|
const loginEndpoint: GetAPISchema<
|
||||||
any,
|
any,
|
||||||
LoginSchema
|
LoginSchema
|
||||||
>['endpoint']['handler'] = async (ctx) => {
|
>['endpoint']['handler'] = async (ctx) => {
|
||||||
const { req, res, handlers } = ctx
|
const { req, handlers } = ctx
|
||||||
|
|
||||||
if (
|
validateHandlers(req, {
|
||||||
!isAllowedOperation(req, res, {
|
|
||||||
POST: handlers['login'],
|
POST: handlers['login'],
|
||||||
GET: handlers['login'],
|
GET: handlers['login'],
|
||||||
})
|
})
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
const input = await getInput(req)
|
||||||
const body = req.body ?? {}
|
const body = loginBodySchema.parse(input)
|
||||||
return await handlers['login']({ ...ctx, body })
|
return handlers['login']({ ...ctx, body })
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
const message =
|
|
||||||
error instanceof CommerceAPIError
|
|
||||||
? 'An unexpected error ocurred with the Commerce API'
|
|
||||||
: 'An unexpected error ocurred'
|
|
||||||
|
|
||||||
res.status(500).json({ data: null, errors: [{ message }] })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default loginEndpoint
|
export default loginEndpoint
|
||||||
|
@ -1,35 +1,26 @@
|
|||||||
import type { LogoutSchema } from '../../types/logout'
|
|
||||||
import { CommerceAPIError } from '../utils/errors'
|
|
||||||
import isAllowedOperation from '../utils/is-allowed-operation'
|
|
||||||
import type { GetAPISchema } from '..'
|
import type { GetAPISchema } from '..'
|
||||||
|
import type { LogoutSchema } from '../../types/logout'
|
||||||
|
|
||||||
const logoutEndpoint: GetAPISchema<any, LogoutSchema>['endpoint']['handler'] =
|
import { logoutBodySchema } from '../../schemas/auth'
|
||||||
async (ctx) => {
|
import validateHandlers from '../utils/validate-handlers'
|
||||||
const { req, res, handlers } = ctx
|
|
||||||
|
|
||||||
if (
|
const logoutEndpoint: GetAPISchema<
|
||||||
!isAllowedOperation(req, res, {
|
any,
|
||||||
|
LogoutSchema
|
||||||
|
>['endpoint']['handler'] = async (ctx) => {
|
||||||
|
const { req, handlers } = ctx
|
||||||
|
|
||||||
|
validateHandlers(req, {
|
||||||
GET: handlers['logout'],
|
GET: handlers['logout'],
|
||||||
})
|
})
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
const redirectTo = new URL(req.url).searchParams.get('redirectTo')
|
||||||
const redirectTo = req.query.redirect_to
|
|
||||||
const body = typeof redirectTo === 'string' ? { redirectTo } : {}
|
|
||||||
|
|
||||||
return await handlers['logout']({ ...ctx, body })
|
const body = logoutBodySchema.parse(
|
||||||
} catch (error) {
|
typeof redirectTo === 'string' ? { redirectTo } : {}
|
||||||
console.error(error)
|
)
|
||||||
|
|
||||||
const message =
|
return handlers['logout']({ ...ctx, body })
|
||||||
error instanceof CommerceAPIError
|
|
||||||
? 'An unexpected error ocurred with the Commerce API'
|
|
||||||
: 'An unexpected error ocurred'
|
|
||||||
|
|
||||||
res.status(500).json({ data: null, errors: [{ message }] })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default logoutEndpoint
|
export default logoutEndpoint
|
||||||
|
@ -1,36 +1,27 @@
|
|||||||
import type { SignupSchema } from '../../types/signup'
|
|
||||||
import { CommerceAPIError } from '../utils/errors'
|
|
||||||
import isAllowedOperation from '../utils/is-allowed-operation'
|
|
||||||
import type { GetAPISchema } from '..'
|
import type { GetAPISchema } from '..'
|
||||||
|
import type { SignupSchema } from '../../types/signup'
|
||||||
|
|
||||||
const signupEndpoint: GetAPISchema<any, SignupSchema>['endpoint']['handler'] =
|
import { getInput } from '../utils'
|
||||||
async (ctx) => {
|
import validateHandlers from '../utils/validate-handlers'
|
||||||
const { req, res, handlers, config } = ctx
|
|
||||||
|
|
||||||
if (
|
import { signupBodySchema } from '../../schemas/auth'
|
||||||
!isAllowedOperation(req, res, {
|
|
||||||
|
const signupEndpoint: GetAPISchema<
|
||||||
|
any,
|
||||||
|
SignupSchema
|
||||||
|
>['endpoint']['handler'] = async (ctx) => {
|
||||||
|
const { req, handlers, config } = ctx
|
||||||
|
|
||||||
|
validateHandlers(req, {
|
||||||
POST: handlers['signup'],
|
POST: handlers['signup'],
|
||||||
})
|
})
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const input = await getInput(req)
|
||||||
const { cookies } = req
|
const { cookies } = req
|
||||||
const cartId = cookies[config.cartCookie]
|
const cartId = cookies.get(config.cartCookie)
|
||||||
|
|
||||||
try {
|
const body = signupBodySchema.parse({ ...input, cartId })
|
||||||
const body = { ...req.body, cartId }
|
return handlers['signup']({ ...ctx, body })
|
||||||
return await handlers['signup']({ ...ctx, body })
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
const message =
|
|
||||||
error instanceof CommerceAPIError
|
|
||||||
? 'An unexpected error ocurred with the Commerce API'
|
|
||||||
: 'An unexpected error ocurred'
|
|
||||||
|
|
||||||
res.status(500).json({ data: null, errors: [{ message }] })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default signupEndpoint
|
export default signupEndpoint
|
||||||
|
@ -1,58 +1,58 @@
|
|||||||
import type { WishlistSchema } from '../../types/wishlist'
|
|
||||||
import { CommerceAPIError } from '../utils/errors'
|
|
||||||
import isAllowedOperation from '../utils/is-allowed-operation'
|
|
||||||
import type { GetAPISchema } from '..'
|
import type { GetAPISchema } from '..'
|
||||||
|
import type { WishlistSchema } from '../../types/wishlist'
|
||||||
|
|
||||||
|
import { parse, getInput } from '../utils'
|
||||||
|
|
||||||
|
import {
|
||||||
|
wishlistSchema,
|
||||||
|
addItemBodySchema,
|
||||||
|
removeItemBodySchema,
|
||||||
|
getWishlistBodySchema,
|
||||||
|
} from '../../schemas/whishlist'
|
||||||
|
|
||||||
|
import validateHandlers from '../utils/validate-handlers'
|
||||||
|
|
||||||
const wishlistEndpoint: GetAPISchema<
|
const wishlistEndpoint: GetAPISchema<
|
||||||
any,
|
any,
|
||||||
WishlistSchema
|
WishlistSchema
|
||||||
>['endpoint']['handler'] = async (ctx) => {
|
>['endpoint']['handler'] = async (ctx) => {
|
||||||
const { req, res, handlers, config } = ctx
|
const { req, handlers, config } = ctx
|
||||||
|
|
||||||
if (
|
validateHandlers(req, {
|
||||||
!isAllowedOperation(req, res, {
|
|
||||||
GET: handlers['getWishlist'],
|
GET: handlers['getWishlist'],
|
||||||
POST: handlers['addItem'],
|
POST: handlers['addItem'],
|
||||||
DELETE: handlers['removeItem'],
|
DELETE: handlers['removeItem'],
|
||||||
})
|
})
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let output
|
||||||
const { cookies } = req
|
const { cookies } = req
|
||||||
const customerToken = cookies[config.customerCookie]
|
const input = await getInput(req)
|
||||||
|
|
||||||
|
const customerToken = cookies.get(config.customerCookie)
|
||||||
|
const products = new URL(req.url).searchParams.get('products')
|
||||||
|
|
||||||
try {
|
|
||||||
// Return current wishlist info
|
// Return current wishlist info
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
const body = {
|
const body = getWishlistBodySchema.parse({
|
||||||
customerToken,
|
customerToken,
|
||||||
includeProducts: req.query.products === '1',
|
includeProducts: !!products,
|
||||||
}
|
})
|
||||||
return await handlers['getWishlist']({ ...ctx, body })
|
output = await handlers['getWishlist']({ ...ctx, body })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add an item to the wishlist
|
// Add an item to the wishlist
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const body = { ...req.body, customerToken }
|
const body = addItemBodySchema.parse({ ...input, customerToken })
|
||||||
return await handlers['addItem']({ ...ctx, body })
|
output = await handlers['addItem']({ ...ctx, body })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove an item from the wishlist
|
// Remove an item from the wishlist
|
||||||
if (req.method === 'DELETE') {
|
if (req.method === 'DELETE') {
|
||||||
const body = { ...req.body, customerToken }
|
const body = removeItemBodySchema.parse({ ...input, customerToken })
|
||||||
return await handlers['removeItem']({ ...ctx, body })
|
output = await handlers['removeItem']({ ...ctx, body })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
const message =
|
return output ? parse(output, wishlistSchema.optional()) : { status: 405 }
|
||||||
error instanceof CommerceAPIError
|
|
||||||
? 'An unexpected error ocurred with the Commerce API'
|
|
||||||
: 'An unexpected error ocurred'
|
|
||||||
|
|
||||||
res.status(500).json({ data: null, errors: [{ message }] })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default wishlistEndpoint
|
export default wishlistEndpoint
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import type { NextApiHandler } from 'next'
|
import type { NextRequest } from 'next/server'
|
||||||
import type { FetchOptions, Response } from '@vercel/fetch'
|
import type { APIEndpoint, APIHandler, APIResponse } from './utils/types'
|
||||||
import type { APIEndpoint, APIHandler } from './utils/types'
|
|
||||||
import type { CartSchema } from '../types/cart'
|
import type { CartSchema } from '../types/cart'
|
||||||
import type { CustomerSchema } from '../types/customer'
|
import type { CustomerSchema } from '../types/customer'
|
||||||
import type { LoginSchema } from '../types/login'
|
import type { LoginSchema } from '../types/login'
|
||||||
@ -119,6 +118,8 @@ export function getCommerceApi<P extends APIProvider>(
|
|||||||
return commerce
|
return commerce
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EndpointHandler = (req: NextRequest) => Promise<APIResponse>
|
||||||
|
|
||||||
export function getEndpoint<
|
export function getEndpoint<
|
||||||
P extends APIProvider,
|
P extends APIProvider,
|
||||||
T extends GetAPISchema<any, any>
|
T extends GetAPISchema<any, any>
|
||||||
@ -128,13 +129,11 @@ export function getEndpoint<
|
|||||||
config?: P['config']
|
config?: P['config']
|
||||||
options?: T['schema']['endpoint']['options']
|
options?: T['schema']['endpoint']['options']
|
||||||
}
|
}
|
||||||
): NextApiHandler {
|
): EndpointHandler {
|
||||||
const cfg = commerce.getConfig(context.config)
|
const cfg = commerce.getConfig(context.config)
|
||||||
|
return function apiHandler(req) {
|
||||||
return function apiHandler(req, res) {
|
|
||||||
return context.handler({
|
return context.handler({
|
||||||
req,
|
req,
|
||||||
res,
|
|
||||||
commerce,
|
commerce,
|
||||||
config: cfg,
|
config: cfg,
|
||||||
handlers: context.handlers,
|
handlers: context.handlers,
|
||||||
@ -151,7 +150,7 @@ export const createEndpoint =
|
|||||||
config?: P['config']
|
config?: P['config']
|
||||||
options?: API['schema']['endpoint']['options']
|
options?: API['schema']['endpoint']['options']
|
||||||
}
|
}
|
||||||
): NextApiHandler => {
|
): EndpointHandler => {
|
||||||
return getEndpoint(commerce, { ...endpoint, ...context })
|
return getEndpoint(commerce, { ...endpoint, ...context })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +165,7 @@ export interface CommerceAPIConfig {
|
|||||||
fetch<Data = any, Variables = any>(
|
fetch<Data = any, Variables = any>(
|
||||||
query: string,
|
query: string,
|
||||||
queryData?: CommerceAPIFetchOptions<Variables>,
|
queryData?: CommerceAPIFetchOptions<Variables>,
|
||||||
fetchOptions?: FetchOptions
|
headers?: HeadersInit
|
||||||
): Promise<GraphQLFetcherResult<Data>>
|
): Promise<GraphQLFetcherResult<Data>>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,8 +174,7 @@ export type GraphQLFetcher<
|
|||||||
Variables = any
|
Variables = any
|
||||||
> = (
|
> = (
|
||||||
query: string,
|
query: string,
|
||||||
queryData?: CommerceAPIFetchOptions<Variables>,
|
queryData?: CommerceAPIFetchOptions<Variables>
|
||||||
fetchOptions?: FetchOptions
|
|
||||||
) => Promise<Data>
|
) => Promise<Data>
|
||||||
|
|
||||||
export interface GraphQLFetcherResult<Data = any> {
|
export interface GraphQLFetcherResult<Data = any> {
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import type { ServerResponse } from 'http'
|
|
||||||
import type { LoginOperation } from '../types/login'
|
import type { LoginOperation } from '../types/login'
|
||||||
import type { GetAllPagesOperation, GetPageOperation } from '../types/page'
|
import type { GetAllPagesOperation, GetPageOperation } from '../types/page'
|
||||||
import type { GetSiteInfoOperation } from '../types/site'
|
import type { GetSiteInfoOperation } from '../types/site'
|
||||||
@ -44,14 +43,14 @@ export type Operations<P extends APIProvider> = {
|
|||||||
<T extends LoginOperation>(opts: {
|
<T extends LoginOperation>(opts: {
|
||||||
variables: T['variables']
|
variables: T['variables']
|
||||||
config?: P['config']
|
config?: P['config']
|
||||||
res: ServerResponse
|
res: Response
|
||||||
}): Promise<T['data']>
|
}): Promise<T['data']>
|
||||||
|
|
||||||
<T extends LoginOperation>(
|
<T extends LoginOperation>(
|
||||||
opts: {
|
opts: {
|
||||||
variables: T['variables']
|
variables: T['variables']
|
||||||
config?: P['config']
|
config?: P['config']
|
||||||
res: ServerResponse
|
res: Response
|
||||||
} & OperationOptions
|
} & OperationOptions
|
||||||
): Promise<T['data']>
|
): Promise<T['data']>
|
||||||
}
|
}
|
||||||
|
81
packages/commerce/src/api/utils/edge-handler.ts
Normal file
81
packages/commerce/src/api/utils/edge-handler.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import type { APIProvider, CommerceAPI, EndpointHandler } from '..'
|
||||||
|
import type { NextRequest } from 'next/server'
|
||||||
|
|
||||||
|
import { normalizeApiError } from './errors'
|
||||||
|
import { transformHeaders } from '.'
|
||||||
|
|
||||||
|
export default function edgeHandler<P extends APIProvider>(
|
||||||
|
commerce: CommerceAPI<P>,
|
||||||
|
endpoints: Record<string, (commerce: CommerceAPI<P>) => EndpointHandler>
|
||||||
|
) {
|
||||||
|
const endpointsKeys = Object.keys(endpoints)
|
||||||
|
|
||||||
|
const handlers = endpointsKeys.reduce<Record<string, EndpointHandler>>(
|
||||||
|
(acc, endpoint) =>
|
||||||
|
Object.assign(acc, {
|
||||||
|
[endpoint]: endpoints[endpoint](commerce),
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
return async (req: NextRequest) => {
|
||||||
|
try {
|
||||||
|
const { pathname } = new URL(req.url)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current endpoint by removing the leading and trailing slash & base path.
|
||||||
|
* Csovers: /api/commerce/cart & /checkout
|
||||||
|
*/
|
||||||
|
const endpoint = pathname
|
||||||
|
.replace('/api/commerce/', '')
|
||||||
|
.replace(/^\/|\/$/g, '')
|
||||||
|
|
||||||
|
// Check if the handler for this path exists and return a 404 if it doesn't
|
||||||
|
if (!endpointsKeys.includes(endpoint)) {
|
||||||
|
throw new Error(
|
||||||
|
`Endpoint "${endpoint}" not implemented. Please use one of the available api endpoints: ${endpointsKeys.join(
|
||||||
|
', '
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the handler for this endpoint, provided by the provider,
|
||||||
|
* parses the input body and returns the parsed output
|
||||||
|
*/
|
||||||
|
const output = await handlers[endpoint](req)
|
||||||
|
|
||||||
|
// If the output is a Response, return it directly (E.g. checkout page & validateMethod util)
|
||||||
|
if (output instanceof Response) {
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = transformHeaders(output.headers)
|
||||||
|
|
||||||
|
// If the output contains a redirectTo property, return a Response with the redirect
|
||||||
|
if (output.redirectTo) {
|
||||||
|
headers.append('Location', output.redirectTo)
|
||||||
|
return new Response(null, {
|
||||||
|
status: 302,
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, return a JSON response with the output data or errors returned by the handler
|
||||||
|
const { data = null, errors, status } = output
|
||||||
|
return new Response(JSON.stringify({ data, errors }), {
|
||||||
|
status,
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
const output = normalizeApiError(error)
|
||||||
|
if (output instanceof Response) {
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
const { status = 500, ...rest } = output
|
||||||
|
return output instanceof Response
|
||||||
|
? output
|
||||||
|
: new Response(JSON.stringify(rest), { status })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
|
import type { NextRequest } from 'next/server'
|
||||||
|
|
||||||
|
import { CommerceError } from '../../utils/errors'
|
||||||
import { ZodError } from 'zod'
|
import { ZodError } from 'zod'
|
||||||
|
|
||||||
import type { Response } from '@vercel/fetch'
|
export class CommerceAPIResponseError extends Error {
|
||||||
import { CommerceError } from '../../utils/errors'
|
|
||||||
|
|
||||||
export class CommerceAPIError extends Error {
|
|
||||||
status: number
|
status: number
|
||||||
res: Response
|
res: Response
|
||||||
data: any
|
data: any
|
||||||
@ -17,6 +17,23 @@ export class CommerceAPIError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CommerceAPIError extends Error {
|
||||||
|
status: number
|
||||||
|
code: string
|
||||||
|
constructor(
|
||||||
|
msg: string,
|
||||||
|
options?: {
|
||||||
|
status?: number
|
||||||
|
code?: string
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
super(msg)
|
||||||
|
this.name = 'CommerceApiError'
|
||||||
|
this.status = options?.status || 500
|
||||||
|
this.code = options?.code || 'api_error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class CommerceNetworkError extends Error {
|
export class CommerceNetworkError extends Error {
|
||||||
constructor(msg: string) {
|
constructor(msg: string) {
|
||||||
super(msg)
|
super(msg)
|
||||||
@ -24,24 +41,59 @@ export class CommerceNetworkError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const normalizeZodIssues = (issues: ZodError['issues']) =>
|
||||||
|
issues.map(({ path, message }) => `${message} at "${path.join('.')}" field`)
|
||||||
|
|
||||||
export const getOperationError = (operation: string, error: unknown) => {
|
export const getOperationError = (operation: string, error: unknown) => {
|
||||||
if (error instanceof ZodError) {
|
if (error instanceof ZodError) {
|
||||||
return new CommerceError({
|
return new CommerceError({
|
||||||
code: 'SCHEMA_VALIDATION_ERROR',
|
code: 'SCHEMA_VALIDATION_ERROR',
|
||||||
message:
|
message:
|
||||||
`The ${operation} operation returned invalid data and has ${
|
`Validation ${
|
||||||
error.issues.length
|
error.issues.length === 1 ? 'error' : 'errors'
|
||||||
} parse ${error.issues.length === 1 ? 'error' : 'errors'}: \n` +
|
} at "${operation}" operation: \n` +
|
||||||
error.issues
|
normalizeZodIssues(error.issues).join('\n'),
|
||||||
.map(
|
|
||||||
(e, index) =>
|
|
||||||
`Error #${index + 1} ${
|
|
||||||
e.path.length > 0 ? `Path: ${e.path.join('.')}, ` : ''
|
|
||||||
}Code: ${e.code}, Message: ${e.message}`
|
|
||||||
)
|
|
||||||
.join('\n'),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const normalizeApiError = (error: unknown, req?: NextRequest) => {
|
||||||
|
if (error instanceof CommerceAPIResponseError && error.res) {
|
||||||
|
return error.res
|
||||||
|
}
|
||||||
|
|
||||||
|
req?.url && console.log(req.url)
|
||||||
|
|
||||||
|
if (error instanceof ZodError) {
|
||||||
|
const message = 'Validation error, please check the input data!'
|
||||||
|
const errors = normalizeZodIssues(error.issues).map((message) => ({
|
||||||
|
message,
|
||||||
|
}))
|
||||||
|
console.error(`${message}\n${errors.map((e) => e.message).join('\n')}`)
|
||||||
|
return {
|
||||||
|
status: 400,
|
||||||
|
data: null,
|
||||||
|
errors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
if (error instanceof CommerceAPIError) {
|
||||||
|
return {
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: error.message,
|
||||||
|
code: error.code,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
status: error.status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'An unexpected error ocurred' }],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
88
packages/commerce/src/api/utils/index.ts
Normal file
88
packages/commerce/src/api/utils/index.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
import type { ZodSchema } from 'zod'
|
||||||
|
import type { APIResponse } from './types'
|
||||||
|
|
||||||
|
import { NextRequest } from 'next/server'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the output data of the API handler and returns a valid APIResponse
|
||||||
|
* or throws an error if the data is invalid.
|
||||||
|
* @param res APIResponse
|
||||||
|
* @param parser ZodSchema
|
||||||
|
*/
|
||||||
|
export const parse = <T>(res: APIResponse<T>, parser: ZodSchema) => {
|
||||||
|
if (res.data) {
|
||||||
|
res.data = parser.parse(res.data)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the body of the request as a JSON object.
|
||||||
|
* @param req NextRequest
|
||||||
|
*/
|
||||||
|
export const getInput = (req: NextRequest) => req.json().catch(() => ({}))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert NextApiRequest to NextRequest
|
||||||
|
* @param req NextApiRequest
|
||||||
|
* @param path string
|
||||||
|
*/
|
||||||
|
export const transformRequest = (req: NextApiRequest, path: string) => {
|
||||||
|
const headers = new Headers()
|
||||||
|
let body
|
||||||
|
|
||||||
|
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
||||||
|
headers.append(req.rawHeaders[i], req.rawHeaders[i + 1])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
req.method === 'POST' ||
|
||||||
|
req.method === 'PUT' ||
|
||||||
|
req.method === 'DELETE'
|
||||||
|
) {
|
||||||
|
body = JSON.stringify(req.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NextRequest(`https://${req.headers.host}/api/commerce/${path}`, {
|
||||||
|
headers,
|
||||||
|
method: req.method,
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the custom headers received in the APIResponse in the
|
||||||
|
* @param headers Record<string, string|string[]> | Headers | undefined
|
||||||
|
* @returns Headers
|
||||||
|
*/
|
||||||
|
export const transformHeaders = (
|
||||||
|
headers: Record<string, string | number | string[]> | Headers = {}
|
||||||
|
) => {
|
||||||
|
if (headers instanceof Headers) {
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
const newHeaders = new Headers()
|
||||||
|
|
||||||
|
Object.entries(headers).forEach(([key, value]) => {
|
||||||
|
newHeaders.append(key, value as string)
|
||||||
|
})
|
||||||
|
|
||||||
|
return newHeaders
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setHeaders = (
|
||||||
|
res: NextApiResponse,
|
||||||
|
headers: Record<string, string | number | string[]> | Headers = {}
|
||||||
|
) => {
|
||||||
|
if (headers instanceof Headers) {
|
||||||
|
headers.forEach((value, key) => {
|
||||||
|
res.setHeader(key, value)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Object.entries(headers).forEach(([key, value]) => {
|
||||||
|
res.setHeader(key, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,30 +0,0 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
|
|
||||||
export type HTTP_METHODS = 'OPTIONS' | 'GET' | 'POST' | 'PUT' | 'DELETE'
|
|
||||||
|
|
||||||
export default function isAllowedMethod(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse,
|
|
||||||
allowedMethods: HTTP_METHODS[]
|
|
||||||
) {
|
|
||||||
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,19 +0,0 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
import isAllowedMethod, { HTTP_METHODS } from './is-allowed-method'
|
|
||||||
import { APIHandler } from './types'
|
|
||||||
|
|
||||||
export default function isAllowedOperation(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse,
|
|
||||||
allowedOperations: { [k in HTTP_METHODS]?: APIHandler<any, any> }
|
|
||||||
) {
|
|
||||||
const methods = Object.keys(allowedOperations) as HTTP_METHODS[]
|
|
||||||
const allowedMethods = methods.reduce<HTTP_METHODS[]>((arr, method) => {
|
|
||||||
if (allowedOperations[method]) {
|
|
||||||
arr.push(method)
|
|
||||||
}
|
|
||||||
return arr
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return isAllowedMethod(req, res, allowedMethods)
|
|
||||||
}
|
|
75
packages/commerce/src/api/utils/node-handler.ts
Normal file
75
packages/commerce/src/api/utils/node-handler.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
import type { APIProvider, CommerceAPI, EndpointHandler } from '..'
|
||||||
|
|
||||||
|
import { normalizeApiError } from './errors'
|
||||||
|
import { transformRequest, setHeaders } from '.'
|
||||||
|
|
||||||
|
export default function nodeHandler<P extends APIProvider>(
|
||||||
|
commerce: CommerceAPI<P>,
|
||||||
|
endpoints: {
|
||||||
|
[key: string]: (commerce: CommerceAPI<P>) => EndpointHandler
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const paths = Object.keys(endpoints)
|
||||||
|
|
||||||
|
const handlers = paths.reduce<Record<string, EndpointHandler>>(
|
||||||
|
(acc, path) =>
|
||||||
|
Object.assign(acc, {
|
||||||
|
[path]: endpoints[path](commerce),
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
return async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
try {
|
||||||
|
if (!req.query.commerce) {
|
||||||
|
throw new Error(
|
||||||
|
'Invalid configuration. Please make sure that the /pages/api/commerce/[[...commerce]].ts route is configured correctly, and it passes the commerce instance.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the url path
|
||||||
|
*/
|
||||||
|
const path = Array.isArray(req.query.commerce)
|
||||||
|
? req.query.commerce.join('/')
|
||||||
|
: req.query.commerce
|
||||||
|
|
||||||
|
// Check if the handler for this path exists and return a 404 if it doesn't
|
||||||
|
if (!paths.includes(path)) {
|
||||||
|
throw new Error(
|
||||||
|
`Endpoint handler not implemented. Please use one of the available api endpoints: ${paths.join(
|
||||||
|
', '
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = await handlers[path](transformRequest(req, path))
|
||||||
|
const { status, errors, data, redirectTo, headers } = output
|
||||||
|
|
||||||
|
setHeaders(res, headers)
|
||||||
|
|
||||||
|
if (output instanceof Response) {
|
||||||
|
return res.end(output.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redirectTo) {
|
||||||
|
return res.redirect(redirectTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(status || 200).json({
|
||||||
|
data,
|
||||||
|
errors,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
const output = normalizeApiError(error)
|
||||||
|
|
||||||
|
if (output instanceof Response) {
|
||||||
|
return res.end(output.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { status = 500, ...rest } = output
|
||||||
|
res.status(status).json(rest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,19 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
import type { NextRequest } from 'next/server'
|
||||||
import type { CommerceAPI } from '..'
|
import type { CommerceAPI } from '..'
|
||||||
|
|
||||||
export type ErrorData = { message: string; code?: string }
|
export type ErrorData = { message: string; code?: string }
|
||||||
|
|
||||||
export type APIResponse<Data = any> =
|
export type APIResponse<Data = any> = {
|
||||||
| { data: Data; errors?: ErrorData[] }
|
data?: Data
|
||||||
// If `data` doesn't include `null`, then `null` is only allowed on errors
|
errors?: ErrorData[]
|
||||||
| (Data extends null
|
status?: number
|
||||||
? { data: null; errors?: ErrorData[] }
|
headers?: Record<string, number | string | string[]> | Headers
|
||||||
: { data: null; errors: ErrorData[] })
|
/**
|
||||||
|
* @type {string}
|
||||||
|
* @example redirectTo: '/cart'
|
||||||
|
*/
|
||||||
|
redirectTo?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type APIHandlerContext<
|
export type APIHandlerContext<
|
||||||
C extends CommerceAPI,
|
C extends CommerceAPI,
|
||||||
@ -16,14 +21,10 @@ export type APIHandlerContext<
|
|||||||
Data = any,
|
Data = any,
|
||||||
Options extends {} = {}
|
Options extends {} = {}
|
||||||
> = {
|
> = {
|
||||||
req: NextApiRequest
|
req: NextRequest
|
||||||
res: NextApiResponse<APIResponse<Data>>
|
|
||||||
commerce: C
|
commerce: C
|
||||||
config: C['provider']['config']
|
config: C['provider']['config']
|
||||||
handlers: H
|
handlers: H
|
||||||
/**
|
|
||||||
* Custom configs that may be used by a particular handler
|
|
||||||
*/
|
|
||||||
options: Options
|
options: Options
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,7 +36,7 @@ export type APIHandler<
|
|||||||
Options extends {} = {}
|
Options extends {} = {}
|
||||||
> = (
|
> = (
|
||||||
context: APIHandlerContext<C, H, Data, Options> & { body: Body }
|
context: APIHandlerContext<C, H, Data, Options> & { body: Body }
|
||||||
) => void | Promise<void>
|
) => Promise<APIResponse<Data>>
|
||||||
|
|
||||||
export type APIHandlers<C extends CommerceAPI> = {
|
export type APIHandlers<C extends CommerceAPI> = {
|
||||||
[k: string]: APIHandler<C, any, any, any, any>
|
[k: string]: APIHandler<C, any, any, any, any>
|
||||||
@ -46,4 +47,6 @@ export type APIEndpoint<
|
|||||||
H extends APIHandlers<C> = {},
|
H extends APIHandlers<C> = {},
|
||||||
Data = any,
|
Data = any,
|
||||||
Options extends {} = {}
|
Options extends {} = {}
|
||||||
> = (context: APIHandlerContext<C, H, Data, Options>) => void | Promise<void>
|
> = (
|
||||||
|
context: APIHandlerContext<C, H, Data, Options>
|
||||||
|
) => Promise<APIResponse<Data>>
|
||||||
|
24
packages/commerce/src/api/utils/validate-handlers.ts
Normal file
24
packages/commerce/src/api/utils/validate-handlers.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type { NextRequest } from 'next/server'
|
||||||
|
import type { APIHandler } from './types'
|
||||||
|
import validateMethod, { HTTP_METHODS } from './validate-method'
|
||||||
|
/**
|
||||||
|
* Validates the request method and throws an error if it's not allowed, or if the handler is not implemented.
|
||||||
|
* and stops the execution of the handler.
|
||||||
|
* @param req The request object.
|
||||||
|
* @param allowedOperations An object containing the handlers for each method.
|
||||||
|
* @throws Error when the method is not allowed or the handler is not implemented.
|
||||||
|
*/
|
||||||
|
export default function validateHandlers(
|
||||||
|
req: NextRequest,
|
||||||
|
allowedOperations: { [k in HTTP_METHODS]?: APIHandler<any, any> }
|
||||||
|
) {
|
||||||
|
const methods = Object.keys(allowedOperations) as HTTP_METHODS[]
|
||||||
|
const allowedMethods = methods.reduce<HTTP_METHODS[]>((arr, method) => {
|
||||||
|
if (allowedOperations[method]) {
|
||||||
|
arr.push(method)
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return validateMethod(req, allowedMethods)
|
||||||
|
}
|
48
packages/commerce/src/api/utils/validate-method.ts
Normal file
48
packages/commerce/src/api/utils/validate-method.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import type { NextRequest } from 'next/server'
|
||||||
|
import { CommerceAPIResponseError } from './errors'
|
||||||
|
|
||||||
|
export type HTTP_METHODS = 'OPTIONS' | 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||||
|
|
||||||
|
export default function validateMethod(
|
||||||
|
req: NextRequest,
|
||||||
|
allowedMethods: HTTP_METHODS[]
|
||||||
|
) {
|
||||||
|
const methods = allowedMethods.includes('OPTIONS')
|
||||||
|
? allowedMethods
|
||||||
|
: [...allowedMethods, 'OPTIONS']
|
||||||
|
|
||||||
|
if (!req.method || !methods.includes(req.method)) {
|
||||||
|
throw new CommerceAPIResponseError(
|
||||||
|
`The HTTP ${req.method} method is not supported at this route.`,
|
||||||
|
new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
code: 'invalid_method',
|
||||||
|
message: `The HTTP ${req.method} method is not supported at this route.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 405,
|
||||||
|
headers: {
|
||||||
|
Allow: methods.join(', '),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
throw new CommerceAPIResponseError(
|
||||||
|
'This is a CORS preflight request.',
|
||||||
|
new Response(null, {
|
||||||
|
status: 204,
|
||||||
|
headers: {
|
||||||
|
Allow: methods.join(', '),
|
||||||
|
'Content-Length': '0',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
18
packages/commerce/src/schemas/auth.ts
Normal file
18
packages/commerce/src/schemas/auth.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const loginBodySchema = z.object({
|
||||||
|
redirectTo: z.string().optional(),
|
||||||
|
email: z.string().email(),
|
||||||
|
password: z.string().min(7),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const logoutBodySchema = z.object({
|
||||||
|
redirectTo: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const signupBodySchema = z.object({
|
||||||
|
firstName: z.string(),
|
||||||
|
lastName: z.string(),
|
||||||
|
email: z.string().email(),
|
||||||
|
password: z.string().min(7),
|
||||||
|
})
|
102
packages/commerce/src/schemas/cart.ts
Normal file
102
packages/commerce/src/schemas/cart.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const getCartBodySchema = z.object({
|
||||||
|
cartId: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const cartItemBodySchema = z.object({
|
||||||
|
variantId: z.string(),
|
||||||
|
productId: z.string().optional(),
|
||||||
|
quantity: z.number().min(1).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const addItemBodySchema = z.object({
|
||||||
|
cartId: z.string().optional(),
|
||||||
|
item: cartItemBodySchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const updateItemBodySchema = z.object({
|
||||||
|
cartId: z.string(),
|
||||||
|
itemId: z.string(),
|
||||||
|
item: cartItemBodySchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const removeItemBodySchema = z.object({
|
||||||
|
cartId: z.string(),
|
||||||
|
itemId: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const discountSchema = z.object({
|
||||||
|
value: z.number(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const optionsSchema = z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
name: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const cartProductVariantSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
sku: z.string().optional(),
|
||||||
|
name: z.string(),
|
||||||
|
price: z.number(),
|
||||||
|
listPrice: z.number(),
|
||||||
|
availableForSale: z.boolean().optional(),
|
||||||
|
requiresShipping: z.boolean().optional(),
|
||||||
|
image: z.object({
|
||||||
|
url: z.string(),
|
||||||
|
altText: z.string().optional(),
|
||||||
|
}),
|
||||||
|
weight: z
|
||||||
|
.object({
|
||||||
|
value: z.number(),
|
||||||
|
unit: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
height: z
|
||||||
|
.object({
|
||||||
|
value: z.number(),
|
||||||
|
unit: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
width: z
|
||||||
|
.object({
|
||||||
|
value: z.number(),
|
||||||
|
unit: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
depth: z
|
||||||
|
.object({
|
||||||
|
value: z.number(),
|
||||||
|
unit: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const cartLineItemSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
variantId: z.string(),
|
||||||
|
productId: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
quantity: z.number().min(1),
|
||||||
|
discounts: z.array(discountSchema).optional(),
|
||||||
|
path: z.string().startsWith('/').optional(),
|
||||||
|
variant: cartProductVariantSchema,
|
||||||
|
options: z.array(optionsSchema).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const cartSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
customerId: z.string().optional(),
|
||||||
|
url: z.string().optional(),
|
||||||
|
email: z.string().optional(),
|
||||||
|
createdAt: z.string(),
|
||||||
|
currency: z.object({ code: z.string() }),
|
||||||
|
taxesIncluded: z.boolean(),
|
||||||
|
lineItems: z.array(cartLineItemSchema),
|
||||||
|
lineItemsSubtotalPrice: z.number(),
|
||||||
|
subtotalPrice: z.number(),
|
||||||
|
totalPrice: z.number(),
|
||||||
|
discounts: z.array(discountSchema).optional(),
|
||||||
|
})
|
25
packages/commerce/src/schemas/checkout.ts
Normal file
25
packages/commerce/src/schemas/checkout.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { cartLineItemSchema } from './cart'
|
||||||
|
import { addressFieldsSchema, cardFieldsSchema } from './customer'
|
||||||
|
|
||||||
|
export const getCheckoutBodySchema = z.object({
|
||||||
|
cartId: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const submitCheckoutBodySchema = z.object({
|
||||||
|
cartId: z.string(),
|
||||||
|
item: z.object({
|
||||||
|
cartId: z.string().optional(),
|
||||||
|
card: cardFieldsSchema,
|
||||||
|
address: addressFieldsSchema,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const checkoutSchema = z.object({
|
||||||
|
hasPayment: z.boolean(),
|
||||||
|
hasShipping: z.boolean(),
|
||||||
|
addressId: z.string(),
|
||||||
|
payments: z.array(cardFieldsSchema).optional(),
|
||||||
|
cardId: z.string().optional(),
|
||||||
|
lineItems: z.array(cartLineItemSchema).optional(),
|
||||||
|
})
|
84
packages/commerce/src/schemas/customer.ts
Normal file
84
packages/commerce/src/schemas/customer.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const getCustomerAddressBodySchema = z.object({
|
||||||
|
cartId: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const customerSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
firstName: z.string(),
|
||||||
|
lastName: z.string(),
|
||||||
|
email: z.string().optional(),
|
||||||
|
phone: z.string().optional(),
|
||||||
|
company: z.string().optional(),
|
||||||
|
notes: z.string().optional(),
|
||||||
|
acceptsMarketing: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const addressSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
mask: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const addressFieldsSchema = z.object({
|
||||||
|
type: z.string(),
|
||||||
|
firstName: z.string(),
|
||||||
|
lastName: z.string(),
|
||||||
|
company: z.string(),
|
||||||
|
streetNumber: z.string(),
|
||||||
|
apartments: z.string(),
|
||||||
|
zipCode: z.string(),
|
||||||
|
city: z.string(),
|
||||||
|
country: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const addAddressBodySchema = z.object({
|
||||||
|
cartId: z.string(),
|
||||||
|
item: addressFieldsSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const updateAddressBodySchema = z.object({
|
||||||
|
cartId: z.string(),
|
||||||
|
itemId: z.string(),
|
||||||
|
item: addressFieldsSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const deleteAddressBodySchema = z.object({
|
||||||
|
cartId: z.string(),
|
||||||
|
itemId: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const cardFieldsSchema = z.object({
|
||||||
|
cardHolder: z.string(),
|
||||||
|
cardNumber: z.string(),
|
||||||
|
cardExpireDate: z.string(),
|
||||||
|
cardCvc: z.string(),
|
||||||
|
firstName: z.string(),
|
||||||
|
lastName: z.string(),
|
||||||
|
company: z.string(),
|
||||||
|
streetNumber: z.string(),
|
||||||
|
zipCode: z.string(),
|
||||||
|
city: z.string(),
|
||||||
|
country: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const cardSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
mask: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const addCardBodySchema = z.object({
|
||||||
|
cartId: z.string(),
|
||||||
|
item: cardFieldsSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const updateCardBodySchema = z.object({
|
||||||
|
cartId: z.string(),
|
||||||
|
itemId: z.string(),
|
||||||
|
item: cardFieldsSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const deleteCardBodySchema = z.object({
|
||||||
|
cartId: z.string(),
|
||||||
|
itemId: z.string(),
|
||||||
|
})
|
@ -1,4 +1,4 @@
|
|||||||
import { z } from 'zod'
|
import { boolean, z } from 'zod'
|
||||||
|
|
||||||
export const productPriceSchema = z.object({
|
export const productPriceSchema = z.object({
|
||||||
value: z.number(),
|
value: z.number(),
|
||||||
@ -52,9 +52,14 @@ export const productsPathsSchema = z.array(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const searchProductBodySchema = z.object({
|
export const searchProductBodySchema = z.object({
|
||||||
search: z.string(),
|
search: z.string().optional(),
|
||||||
categoryId: z.string(),
|
categoryId: z.string().optional(),
|
||||||
brandId: z.string().optional(),
|
brandId: z.string().optional(),
|
||||||
sort: z.string().optional(),
|
sort: z.string().optional(),
|
||||||
locale: z.string().optional(),
|
locale: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const searchProductsSchema = z.object({
|
||||||
|
products: z.array(productSchema),
|
||||||
|
found: z.boolean(),
|
||||||
|
})
|
||||||
|
41
packages/commerce/src/schemas/whishlist.ts
Normal file
41
packages/commerce/src/schemas/whishlist.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { productSchema } from './product'
|
||||||
|
|
||||||
|
export const wishlistSchemaItem = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
productId: z.string(),
|
||||||
|
variantId: z.string(),
|
||||||
|
product: productSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const wishlistSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
items: z.array(wishlistSchemaItem),
|
||||||
|
token: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getWishlistBodySchema = z.object({
|
||||||
|
customerAccessToken: z.string(),
|
||||||
|
includeProducts: z.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const wishlistItemBodySchema = z.object({
|
||||||
|
productId: z.string(),
|
||||||
|
variantId: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const addItemBodySchema = z.object({
|
||||||
|
cartId: z.string().optional(),
|
||||||
|
item: wishlistItemBodySchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const updateItemBodySchema = z.object({
|
||||||
|
cartId: z.string(),
|
||||||
|
itemId: z.string(),
|
||||||
|
item: wishlistItemBodySchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const removeItemBodySchema = z.object({
|
||||||
|
cartId: z.string(),
|
||||||
|
itemId: z.string(),
|
||||||
|
})
|
@ -199,14 +199,14 @@ export type CartHooks = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type GetCartHook = {
|
export type GetCartHook = {
|
||||||
data: Cart | null
|
data: Cart | null | undefined
|
||||||
input: {}
|
input: {}
|
||||||
fetcherInput: { cartId?: string }
|
fetcherInput: { cartId?: string }
|
||||||
swrState: { isEmpty: boolean }
|
swrState: { isEmpty: boolean }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AddItemHook = {
|
export type AddItemHook = {
|
||||||
data: Cart
|
data: Cart | null | undefined
|
||||||
input?: CartItemBody
|
input?: CartItemBody
|
||||||
fetcherInput: CartItemBody
|
fetcherInput: CartItemBody
|
||||||
body: { item: CartItemBody }
|
body: { item: CartItemBody }
|
||||||
@ -251,7 +251,8 @@ export type GetCartHandler = GetCartHook & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type AddItemHandler = AddItemHook & {
|
export type AddItemHandler = AddItemHook & {
|
||||||
body: { cartId: string }
|
data: Cart | null | undefined
|
||||||
|
body: { cartId?: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateItemHandler = UpdateItemHook & {
|
export type UpdateItemHandler = UpdateItemHook & {
|
||||||
|
@ -48,7 +48,7 @@ export interface CheckoutBody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type SubmitCheckoutHook = {
|
export type SubmitCheckoutHook = {
|
||||||
data: Checkout
|
data: Checkout | null
|
||||||
input?: CheckoutBody
|
input?: CheckoutBody
|
||||||
fetcherInput: CheckoutBody
|
fetcherInput: CheckoutBody
|
||||||
body: { item: CheckoutBody }
|
body: { item: CheckoutBody }
|
||||||
@ -69,7 +69,7 @@ export type CheckoutHooks = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type GetCheckoutHandler = GetCheckoutHook & {
|
export type GetCheckoutHandler = GetCheckoutHook & {
|
||||||
body: { cartId: string }
|
body: { cartId?: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SubmitCheckoutHandler = SubmitCheckoutHook & {
|
export type SubmitCheckoutHandler = SubmitCheckoutHook & {
|
||||||
|
@ -61,7 +61,7 @@ export type GetAddressesHook = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type AddItemHook = {
|
export type AddItemHook = {
|
||||||
data: Address
|
data: Address | null
|
||||||
input?: AddressFields
|
input?: AddressFields
|
||||||
fetcherInput: AddressFields
|
fetcherInput: AddressFields
|
||||||
body: { item: AddressFields }
|
body: { item: AddressFields }
|
||||||
@ -77,7 +77,7 @@ export type UpdateItemHook = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type RemoveItemHook = {
|
export type RemoveItemHook = {
|
||||||
data: Address | null | undefined
|
data: Address | null
|
||||||
input: { item?: Address }
|
input: { item?: Address }
|
||||||
fetcherInput: { itemId: string }
|
fetcherInput: { itemId: string }
|
||||||
body: { itemId: string }
|
body: { itemId: string }
|
||||||
@ -100,7 +100,6 @@ export type AddItemHandler = AddItemHook & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateItemHandler = UpdateItemHook & {
|
export type UpdateItemHandler = UpdateItemHook & {
|
||||||
data: Address
|
|
||||||
body: { cartId: string }
|
body: { cartId: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ export type GetCardsHook = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type AddItemHook = {
|
export type AddItemHook = {
|
||||||
data: Card
|
data: Card | null
|
||||||
input?: CardFields
|
input?: CardFields
|
||||||
fetcherInput: CardFields
|
fetcherInput: CardFields
|
||||||
body: { item: CardFields }
|
body: { item: CardFields }
|
||||||
@ -86,7 +86,7 @@ export type AddItemHook = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateItemHook = {
|
export type UpdateItemHook = {
|
||||||
data: Card | null | undefined
|
data: Card | null
|
||||||
input: { item?: CardFields; wait?: number }
|
input: { item?: CardFields; wait?: number }
|
||||||
fetcherInput: { itemId: string; item: CardFields }
|
fetcherInput: { itemId: string; item: CardFields }
|
||||||
body: { itemId: string; item: CardFields }
|
body: { itemId: string; item: CardFields }
|
||||||
@ -94,7 +94,7 @@ export type UpdateItemHook = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type RemoveItemHook = {
|
export type RemoveItemHook = {
|
||||||
data: Card | null | undefined
|
data: Card | null
|
||||||
input: { item?: Card }
|
input: { item?: Card }
|
||||||
fetcherInput: { itemId: string }
|
fetcherInput: { itemId: string }
|
||||||
body: { itemId: string }
|
body: { itemId: string }
|
||||||
@ -116,7 +116,6 @@ export type AddItemHandler = AddItemHook & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateItemHandler = UpdateItemHook & {
|
export type UpdateItemHandler = UpdateItemHook & {
|
||||||
data: Card
|
|
||||||
body: { cartId: string }
|
body: { cartId: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ export type HookFetcherOptions = { method?: string } & (
|
|||||||
| { query?: string; url: string }
|
| { query?: string; url: string }
|
||||||
)
|
)
|
||||||
|
|
||||||
export type HookInputValue = string | number | boolean | undefined
|
export type HookInputValue = string | number | boolean | null | undefined
|
||||||
|
|
||||||
export type HookSWRInput = [string, HookInputValue][]
|
export type HookSWRInput = [string, HookInputValue][]
|
||||||
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export default function noopApi(...args: any[]): void {}
|
|
@ -1 +0,0 @@
|
|||||||
export default function noopApi(...args: any[]): void {}
|
|
@ -1 +0,0 @@
|
|||||||
export default function noopApi(...args: any[]): void {}
|
|
@ -1 +1,3 @@
|
|||||||
export default function noopApi(...args: any[]): void {}
|
export default function getCheckout(..._args: any[]) {
|
||||||
|
return Promise.resolve({ data: null })
|
||||||
|
}
|
||||||
|
@ -3,16 +3,15 @@ import checkoutEndpoint from '@vercel/commerce/api/endpoints/checkout'
|
|||||||
import type { CheckoutSchema } from '@vercel/commerce/types/checkout'
|
import type { CheckoutSchema } from '@vercel/commerce/types/checkout'
|
||||||
import type { CommercejsAPI } from '../..'
|
import type { CommercejsAPI } from '../..'
|
||||||
|
|
||||||
import submitCheckout from './submit-checkout'
|
|
||||||
import getCheckout from './get-checkout'
|
import getCheckout from './get-checkout'
|
||||||
|
import submitCheckout from './submit-checkout'
|
||||||
|
|
||||||
export type CheckoutAPI = GetAPISchema<CommercejsAPI, CheckoutSchema>
|
export type CheckoutAPI = GetAPISchema<CommercejsAPI, CheckoutSchema>
|
||||||
|
|
||||||
export type CheckoutEndpoint = CheckoutAPI['endpoint']
|
export type CheckoutEndpoint = CheckoutAPI['endpoint']
|
||||||
|
|
||||||
export const handlers: CheckoutEndpoint['handlers'] = {
|
export const handlers: CheckoutEndpoint['handlers'] = {
|
||||||
submitCheckout,
|
|
||||||
getCheckout,
|
getCheckout,
|
||||||
|
submitCheckout,
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkoutApi = createEndpoint<CheckoutAPI>({
|
const checkoutApi = createEndpoint<CheckoutAPI>({
|
||||||
|
@ -5,7 +5,6 @@ import sdkFetcherFunction from '../../utils/sdk-fetch'
|
|||||||
import { normalizeTestCheckout } from '../../../utils/normalize-checkout'
|
import { normalizeTestCheckout } from '../../../utils/normalize-checkout'
|
||||||
|
|
||||||
const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({
|
const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({
|
||||||
res,
|
|
||||||
body: { item, cartId },
|
body: { item, cartId },
|
||||||
config: { sdkFetch },
|
config: { sdkFetch },
|
||||||
}) => {
|
}) => {
|
||||||
@ -38,7 +37,7 @@ const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({
|
|||||||
// Capture the order
|
// Capture the order
|
||||||
await sdkFetcher('checkout', 'capture', checkoutToken, checkoutData)
|
await sdkFetcher('checkout', 'capture', checkoutToken, checkoutData)
|
||||||
|
|
||||||
res.status(200).json({ data: null, errors: [] })
|
return { data: null }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default submitCheckout
|
export default submitCheckout
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export default function noopApi(...args: any[]): void {}
|
|
@ -1 +0,0 @@
|
|||||||
export default function noopApi(...args: any[]): void {}
|
|
@ -1 +0,0 @@
|
|||||||
export default function noopApi(...args: any[]): void {}
|
|
15
packages/commercejs/src/api/endpoints/index.ts
Normal file
15
packages/commercejs/src/api/endpoints/index.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import type { CommercejsAPI } from '..'
|
||||||
|
|
||||||
|
import createEndpoints from '@vercel/commerce/api/endpoints'
|
||||||
|
|
||||||
|
import login from './login'
|
||||||
|
import checkout from './checkout'
|
||||||
|
|
||||||
|
const endpoints = {
|
||||||
|
login,
|
||||||
|
checkout,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function commercejsAPI(commerce: CommercejsAPI) {
|
||||||
|
return createEndpoints(commerce, endpoints)
|
||||||
|
}
|
@ -1,32 +1,33 @@
|
|||||||
|
import type { LoginEndpoint } from '.'
|
||||||
|
|
||||||
import { serialize } from 'cookie'
|
import { serialize } from 'cookie'
|
||||||
|
|
||||||
import sdkFetcherFunction from '../../utils/sdk-fetch'
|
import sdkFetcherFunction from '../../utils/sdk-fetch'
|
||||||
import { getDeploymentUrl } from '../../../utils/get-deployment-url'
|
import { getDeploymentUrl } from '../../../utils/get-deployment-url'
|
||||||
import type { LoginEndpoint } from '.'
|
|
||||||
|
|
||||||
const login: LoginEndpoint['handlers']['login'] = async ({
|
const login: LoginEndpoint['handlers']['login'] = async ({
|
||||||
req,
|
req,
|
||||||
res,
|
|
||||||
config: { sdkFetch, customerCookie },
|
config: { sdkFetch, customerCookie },
|
||||||
}) => {
|
}) => {
|
||||||
const sdkFetcher: typeof sdkFetcherFunction = sdkFetch
|
const sdkFetcher: typeof sdkFetcherFunction = sdkFetch
|
||||||
const redirectUrl = getDeploymentUrl()
|
const redirectUrl = getDeploymentUrl()
|
||||||
try {
|
const { searchParams } = new URL(req.url)
|
||||||
const loginToken = req.query?.token as string
|
const loginToken = searchParams.get('token')
|
||||||
|
|
||||||
if (!loginToken) {
|
if (!loginToken) {
|
||||||
res.redirect(redirectUrl)
|
return { redirectTo: redirectUrl }
|
||||||
}
|
}
|
||||||
|
|
||||||
const { jwt } = await sdkFetcher('customer', 'getToken', loginToken, false)
|
const { jwt } = await sdkFetcher('customer', 'getToken', loginToken, false)
|
||||||
res.setHeader(
|
|
||||||
'Set-Cookie',
|
return {
|
||||||
serialize(customerCookie, jwt, {
|
headers: {
|
||||||
|
'Set-Cookie': serialize(customerCookie, jwt, {
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: process.env.NODE_ENV === 'production',
|
||||||
maxAge: 60 * 60 * 24,
|
maxAge: 60 * 60 * 24,
|
||||||
path: '/',
|
path: '/',
|
||||||
})
|
}),
|
||||||
)
|
},
|
||||||
res.redirect(redirectUrl)
|
|
||||||
} catch {
|
|
||||||
res.redirect(redirectUrl)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export default function noopApi(...args: any[]): void {}
|
|
@ -1 +0,0 @@
|
|||||||
export default function noopApi(...args: any[]): void {}
|
|
@ -1 +0,0 @@
|
|||||||
export default function noopApi(...args: any[]): void {}
|
|
@ -11,7 +11,7 @@ export default useSubmitCheckout as UseSubmitCheckout<typeof handler>
|
|||||||
|
|
||||||
export const handler: MutationHook<SubmitCheckoutHook> = {
|
export const handler: MutationHook<SubmitCheckoutHook> = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
url: '/api/checkout',
|
url: '/api/commerce/checkout',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
},
|
},
|
||||||
async fetcher({ input: item, options, fetch }) {
|
async fetcher({ input: item, options, fetch }) {
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
|
import type { SWRHook } from '@vercel/commerce/utils/types'
|
||||||
|
import type { CustomerHook } from '@vercel/commerce/types/customer'
|
||||||
|
|
||||||
import Cookies from 'js-cookie'
|
import Cookies from 'js-cookie'
|
||||||
import { decode } from 'jsonwebtoken'
|
import { decode, type JwtPayload } from 'jsonwebtoken'
|
||||||
import { SWRHook } from '@vercel/commerce/utils/types'
|
|
||||||
import useCustomer, {
|
import useCustomer, {
|
||||||
UseCustomer,
|
type UseCustomer,
|
||||||
} from '@vercel/commerce/customer/use-customer'
|
} from '@vercel/commerce/customer/use-customer'
|
||||||
import { CUSTOMER_COOKIE, API_URL } from '../constants'
|
import { CUSTOMER_COOKIE, API_URL } from '../constants'
|
||||||
import type { CustomerHook } from '@vercel/commerce/types/customer'
|
|
||||||
|
type JwtData = JwtPayload & {
|
||||||
|
cid: string
|
||||||
|
}
|
||||||
|
|
||||||
export default useCustomer as UseCustomer<typeof handler>
|
export default useCustomer as UseCustomer<typeof handler>
|
||||||
export const handler: SWRHook<CustomerHook> = {
|
export const handler: SWRHook<CustomerHook> = {
|
||||||
@ -20,7 +25,8 @@ export const handler: SWRHook<CustomerHook> = {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const decodedToken = decode(token) as { cid: string }
|
const decodedToken = decode(token) as JwtData
|
||||||
|
|
||||||
const customer = await fetch<any>({
|
const customer = await fetch<any>({
|
||||||
query: options.query,
|
query: options.query,
|
||||||
method: options.method,
|
method: options.method,
|
||||||
|
@ -4,8 +4,8 @@ const getFilterVariables = ({
|
|||||||
search,
|
search,
|
||||||
categoryId,
|
categoryId,
|
||||||
}: {
|
}: {
|
||||||
search?: string
|
search?: string | null
|
||||||
categoryId?: string | number
|
categoryId?: string | number | null
|
||||||
}) => {
|
}) => {
|
||||||
let filterVariables: { [key: string]: any } = {}
|
let filterVariables: { [key: string]: any } = {}
|
||||||
if (search) {
|
if (search) {
|
||||||
@ -17,7 +17,7 @@ const getFilterVariables = ({
|
|||||||
return filterVariables
|
return filterVariables
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSortVariables = ({ sort }: { sort?: string }) => {
|
const getSortVariables = ({ sort }: { sort?: string | null }) => {
|
||||||
let sortVariables: { [key: string]: any } = {}
|
let sortVariables: { [key: string]: any } = {}
|
||||||
switch (sort) {
|
switch (sort) {
|
||||||
case 'trending-desc':
|
case 'trending-desc':
|
||||||
|
@ -50,9 +50,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vercel/commerce": "workspace:*",
|
"@vercel/commerce": "workspace:*",
|
||||||
"@vercel/fetch": "^6.2.0",
|
"lodash.debounce": "^4.0.8"
|
||||||
"lodash.debounce": "^4.0.8",
|
|
||||||
"node-fetch": "^2.6.7"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"next": "^12",
|
"next": "^12",
|
||||||
|
@ -53,28 +53,19 @@ const buildAddToCartVariables = ({
|
|||||||
|
|
||||||
const addItem: CartEndpoint['handlers']['addItem'] = async ({
|
const addItem: CartEndpoint['handlers']['addItem'] = async ({
|
||||||
req,
|
req,
|
||||||
res,
|
body: { item },
|
||||||
body: { cartId, item },
|
|
||||||
config,
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
if (!item) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Missing item' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (!item.quantity) item.quantity = 1
|
|
||||||
|
|
||||||
const productResponse = await config.fetch(getProductQuery, {
|
const productResponse = await config.fetch(getProductQuery, {
|
||||||
variables: { productCode: item?.productId },
|
variables: { productCode: item?.productId },
|
||||||
})
|
})
|
||||||
|
|
||||||
const cookieHandler = new CookieHandler(config, req, res)
|
const cookieHandler = new CookieHandler(config, req)
|
||||||
let accessToken = null
|
let accessToken = null
|
||||||
|
|
||||||
if (!cookieHandler.getAccessToken()) {
|
if (!cookieHandler.getAccessToken()) {
|
||||||
let anonymousShopperTokenResponse = await cookieHandler.getAnonymousToken()
|
let anonymousShopperTokenResponse = await cookieHandler.getAnonymousToken()
|
||||||
accessToken = anonymousShopperTokenResponse.accessToken;
|
accessToken = anonymousShopperTokenResponse.accessToken
|
||||||
} else {
|
} else {
|
||||||
accessToken = cookieHandler.getAccessToken()
|
accessToken = cookieHandler.getAccessToken()
|
||||||
}
|
}
|
||||||
@ -95,7 +86,8 @@ const addItem: CartEndpoint['handlers']['addItem'] = async ({
|
|||||||
)
|
)
|
||||||
currentCart = result?.data?.currentCart
|
currentCart = result?.data?.currentCart
|
||||||
}
|
}
|
||||||
res.status(200).json({ data: normalizeCart(currentCart) })
|
|
||||||
|
return { data: normalizeCart(currentCart) }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default addItem
|
export default addItem
|
||||||
|
@ -6,17 +6,17 @@ import { getCartQuery } from '../../queries/get-cart-query'
|
|||||||
|
|
||||||
const getCart: CartEndpoint['handlers']['getCart'] = async ({
|
const getCart: CartEndpoint['handlers']['getCart'] = async ({
|
||||||
req,
|
req,
|
||||||
res,
|
|
||||||
body: { cartId },
|
|
||||||
config,
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
let currentCart: Cart = {}
|
let currentCart: Cart = {}
|
||||||
|
let headers
|
||||||
try {
|
try {
|
||||||
const cookieHandler = new CookieHandler(config, req, res)
|
const cookieHandler = new CookieHandler(config, req)
|
||||||
let accessToken = null
|
let accessToken = null
|
||||||
|
|
||||||
if (!cookieHandler.getAccessToken()) {
|
if (!cookieHandler.getAccessToken()) {
|
||||||
let anonymousShopperTokenResponse = await cookieHandler.getAnonymousToken()
|
let anonymousShopperTokenResponse =
|
||||||
|
await cookieHandler.getAnonymousToken()
|
||||||
const response = anonymousShopperTokenResponse.response
|
const response = anonymousShopperTokenResponse.response
|
||||||
accessToken = anonymousShopperTokenResponse.accessToken
|
accessToken = anonymousShopperTokenResponse.accessToken
|
||||||
cookieHandler.setAnonymousShopperCookie(response)
|
cookieHandler.setAnonymousShopperCookie(response)
|
||||||
@ -30,12 +30,14 @@ const getCart: CartEndpoint['handlers']['getCart'] = async ({
|
|||||||
{ headers: { 'x-vol-user-claims': accessToken } }
|
{ headers: { 'x-vol-user-claims': accessToken } }
|
||||||
)
|
)
|
||||||
currentCart = result?.data?.currentCart
|
currentCart = result?.data?.currentCart
|
||||||
|
headers = cookieHandler.headers
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
res.status(200).json({
|
|
||||||
|
return {
|
||||||
data: currentCart ? normalizeCart(currentCart) : null,
|
data: currentCart ? normalizeCart(currentCart) : null,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default getCart
|
export default getCart
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
||||||
import cartEndpoint from '@vercel/commerce/api/endpoints/cart'
|
import cartEndpoint from '@vercel/commerce/api/endpoints/cart'
|
||||||
import type { KiboCommerceAPI } from '../..'
|
import type { KiboCommerceAPI } from '../..'
|
||||||
import getCart from './get-cart';
|
import getCart from './get-cart'
|
||||||
import addItem from './add-item';
|
import addItem from './add-item'
|
||||||
import updateItem from './update-item'
|
import updateItem from './update-item'
|
||||||
import removeItem from './remove-item'
|
import removeItem from './remove-item'
|
||||||
|
|
||||||
|
@ -5,17 +5,10 @@ import { getCartQuery } from '../../../api/queries/get-cart-query'
|
|||||||
|
|
||||||
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
|
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
|
||||||
req,
|
req,
|
||||||
res,
|
body: { itemId },
|
||||||
body: { cartId, itemId },
|
|
||||||
config,
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
if (!itemId) {
|
const encodedToken = req.cookies.get(config.customerCookie)
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Invalid request' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const encodedToken = req.cookies[config.customerCookie]
|
|
||||||
const token = encodedToken
|
const token = encodedToken
|
||||||
? Buffer.from(encodedToken, 'base64').toString('ascii')
|
? Buffer.from(encodedToken, 'base64').toString('ascii')
|
||||||
: null
|
: null
|
||||||
@ -39,7 +32,10 @@ const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
|
|||||||
)
|
)
|
||||||
currentCart = result?.data?.currentCart
|
currentCart = result?.data?.currentCart
|
||||||
}
|
}
|
||||||
res.status(200).json({ data: normalizeCart(currentCart) })
|
|
||||||
|
return {
|
||||||
|
data: normalizeCart(currentCart),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default removeItem
|
export default removeItem
|
||||||
|
@ -5,17 +5,10 @@ import updateCartItemQuantityMutation from '../../../api/mutations/updateCartIte
|
|||||||
|
|
||||||
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
|
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
|
||||||
req,
|
req,
|
||||||
res,
|
body: { itemId, item },
|
||||||
body: { cartId, itemId, item },
|
|
||||||
config,
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
if (!itemId || !item) {
|
const encodedToken = req.cookies.get(config.cartCookie)
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Invalid request' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const encodedToken = req.cookies[config.customerCookie]
|
|
||||||
const token = encodedToken
|
const token = encodedToken
|
||||||
? Buffer.from(encodedToken, 'base64').toString('ascii')
|
? Buffer.from(encodedToken, 'base64').toString('ascii')
|
||||||
: null
|
: null
|
||||||
@ -39,7 +32,8 @@ const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
|
|||||||
)
|
)
|
||||||
currentCart = result?.data?.currentCart
|
currentCart = result?.data?.currentCart
|
||||||
}
|
}
|
||||||
res.status(200).json({ data: normalizeCart(currentCart) })
|
|
||||||
|
return { data: normalizeCart(currentCart) }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default updateItem
|
export default updateItem
|
||||||
|
@ -5,13 +5,12 @@ import { buildProductSearchVars } from '../../../../lib/product-search-vars'
|
|||||||
import { normalizeProduct } from '../../../../lib/normalize'
|
import { normalizeProduct } from '../../../../lib/normalize'
|
||||||
|
|
||||||
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
|
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
|
||||||
res,
|
|
||||||
body: { search, categoryId, brandId, sort },
|
body: { search, categoryId, brandId, sort },
|
||||||
config,
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
const pageSize = 100;
|
const pageSize = 100
|
||||||
const filters = {};
|
const filters = {}
|
||||||
const startIndex = 0;
|
const startIndex = 0
|
||||||
const variables = buildProductSearchVars({
|
const variables = buildProductSearchVars({
|
||||||
categoryCode: categoryId,
|
categoryCode: categoryId,
|
||||||
pageSize,
|
pageSize,
|
||||||
@ -20,12 +19,14 @@ const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
|
|||||||
filters,
|
filters,
|
||||||
startIndex,
|
startIndex,
|
||||||
})
|
})
|
||||||
const {data} = await config.fetch(productSearchQuery, { variables });
|
const { data } = await config.fetch(productSearchQuery, { variables })
|
||||||
const found = data?.products?.items?.length > 0 ? true : false;
|
const found = data?.products?.items?.length > 0 ? true : false
|
||||||
let productsResponse= data?.products?.items.map((item: any) =>normalizeProduct(item,config));
|
let productsResponse = data?.products?.items.map((item: any) =>
|
||||||
const products: Product[] = found ? productsResponse : [];
|
normalizeProduct(item, config)
|
||||||
|
)
|
||||||
|
const products: Product[] = found ? productsResponse : []
|
||||||
|
|
||||||
res.status(200).json({ data: { products, found } });
|
return { data: { products, found } }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default getProducts
|
export default getProducts
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export default function noopApi(...args: any[]): void {}
|
|
@ -1 +0,0 @@
|
|||||||
export default function noopApi(...args: any[]): void {}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user