mirror of
https://github.com/vercel/commerce.git
synced 2025-05-16 14:36:59 +00:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
1f2634a38c
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,9 +1,8 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
.pnpm-debug.log
|
||||
|
||||
# testing
|
||||
coverage
|
||||
|
33
README.md
33
README.md
@ -20,11 +20,11 @@ Demo live at: [demo.vercel.store](https://demo.vercel.store/)
|
||||
|
||||
## Run minimal version locally
|
||||
|
||||
> To run a minimal version of Next.js Commerce you can start with the default local provider `@vercel/commerce-local` that has disabled all features (cart, auth) and use static files for the backend
|
||||
> To run a minimal version of Next.js Commerce you can start with the default local provider `@vercel/commerce-local` that has all features disabled (cart, auth) and uses static files for the backend
|
||||
|
||||
```bash
|
||||
yarn # run this command in root folder of the mono repo
|
||||
yarn dev
|
||||
pnpm install & pnpm build # run these commands in the root folder of the mono repo
|
||||
pnpm dev # run this command in the site folder
|
||||
```
|
||||
|
||||
> If you encounter any problems while installing and running for the first time, please see the Troubleshoot section
|
||||
@ -47,10 +47,10 @@ Next.js Commerce integrates out-of-the-box with BigCommerce, Shopify, Swell, Sal
|
||||
|
||||
## Considerations
|
||||
|
||||
- `packages/commerce` contains all types, helpers and functions to be used as base to build a new **provider**.
|
||||
- `packages/commerce` contains all types, helpers and functions to be used as a base to build a new **provider**.
|
||||
- **Providers** live under `packages`'s root folder and they will extend Next.js Commerce types and functionality (`packages/commerce`).
|
||||
- We have a **Features API** to ensure feature parity between the UI and the Provider. The UI should update accordingly and no extra code should be bundled. All extra configuration for features will live under `features` in `commerce.config.json` and if needed it can also be accessed programatically.
|
||||
- Each **provider** should add its corresponding `next.config.js` and `commerce.config.json` adding specific data related to the provider. For example in case of BigCommerce, the images CDN and additional API routes.
|
||||
- We have a **Features API** to ensure feature parity between the UI and the Provider. The UI should update accordingly and no extra code should be bundled. All extra configuration for features will live under `features` in `commerce.config.json` and if needed it can also be accessed programmatically.
|
||||
- Each **provider** should add its corresponding `next.config.js` and `commerce.config.json` adding specific data related to the provider. For example in the case of BigCommerce, the images CDN and additional API routes.
|
||||
|
||||
## Configuration
|
||||
|
||||
@ -73,7 +73,7 @@ Every provider defines the features that it supports under `packages/{provider}/
|
||||
#### Features Available
|
||||
|
||||
The following features can be enabled or disabled. This means that the UI will remove all code related to the feature.
|
||||
For example: Turning `cart` off will disable Cart capabilities.
|
||||
For example: turning `cart` off will disable Cart capabilities.
|
||||
|
||||
- cart
|
||||
- search
|
||||
@ -83,7 +83,7 @@ For example: Turning `cart` off will disable Cart capabilities.
|
||||
|
||||
#### How to turn Features on and off
|
||||
|
||||
> NOTE: The selected provider should support the feature that you are toggling. (This means that you can't turn wishlist on if the provider doesn't support this functionality out the box)
|
||||
> NOTE: The selected provider should support the feature that you are toggling. (This means that you can't turn wishlist on if the provider doesn't support this functionality out of the box)
|
||||
|
||||
- Open `site/commerce.config.json`
|
||||
- You'll see a config file like this:
|
||||
@ -110,11 +110,12 @@ Our commitment to Open Source can be found [here](https://vercel.com/oss).
|
||||
|
||||
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||
2. Create a new branch `git checkout -b MY_BRANCH_NAME`
|
||||
3. Install the dependencies: `yarn`
|
||||
4. Duplicate `site/.env.template` and rename it to `site/.env.local`
|
||||
5. Add proper store values to `site/.env.local`
|
||||
6. Run `cd site` and `yarn dev` to build and watch for code changes
|
||||
7. Run `yarn turbo run build` to check the build after your changes
|
||||
3. Install the dependencies: `pnpm install`
|
||||
4. Build the packages: `pnpm build`
|
||||
5. Duplicate `site/.env.template` and rename it to `site/.env.local`
|
||||
6. Add proper store values to `site/.env.local`
|
||||
7. Run `cd site` & `pnpm dev` to watch for code changes
|
||||
8. Run `pnpm turbo run build` to check the build after your changes
|
||||
|
||||
## Work in progress
|
||||
|
||||
@ -189,10 +190,10 @@ error Command failed with exit code 1.
|
||||
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
|
||||
```
|
||||
|
||||
The error usually occurs when running yarn dev inside of the `/site/` folder after installing a fresh repository.
|
||||
The error usually occurs when running `pnpm dev` inside of the `/site/` folder after installing a fresh repository.
|
||||
|
||||
In order to fix this, run `yarn dev` in the monorepo root folder first.
|
||||
In order to fix this, run `pnpm build` in the monorepo root folder first.
|
||||
|
||||
> Using `yarn dev` from the root is recommended for developing, which will run watch mode on all packages.
|
||||
> Using `pnpm dev` from the root is recommended for developing, which will run watch mode on all packages.
|
||||
|
||||
</details>
|
||||
|
14
package.json
14
package.json
@ -2,26 +2,22 @@
|
||||
"name": "commerce",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"site",
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "turbo run build --scope=next-commerce --include-dependencies --no-deps",
|
||||
"build": "turbo run build --filter=next-commerce...",
|
||||
"dev": "turbo run dev",
|
||||
"start": "turbo run start",
|
||||
"types": "turbo run types",
|
||||
"prettier-fix": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"husky": "^7.0.4",
|
||||
"prettier": "^2.5.1",
|
||||
"turbo": "^1.1.2"
|
||||
"husky": "^8.0.1",
|
||||
"prettier": "^2.7.1",
|
||||
"turbo": "^1.4.6"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "turbo run lint"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.17"
|
||||
"packageManager": "pnpm@7.5.0"
|
||||
}
|
||||
|
@ -47,17 +47,20 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/fetch": "^6.1.1",
|
||||
"@cfworker/uuid": "^1.12.4",
|
||||
"@tsndr/cloudflare-worker-jwt": "^2.1.0",
|
||||
"@vercel/commerce": "workspace:*",
|
||||
"cookie": "^0.4.1",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"js-cookie": "^3.0.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"uuidv4": "^6.2.12"
|
||||
"uuidv4": "^6.2.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": "^12",
|
||||
"react": "^17",
|
||||
"react-dom": "^17"
|
||||
"next": "^13",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@taskr/clear": "^1.1.0",
|
||||
@ -67,15 +70,16 @@
|
||||
"@types/jsonwebtoken": "^8.5.7",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/node": "^17.0.8",
|
||||
"@types/react": "^17.0.38",
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/react": "^18.0.14",
|
||||
"lint-staged": "^12.1.7",
|
||||
"next": "^12.0.8",
|
||||
"next": "^13.0.6",
|
||||
"prettier": "^2.5.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"taskr": "^1.1.0",
|
||||
"taskr-swc": "^0.0.1",
|
||||
"typescript": "^4.5.4"
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.{js,jsx,ts,tsx,json}": [
|
||||
|
@ -1130,7 +1130,7 @@ export interface definitions {
|
||||
*/
|
||||
search_keywords?: string
|
||||
/**
|
||||
* Image URL used for this category on the storefront. Images can be uploaded via form file post to `/brands/{brandId}/image`, or by providing a publicly accessible URL in this field.
|
||||
* Image URL used for this category on the storefront. Images can be uploaded via form file post to `/{brandId}/image`, or by providing a publicly accessible URL in this field.
|
||||
*/
|
||||
image_url?: string
|
||||
custom_url?: definitions['customUrl_Full']
|
||||
|
@ -1,21 +1,14 @@
|
||||
import type { CartEndpoint } from '.'
|
||||
import type { BigcommerceCart } from '../../../types'
|
||||
|
||||
import { normalizeCart } from '../../../lib/normalize'
|
||||
import { parseCartItem } from '../../utils/parse-item'
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
import type { CartEndpoint } from '.'
|
||||
|
||||
const addItem: CartEndpoint['handlers']['addItem'] = async ({
|
||||
res,
|
||||
body: { cartId, item },
|
||||
config,
|
||||
}) => {
|
||||
if (!item) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Missing item' }],
|
||||
})
|
||||
}
|
||||
if (!item.quantity) item.quantity = 1
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
@ -25,22 +18,27 @@ const addItem: CartEndpoint['handlers']['addItem'] = async ({
|
||||
: {}),
|
||||
}),
|
||||
}
|
||||
|
||||
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`,
|
||||
options
|
||||
)
|
||||
: await config.storeApiFetch(
|
||||
: await config.storeApiFetch<{ data: BigcommerceCart }>(
|
||||
'/v3/carts?include=line_items.physical_items.options,line_items.digital_items.options',
|
||||
options
|
||||
)
|
||||
|
||||
// Create or update the cart cookie
|
||||
res.setHeader(
|
||||
'Set-Cookie',
|
||||
getCartCookie(config.cartCookie, data.id, config.cartCookieMaxAge)
|
||||
)
|
||||
res.status(200).json({ data: normalizeCart(data) })
|
||||
return {
|
||||
data: normalizeCart(data),
|
||||
headers: {
|
||||
'Set-Cookie': getCartCookie(
|
||||
config.cartCookie,
|
||||
data.id,
|
||||
config.cartCookieMaxAge
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default addItem
|
||||
|
@ -1,35 +1,41 @@
|
||||
import type { CartEndpoint } from '.'
|
||||
import type { BigcommerceCart } from '../../../types'
|
||||
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
|
||||
import { normalizeCart } from '../../../lib/normalize'
|
||||
import { BigcommerceApiError } from '../../utils/errors'
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
import type { BigcommerceCart } from '../../../types/cart'
|
||||
import type { CartEndpoint } from '.'
|
||||
|
||||
// Return current cart info
|
||||
const getCart: CartEndpoint['handlers']['getCart'] = async ({
|
||||
res,
|
||||
body: { cartId },
|
||||
config,
|
||||
}) => {
|
||||
let result: { data?: BigcommerceCart } = {}
|
||||
|
||||
if (cartId) {
|
||||
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`
|
||||
)
|
||||
|
||||
return {
|
||||
data: result?.data ? normalizeCart(result.data) : null,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof BigcommerceApiError && error.status === 404) {
|
||||
// Remove the cookie if it exists but the cart wasn't found
|
||||
res.setHeader('Set-Cookie', getCartCookie(config.cartCookie))
|
||||
return {
|
||||
headers: { 'Set-Cookie': getCartCookie(config.cartCookie) },
|
||||
}
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
data: result.data ? normalizeCart(result.data) : null,
|
||||
})
|
||||
return {
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
|
||||
export default getCart
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
||||
import { type GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
||||
import cartEndpoint from '@vercel/commerce/api/endpoints/cart'
|
||||
import type { CartSchema } from '../../../types/cart'
|
||||
import type { CartSchema } from '@vercel/commerce/types/cart'
|
||||
import type { BigcommerceAPI } from '../..'
|
||||
import getCart from './get-cart'
|
||||
import addItem from './add-item'
|
||||
|
@ -1,34 +1,26 @@
|
||||
import { normalizeCart } from '../../../lib/normalize'
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
import type { CartEndpoint } from '.'
|
||||
|
||||
import { normalizeCart } from '../../../lib/normalize'
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
|
||||
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
|
||||
res,
|
||||
body: { cartId, itemId },
|
||||
config,
|
||||
}) => {
|
||||
if (!cartId || !itemId) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Invalid request' }],
|
||||
})
|
||||
}
|
||||
|
||||
const result = await config.storeApiFetch<{ data: any } | null>(
|
||||
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
|
||||
{ method: 'DELETE' }
|
||||
)
|
||||
const data = result?.data ?? null
|
||||
|
||||
res.setHeader(
|
||||
'Set-Cookie',
|
||||
data
|
||||
? // Update the cart cookie
|
||||
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
|
||||
: // Remove the cart cookie if the cart was removed (empty items)
|
||||
getCartCookie(config.cartCookie)
|
||||
)
|
||||
res.status(200).json({ data: data && normalizeCart(data) })
|
||||
return {
|
||||
data: result?.data ? normalizeCart(result.data) : null,
|
||||
headers: {
|
||||
'Set-Cookie': result?.data
|
||||
? // Update the cart cookie
|
||||
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
|
||||
: // Remove the cart cookie if the cart was removed (empty items)
|
||||
getCartCookie(config.cartCookie),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default removeItem
|
||||
|
@ -1,21 +1,15 @@
|
||||
import type { CartEndpoint } from '.'
|
||||
import type { BigcommerceCart } from '../../../types'
|
||||
|
||||
import { normalizeCart } from '../../../lib/normalize'
|
||||
import { parseCartItem } from '../../utils/parse-item'
|
||||
import getCartCookie from '../../utils/get-cart-cookie'
|
||||
import type { CartEndpoint } from '.'
|
||||
|
||||
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
|
||||
res,
|
||||
body: { cartId, itemId, item },
|
||||
config,
|
||||
}) => {
|
||||
if (!cartId || !itemId || !item) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Invalid request' }],
|
||||
})
|
||||
}
|
||||
|
||||
const { data } = await config.storeApiFetch(
|
||||
const { data } = await config.storeApiFetch<{ data: BigcommerceCart }>(
|
||||
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
|
||||
{
|
||||
method: 'PUT',
|
||||
@ -25,12 +19,16 @@ const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
|
||||
}
|
||||
)
|
||||
|
||||
// Update the cart cookie
|
||||
res.setHeader(
|
||||
'Set-Cookie',
|
||||
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
|
||||
)
|
||||
res.status(200).json({ data: normalizeCart(data) })
|
||||
return {
|
||||
data: normalizeCart(data),
|
||||
headers: {
|
||||
'Set-Cookie': getCartCookie(
|
||||
config.cartCookie,
|
||||
cartId,
|
||||
config.cartCookieMaxAge
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default updateItem
|
||||
|
@ -11,7 +11,6 @@ const LIMIT = 12
|
||||
|
||||
// Return current cart info
|
||||
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
|
||||
res,
|
||||
body: { search, categoryId, brandId, sort },
|
||||
config,
|
||||
commerce,
|
||||
@ -73,7 +72,7 @@ const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
|
||||
if (product) products.push(product)
|
||||
})
|
||||
|
||||
res.status(200).json({ data: { products, found } })
|
||||
return { data: { products, found } }
|
||||
}
|
||||
|
||||
export default getProducts
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
||||
import productsEndpoint from '@vercel/commerce/api/endpoints/catalog/products'
|
||||
import type { ProductsSchema } from '../../../../types/product'
|
||||
import type { ProductsSchema } from '@vercel/commerce/types/product'
|
||||
import type { BigcommerceAPI } from '../../..'
|
||||
import getProducts from './get-products'
|
||||
|
||||
|
@ -1,38 +1,47 @@
|
||||
import type { CheckoutEndpoint } from '.'
|
||||
import getCustomerId from '../../utils/get-customer-id'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { uuid } from 'uuidv4'
|
||||
|
||||
const fullCheckout = true
|
||||
|
||||
const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
|
||||
req,
|
||||
res,
|
||||
config,
|
||||
}) => {
|
||||
const { cookies } = req
|
||||
const cartId = cookies[config.cartCookie]
|
||||
const customerToken = cookies[config.customerCookie]
|
||||
const cartId = cookies.get(config.cartCookie)?.value
|
||||
const customerToken = cookies.get(config.customerCookie)?.value
|
||||
|
||||
if (!cartId) {
|
||||
res.redirect('/cart')
|
||||
return
|
||||
return { redirectTo: '/cart' }
|
||||
}
|
||||
const { data } = await config.storeApiFetch(
|
||||
|
||||
const { data } = await config.storeApiFetch<any>(
|
||||
`/v3/carts/${cartId}/redirect_urls`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
|
||||
const customerId =
|
||||
customerToken && (await getCustomerId({ customerToken, config }))
|
||||
|
||||
//if there is a customer create a jwt token
|
||||
if (!customerId) {
|
||||
if (fullCheckout) {
|
||||
res.redirect(data.checkout_url)
|
||||
return
|
||||
return { redirectTo: data.checkout_url }
|
||||
}
|
||||
} 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 payload = {
|
||||
iss: config.storeApiClientId,
|
||||
@ -42,49 +51,51 @@ const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
|
||||
store_hash: config.storeHash,
|
||||
customer_id: customerId,
|
||||
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!, {
|
||||
algorithm: 'HS256',
|
||||
})
|
||||
let checkouturl = `${config.storeUrl}/login/token/${token}`
|
||||
console.log('checkouturl', checkouturl)
|
||||
|
||||
if (fullCheckout) {
|
||||
res.redirect(checkouturl)
|
||||
return
|
||||
return { redirectTo: checkouturl }
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: make the embedded checkout work too!
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Checkout</title>
|
||||
<script src="https://checkout-sdk.bigcommerce.com/v1/loader.js"></script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
checkoutKitLoader.load('checkout-sdk').then(function (service) {
|
||||
service.embedCheckout({
|
||||
containerId: 'checkout',
|
||||
url: '${data.embedded_checkout_url}'
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="checkout"></div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
// const html = `
|
||||
// <!DOCTYPE html>
|
||||
// <html lang="en">
|
||||
// <head>
|
||||
// <meta charset="UTF-8">
|
||||
// <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
// <title>Checkout</title>
|
||||
// <script src="https://checkout-sdk.bigcommerce.com/v1/loader.js"></script>
|
||||
// <script>
|
||||
// window.onload = function() {
|
||||
// checkoutKitLoader.load('checkout-sdk').then(function (service) {
|
||||
// service.embedCheckout({
|
||||
// containerId: 'checkout',
|
||||
// url: '${data.embedded_checkout_url}'
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// </script>
|
||||
// </head>
|
||||
// <body>
|
||||
// <div id="checkout"></div>
|
||||
// </body>
|
||||
// </html>
|
||||
// `
|
||||
|
||||
res.status(200)
|
||||
res.setHeader('Content-Type', 'text/html')
|
||||
res.write(html)
|
||||
res.end()
|
||||
// return new Response(html, {
|
||||
// headers: {
|
||||
// 'Content-Type': 'text/html',
|
||||
// },
|
||||
// })
|
||||
|
||||
return { data: null }
|
||||
}
|
||||
|
||||
export default getCheckout
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
||||
import checkoutEndpoint from '@vercel/commerce/api/endpoints/checkout'
|
||||
import type { CheckoutSchema } from '../../../types/checkout'
|
||||
import type { CheckoutSchema } from '@vercel/commerce/types/checkout'
|
||||
import type { BigcommerceAPI } from '../..'
|
||||
import getCheckout from './get-checkout'
|
||||
|
||||
|
@ -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 { CustomerEndpoint } from '.'
|
||||
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
|
||||
|
||||
export const getLoggedInCustomerQuery = /* GraphQL */ `
|
||||
query getLoggedInCustomer {
|
||||
@ -25,8 +26,8 @@ export const getLoggedInCustomerQuery = /* GraphQL */ `
|
||||
export type Customer = NonNullable<GetLoggedInCustomerQuery['customer']>
|
||||
|
||||
const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] =
|
||||
async ({ req, res, config }) => {
|
||||
const token = req.cookies[config.customerCookie]
|
||||
async ({ req, config }) => {
|
||||
const token = req.cookies.get(config.customerCookie)?.value
|
||||
|
||||
if (token) {
|
||||
const { data } = await config.fetch<GetLoggedInCustomerQuery>(
|
||||
@ -41,16 +42,29 @@ const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] =
|
||||
const { customer } = data
|
||||
|
||||
if (!customer) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Customer not found', code: 'not_found' }],
|
||||
throw new CommerceAPIError('Customer not found', {
|
||||
status: 404,
|
||||
})
|
||||
}
|
||||
|
||||
return res.status(200).json({ data: { customer } })
|
||||
return {
|
||||
data: {
|
||||
customer: {
|
||||
id: String(customer.entityId),
|
||||
firstName: customer.firstName,
|
||||
lastName: customer.lastName,
|
||||
email: customer.email,
|
||||
company: customer.company,
|
||||
phone: customer.phone,
|
||||
notes: customer.notes,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({ data: null })
|
||||
return {
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
|
||||
export default getLoggedInCustomer
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
||||
import customerEndpoint from '@vercel/commerce/api/endpoints/customer'
|
||||
import type { CustomerSchema } from '../../../types/customer'
|
||||
import type { CustomerSchema } from '@vercel/commerce/types/customer'
|
||||
import type { BigcommerceAPI } from '../..'
|
||||
import getLoggedInCustomer from './get-logged-in-customer'
|
||||
|
||||
|
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,6 +1,6 @@
|
||||
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
||||
import loginEndpoint from '@vercel/commerce/api/endpoints/login'
|
||||
import type { LoginSchema } from '../../../types/login'
|
||||
import type { LoginSchema } from '@vercel/commerce/types/login'
|
||||
import type { BigcommerceAPI } from '../..'
|
||||
import login from './login'
|
||||
|
||||
|
@ -1,49 +1,35 @@
|
||||
import { FetcherError } from '@vercel/commerce/utils/errors'
|
||||
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 login: LoginEndpoint['handlers']['login'] = async ({
|
||||
res,
|
||||
body: { email, password },
|
||||
config,
|
||||
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 {
|
||||
await commerce.login({ variables: { email, password }, config, res })
|
||||
const response = await commerce.login({
|
||||
variables: { email, password },
|
||||
config,
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
// Check if the email and password didn't match an existing account
|
||||
if (
|
||||
error instanceof FetcherError &&
|
||||
invalidCredentials.test(error.message)
|
||||
) {
|
||||
return res.status(401).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Cannot find an account that matches the provided credentials',
|
||||
code: 'invalid_credentials',
|
||||
},
|
||||
],
|
||||
})
|
||||
if (error instanceof FetcherError) {
|
||||
throw new CommerceAPIError(
|
||||
invalidCredentials.test(error.message)
|
||||
? 'Cannot find an account that matches the provided credentials'
|
||||
: error.message,
|
||||
{ status: 401 }
|
||||
)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
res.status(200).json({ data: null })
|
||||
}
|
||||
|
||||
export default login
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
||||
import logoutEndpoint from '@vercel/commerce/api/endpoints/logout'
|
||||
import type { LogoutSchema } from '../../../types/logout'
|
||||
import type { LogoutSchema } from '@vercel/commerce/types/logout'
|
||||
import type { BigcommerceAPI } from '../..'
|
||||
import logout from './logout'
|
||||
|
||||
|
@ -2,22 +2,24 @@ import { serialize } from 'cookie'
|
||||
import type { LogoutEndpoint } from '.'
|
||||
|
||||
const logout: LogoutEndpoint['handlers']['logout'] = async ({
|
||||
res,
|
||||
body: { redirectTo },
|
||||
config,
|
||||
}) => {
|
||||
// Remove the cookie
|
||||
res.setHeader(
|
||||
'Set-Cookie',
|
||||
serialize(config.customerCookie, '', { maxAge: -1, path: '/' })
|
||||
)
|
||||
|
||||
// Only allow redirects to a relative URL
|
||||
if (redirectTo?.startsWith('/')) {
|
||||
res.redirect(redirectTo)
|
||||
} else {
|
||||
res.status(200).json({ data: null })
|
||||
const headers = {
|
||||
'Set-Cookie': serialize(config.customerCookie, '', {
|
||||
maxAge: -1,
|
||||
path: '/',
|
||||
}),
|
||||
}
|
||||
|
||||
return redirectTo
|
||||
? {
|
||||
redirectTo,
|
||||
headers,
|
||||
}
|
||||
: {
|
||||
headers,
|
||||
}
|
||||
}
|
||||
|
||||
export default logout
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
||||
import signupEndpoint from '@vercel/commerce/api/endpoints/signup'
|
||||
import type { SignupSchema } from '../../../types/signup'
|
||||
import type { SignupSchema } from '@vercel/commerce/types/signup'
|
||||
import type { BigcommerceAPI } from '../..'
|
||||
import signup from './signup'
|
||||
|
||||
|
@ -1,23 +1,12 @@
|
||||
import { BigcommerceApiError } from '../../utils/errors'
|
||||
import type { SignupEndpoint } from '.'
|
||||
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
|
||||
import { BigcommerceApiError } from '../../utils/errors'
|
||||
|
||||
const signup: SignupEndpoint['handlers']['signup'] = async ({
|
||||
res,
|
||||
body: { firstName, lastName, email, password },
|
||||
config,
|
||||
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 {
|
||||
await config.storeApiFetch('/v3/customers', {
|
||||
method: 'POST',
|
||||
@ -32,31 +21,28 @@ const signup: SignupEndpoint['handlers']['signup'] = async ({
|
||||
},
|
||||
]),
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof BigcommerceApiError && error.status === 422) {
|
||||
const hasEmailError = '0.email' in error.data?.errors
|
||||
|
||||
// If there's an error with the email, it most likely means it's duplicated
|
||||
if (hasEmailError) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
message: 'The email is already in use',
|
||||
code: 'duplicated_email',
|
||||
},
|
||||
],
|
||||
// Login the customer right after creating it
|
||||
const response = await commerce.login({
|
||||
variables: { email, password },
|
||||
config,
|
||||
})
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
// Display all validation errors from BigCommerce in a single error message
|
||||
if (error instanceof BigcommerceApiError && error.status >= 400) {
|
||||
const message = Object.values(error.data.errors).join('<br />')
|
||||
if (message) {
|
||||
throw new CommerceAPIError(message, {
|
||||
status: 400,
|
||||
code: 'invalid_request',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
// Login the customer right after creating it
|
||||
await commerce.login({ variables: { email, password }, res, config })
|
||||
|
||||
res.status(200).json({ data: null })
|
||||
}
|
||||
|
||||
export default signup
|
||||
|
@ -1,66 +1,55 @@
|
||||
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
||||
import { parseWishlistItem } from '../../utils/parse-item'
|
||||
import getCustomerId from '../../utils/get-customer-id'
|
||||
import type { WishlistEndpoint } from '.'
|
||||
import { normalizeWishlist } from '../../../lib/normalize'
|
||||
|
||||
const addItem: WishlistEndpoint['handlers']['addItem'] = async ({
|
||||
res,
|
||||
body: { customerToken, item },
|
||||
config,
|
||||
commerce,
|
||||
}) => {
|
||||
if (!item) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Missing item' }],
|
||||
})
|
||||
const customerId =
|
||||
customerToken && (await getCustomerId({ customerToken, config }))
|
||||
|
||||
if (!customerId) {
|
||||
throw new Error('Invalid request. No CustomerId')
|
||||
}
|
||||
|
||||
try {
|
||||
const customerId =
|
||||
customerToken && (await getCustomerId({ customerToken, config }))
|
||||
let { wishlist } = await commerce.getCustomerWishlist({
|
||||
variables: { customerId },
|
||||
config,
|
||||
})
|
||||
|
||||
if (!customerId) {
|
||||
throw new Error('Invalid request. No CustomerId')
|
||||
}
|
||||
|
||||
let { wishlist } = await commerce.getCustomerWishlist({
|
||||
variables: { customerId },
|
||||
config,
|
||||
if (!wishlist) {
|
||||
// If user has no wishlist, then let's create one with new item
|
||||
const { data } = await config.storeApiFetch<any>('/v3/wishlists', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: 'Next.js Commerce Wishlist',
|
||||
is_public: false,
|
||||
customer_id: Number(customerId),
|
||||
items: [parseWishlistItem(item)],
|
||||
}),
|
||||
})
|
||||
|
||||
if (!wishlist) {
|
||||
// If user has no wishlist, then let's create one with new item
|
||||
const { data } = await config.storeApiFetch('/v3/wishlists', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: 'Next.js Commerce Wishlist',
|
||||
is_public: false,
|
||||
customer_id: Number(customerId),
|
||||
items: [parseWishlistItem(item)],
|
||||
}),
|
||||
})
|
||||
return res.status(200).json(data)
|
||||
return {
|
||||
data: normalizeWishlist(data),
|
||||
}
|
||||
}
|
||||
|
||||
// Existing Wishlist, let's add Item to Wishlist
|
||||
const { data } = await config.storeApiFetch(
|
||||
`/v3/wishlists/${wishlist.id}/items`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
items: [parseWishlistItem(item)],
|
||||
}),
|
||||
}
|
||||
)
|
||||
// Existing Wishlist, let's add Item to Wishlist
|
||||
const { data } = await config.storeApiFetch<any>(
|
||||
`/v3/wishlists/${wishlist.id}/items`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
items: [parseWishlistItem(item)],
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
// Returns Wishlist
|
||||
return res.status(200).json(data)
|
||||
} catch (err: any) {
|
||||
res.status(500).json({
|
||||
data: null,
|
||||
errors: [{ message: err.message }],
|
||||
})
|
||||
// Returns Wishlist
|
||||
return {
|
||||
data: normalizeWishlist(data),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,27 +1,19 @@
|
||||
import type { Wishlist } from '../../../types/wishlist'
|
||||
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
|
||||
import type { WishlistEndpoint } from '.'
|
||||
import getCustomerId from '../../utils/get-customer-id'
|
||||
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
||||
|
||||
// Return wishlist info
|
||||
const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({
|
||||
res,
|
||||
body: { customerToken, includeProducts },
|
||||
config,
|
||||
commerce,
|
||||
}) => {
|
||||
let result: { data?: Wishlist } = {}
|
||||
|
||||
if (customerToken) {
|
||||
const customerId =
|
||||
customerToken && (await getCustomerId({ customerToken, config }))
|
||||
|
||||
if (!customerId) {
|
||||
// If the customerToken is invalid, then this request is too
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Wishlist not found' }],
|
||||
})
|
||||
throw new CommerceAPIError('Wishlist not found', { status: 404 })
|
||||
}
|
||||
|
||||
const { wishlist } = await commerce.getCustomerWishlist({
|
||||
@ -30,10 +22,10 @@ const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({
|
||||
config,
|
||||
})
|
||||
|
||||
result = { data: wishlist }
|
||||
return { data: wishlist }
|
||||
}
|
||||
|
||||
res.status(200).json({ data: result.data ?? null })
|
||||
return { data: null }
|
||||
}
|
||||
|
||||
export default getWishlist
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
|
||||
import wishlistEndpoint from '@vercel/commerce/api/endpoints/wishlist'
|
||||
import type { WishlistSchema } from '../../../types/wishlist'
|
||||
import type { WishlistSchema } from '@vercel/commerce/types/wishlist'
|
||||
import type { BigcommerceAPI } from '../..'
|
||||
import getWishlist from './get-wishlist'
|
||||
import addItem from './add-item'
|
||||
|
@ -1,17 +1,19 @@
|
||||
import type { Wishlist } from '../../../types/wishlist'
|
||||
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
||||
import getCustomerId from '../../utils/get-customer-id'
|
||||
import type { WishlistEndpoint } from '.'
|
||||
import type { BCWishlist } from '../../utils/types'
|
||||
|
||||
import getCustomerId from '../../utils/get-customer-id'
|
||||
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
|
||||
import { normalizeWishlist } from '../../../lib/normalize'
|
||||
|
||||
// Return wishlist info
|
||||
const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({
|
||||
res,
|
||||
body: { customerToken, itemId },
|
||||
config,
|
||||
commerce,
|
||||
}) => {
|
||||
const customerId =
|
||||
customerToken && (await getCustomerId({ customerToken, config }))
|
||||
|
||||
const { wishlist } =
|
||||
(customerId &&
|
||||
(await commerce.getCustomerWishlist({
|
||||
@ -21,19 +23,15 @@ const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({
|
||||
{}
|
||||
|
||||
if (!wishlist || !itemId) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Invalid request' }],
|
||||
})
|
||||
throw new CommerceAPIError('Wishlist not found', { status: 400 })
|
||||
}
|
||||
|
||||
const result = await config.storeApiFetch<{ data: Wishlist } | null>(
|
||||
const result = await config.storeApiFetch<{ data: BCWishlist } | null>(
|
||||
`/v3/wishlists/${wishlist.id}/items/${itemId}`,
|
||||
{ method: 'DELETE' }
|
||||
)
|
||||
const data = result?.data ?? null
|
||||
|
||||
res.status(200).json({ data })
|
||||
return { data: result?.data ? normalizeWishlist(result.data) : null }
|
||||
}
|
||||
|
||||
export default removeItem
|
||||
|
@ -1,4 +1,3 @@
|
||||
import type { RequestInit } from '@vercel/fetch'
|
||||
import {
|
||||
CommerceAPI,
|
||||
CommerceAPIConfig,
|
||||
@ -35,7 +34,14 @@ export interface BigcommerceConfig extends CommerceAPIConfig {
|
||||
storeUrl?: string
|
||||
storeApiClientSecret?: 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
|
||||
|
@ -2,10 +2,13 @@ import type {
|
||||
OperationContext,
|
||||
OperationOptions,
|
||||
} from '@vercel/commerce/api/operations'
|
||||
import type { Page, GetAllPagesOperation } from '../../types/page'
|
||||
import type { GetAllPagesOperation } from '@vercel/commerce/types/page'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||
import { BigcommerceConfig, Provider } from '..'
|
||||
|
||||
import { definitions } from '../definitions/store-content'
|
||||
import { normalizePage } from '../../lib/normalize'
|
||||
|
||||
export default function getAllPagesOperation({
|
||||
commerce,
|
||||
}: OperationContext<Provider>) {
|
||||
@ -33,12 +36,14 @@ export default function getAllPagesOperation({
|
||||
// RecursivePartial forces the method to check for every prop in the data, which is
|
||||
// required in case there's a custom `url`
|
||||
const { data } = await cfg.storeApiFetch<
|
||||
RecursivePartial<{ data: Page[] }>
|
||||
RecursivePartial<{ data: definitions['page_Full'][] }>
|
||||
>('/v3/content/pages')
|
||||
const pages = (data as RecursiveRequired<typeof data>) ?? []
|
||||
|
||||
return {
|
||||
pages: preview ? pages : pages.filter((p) => p.is_visible),
|
||||
pages: preview
|
||||
? pages.map(normalizePage)
|
||||
: pages.filter((p) => p.is_visible).map(normalizePage),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ import type {
|
||||
OperationOptions,
|
||||
} from '@vercel/commerce/api/operations'
|
||||
import type { GetAllProductPathsQuery } from '../../../schema'
|
||||
import type { GetAllProductPathsOperation } from '../../types/product'
|
||||
import type { GetAllProductPathsOperation } from '@vercel/commerce/types/product'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||
import filterEdges from '../utils/filter-edges'
|
||||
import { BigcommerceConfig, Provider } from '..'
|
||||
|
@ -6,7 +6,7 @@ import type {
|
||||
GetAllProductsQuery,
|
||||
GetAllProductsQueryVariables,
|
||||
} from '../../../schema'
|
||||
import type { GetAllProductsOperation } from '../../types/product'
|
||||
import type { GetAllProductsOperation } from '@vercel/commerce/types/product'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||
import filterEdges from '../utils/filter-edges'
|
||||
import setProductLocaleMeta from '../utils/set-product-locale-meta'
|
||||
|
@ -2,13 +2,11 @@ import type {
|
||||
OperationContext,
|
||||
OperationOptions,
|
||||
} from '@vercel/commerce/api/operations'
|
||||
import type {
|
||||
GetCustomerWishlistOperation,
|
||||
Wishlist,
|
||||
} from '../../types/wishlist'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||
import type { GetCustomerWishlistOperation } from '@vercel/commerce/types/wishlist'
|
||||
import type { RecursivePartial, BCWishlist } from '../utils/types'
|
||||
import { BigcommerceConfig, Provider } from '..'
|
||||
import getAllProducts, { ProductEdge } from './get-all-products'
|
||||
import { ProductEdge } from './get-all-products'
|
||||
import { normalizeWishlist } from '../../lib/normalize'
|
||||
|
||||
export default function getCustomerWishlistOperation({
|
||||
commerce,
|
||||
@ -41,18 +39,22 @@ export default function getCustomerWishlistOperation({
|
||||
}): Promise<T['data']> {
|
||||
config = commerce.getConfig(config)
|
||||
|
||||
const { data = [] } = await config.storeApiFetch<
|
||||
RecursivePartial<{ data: Wishlist[] }>
|
||||
>(`/v3/wishlists?customer_id=${variables.customerId}`)
|
||||
const { data = [] } = await config.storeApiFetch<{ data: BCWishlist[] }>(
|
||||
`/v3/wishlists?customer_id=${variables.customerId}`
|
||||
)
|
||||
|
||||
const wishlist = data[0]
|
||||
|
||||
if (includeProducts && wishlist?.items?.length) {
|
||||
const ids = wishlist.items
|
||||
?.map((item) => (item?.product_id ? String(item?.product_id) : null))
|
||||
.filter((id): id is string => !!id)
|
||||
const ids = []
|
||||
|
||||
if (ids?.length) {
|
||||
for (const wishlistItem of wishlist.items) {
|
||||
if (wishlistItem.product_id) {
|
||||
ids.push(String(wishlistItem.product_id))
|
||||
}
|
||||
}
|
||||
|
||||
if (ids.length) {
|
||||
const graphqlData = await commerce.getAllProducts({
|
||||
variables: { first: 50, ids },
|
||||
config,
|
||||
@ -66,7 +68,7 @@ export default function getCustomerWishlistOperation({
|
||||
}, {})
|
||||
// Populate the wishlist items with the graphql products
|
||||
wishlist.items.forEach((item) => {
|
||||
const product = item && productsById[item.product_id!]
|
||||
const product = item && productsById[Number(item.product_id)]
|
||||
if (item && product) {
|
||||
// @ts-ignore Fix this type when the wishlist type is properly defined
|
||||
item.product = product
|
||||
@ -75,7 +77,7 @@ export default function getCustomerWishlistOperation({
|
||||
}
|
||||
}
|
||||
|
||||
return { wishlist: wishlist as RecursiveRequired<typeof wishlist> }
|
||||
return { wishlist: wishlist && normalizeWishlist(wishlist) }
|
||||
}
|
||||
|
||||
return getCustomerWishlist
|
||||
|
@ -2,7 +2,7 @@ import type {
|
||||
OperationContext,
|
||||
OperationOptions,
|
||||
} from '@vercel/commerce/api/operations'
|
||||
import type { GetPageOperation, Page } from '../../types/page'
|
||||
import type { GetPageOperation, Page } from '@vercel/commerce/types/page'
|
||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
||||
import type { BigcommerceConfig, Provider } from '..'
|
||||
import { normalizePage } from '../../lib/normalize'
|
||||
|
@ -2,7 +2,7 @@ import type {
|
||||
OperationContext,
|
||||
OperationOptions,
|
||||
} from '@vercel/commerce/api/operations'
|
||||
import type { GetProductOperation } from '../../types/product'
|
||||
import type { GetProductOperation } from '@vercel/commerce/types/product'
|
||||
import type { GetProductQuery, GetProductQueryVariables } from '../../../schema'
|
||||
import setProductLocaleMeta from '../utils/set-product-locale-meta'
|
||||
import { productInfoFragment } from '../fragments/product'
|
||||
@ -100,7 +100,7 @@ export default function getAllProductPathsOperation({
|
||||
const variables: GetProductQueryVariables = {
|
||||
locale,
|
||||
hasLocale: !!locale,
|
||||
path: slug ? `/${slug}/` : vars.path!,
|
||||
path: slug ? `/${slug}` : vars.path!,
|
||||
}
|
||||
const { data } = await config.fetch<GetProductQuery>(query, { variables })
|
||||
const product = data.site?.route?.node
|
||||
|
@ -2,12 +2,12 @@ import type {
|
||||
OperationContext,
|
||||
OperationOptions,
|
||||
} from '@vercel/commerce/api/operations'
|
||||
import type { GetSiteInfoOperation } from '../../types/site'
|
||||
import type { GetSiteInfoOperation } from '@vercel/commerce/types/site'
|
||||
import type { GetSiteInfoQuery } from '../../../schema'
|
||||
import filterEdges from '../utils/filter-edges'
|
||||
import type { BigcommerceConfig, Provider } from '..'
|
||||
import { categoryTreeItemFragment } from '../fragments/category-tree'
|
||||
import { normalizeCategory } from '../../lib/normalize'
|
||||
import { normalizeBrand, normalizeCategory } from '../../lib/normalize'
|
||||
|
||||
// Get 3 levels of categories
|
||||
export const getSiteInfoQuery = /* GraphQL */ `
|
||||
@ -79,7 +79,7 @@ export default function getSiteInfoOperation({
|
||||
|
||||
return {
|
||||
categories: categories ?? [],
|
||||
brands: filterEdges(brands),
|
||||
brands: filterEdges(brands).map(normalizeBrand),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,10 @@
|
||||
import type { ServerResponse } from 'http'
|
||||
import type {
|
||||
OperationContext,
|
||||
OperationOptions,
|
||||
} from '@vercel/commerce/api/operations'
|
||||
import type { LoginOperation } from '../../types/login'
|
||||
import type { LoginOperation } from '@vercel/commerce/types/login'
|
||||
import type { LoginMutation } from '../../../schema'
|
||||
import type { RecursivePartial } from '../utils/types'
|
||||
import concatHeader from '../utils/concat-cookie'
|
||||
import type { BigcommerceConfig, Provider } from '..'
|
||||
|
||||
export const loginMutation = /* GraphQL */ `
|
||||
@ -23,26 +21,23 @@ export default function loginOperation({
|
||||
async function login<T extends LoginOperation>(opts: {
|
||||
variables: T['variables']
|
||||
config?: BigcommerceConfig
|
||||
res: ServerResponse
|
||||
}): Promise<T['data']>
|
||||
|
||||
async function login<T extends LoginOperation>(
|
||||
opts: {
|
||||
variables: T['variables']
|
||||
config?: BigcommerceConfig
|
||||
res: ServerResponse
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
|
||||
async function login<T extends LoginOperation>({
|
||||
query = loginMutation,
|
||||
variables,
|
||||
res: response,
|
||||
config,
|
||||
}: {
|
||||
query?: string
|
||||
variables: T['variables']
|
||||
res: ServerResponse
|
||||
|
||||
config?: BigcommerceConfig
|
||||
}): Promise<T['data']> {
|
||||
config = commerce.getConfig(config)
|
||||
@ -51,6 +46,9 @@ export default function loginOperation({
|
||||
query,
|
||||
{ variables }
|
||||
)
|
||||
|
||||
const headers = new Headers()
|
||||
|
||||
// Bigcommerce returns a Set-Cookie header with the auth cookie
|
||||
let cookie = res.headers.get('Set-Cookie')
|
||||
|
||||
@ -64,14 +62,13 @@ export default function loginOperation({
|
||||
cookie = cookie.replace(/; SameSite=none/gi, '; SameSite=lax')
|
||||
}
|
||||
|
||||
response.setHeader(
|
||||
'Set-Cookie',
|
||||
concatHeader(response.getHeader('Set-Cookie'), cookie)!
|
||||
)
|
||||
headers.set('Set-Cookie', cookie)
|
||||
}
|
||||
|
||||
return {
|
||||
result: data.login?.result,
|
||||
headers,
|
||||
status: res.status,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
if (!val) return prev
|
||||
|
@ -1,5 +1,3 @@
|
||||
import type { Response } from '@vercel/fetch'
|
||||
|
||||
// Used for GraphQL errors
|
||||
export class BigcommerceGraphQLError extends Error {}
|
||||
|
||||
|
@ -1,22 +1,26 @@
|
||||
import { FetcherError } from '@vercel/commerce/utils/errors'
|
||||
import type { GraphQLFetcher } from '@vercel/commerce/api'
|
||||
import type { FetchOptions, GraphQLFetcher } from '@vercel/commerce/api'
|
||||
import type { BigcommerceConfig } from '../index'
|
||||
import fetch from './fetch'
|
||||
|
||||
const fetchGraphqlApi: (getConfig: () => BigcommerceConfig) => GraphQLFetcher =
|
||||
(getConfig) =>
|
||||
async (query: string, { variables, preview } = {}, fetchOptions) => {
|
||||
async (
|
||||
query: string,
|
||||
{ variables, preview } = {},
|
||||
options?: FetchOptions
|
||||
): Promise<any> => {
|
||||
// log.warn(query)
|
||||
const config = getConfig()
|
||||
|
||||
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
|
||||
...fetchOptions,
|
||||
method: 'POST',
|
||||
method: options?.method || 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.apiToken}`,
|
||||
...fetchOptions?.headers,
|
||||
...options?.headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...options?.body,
|
||||
query,
|
||||
variables,
|
||||
}),
|
||||
|
@ -1,11 +1,16 @@
|
||||
import type { FetchOptions, Response } from '@vercel/fetch'
|
||||
import type { BigcommerceConfig } from '../index'
|
||||
import { BigcommerceApiError, BigcommerceNetworkError } from './errors'
|
||||
import fetch from './fetch'
|
||||
|
||||
const fetchStoreApi =
|
||||
<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()
|
||||
let res: Response
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { WishlistItemBody } from '../../types/wishlist'
|
||||
import type { CartItemBody, OptionSelections } from '../../types/cart'
|
||||
import type { WishlistItemBody } from '@vercel/commerce/types/wishlist'
|
||||
import type { CartItemBody, SelectedOption } from '@vercel/commerce/types/cart'
|
||||
|
||||
type BCWishlistItemBody = {
|
||||
product_id: number
|
||||
@ -10,7 +10,7 @@ type BCCartItemBody = {
|
||||
product_id: number
|
||||
variant_id: number
|
||||
quantity?: number
|
||||
option_selections?: OptionSelections[]
|
||||
option_selections?: SelectedOption[]
|
||||
}
|
||||
|
||||
export const parseWishlistItem = (
|
||||
@ -24,5 +24,5 @@ export const parseCartItem = (item: CartItemBody): BCCartItemBody => ({
|
||||
quantity: item.quantity,
|
||||
product_id: Number(item.productId),
|
||||
variant_id: Number(item.variantId),
|
||||
option_selections: item.optionSelections,
|
||||
option_selections: item.optionsSelected,
|
||||
})
|
||||
|
@ -5,3 +5,15 @@ export type RecursivePartial<T> = {
|
||||
export type RecursiveRequired<T> = {
|
||||
[P in keyof T]-?: RecursiveRequired<T[P]>
|
||||
}
|
||||
|
||||
export interface BCWishlist {
|
||||
id: number
|
||||
items: {
|
||||
id: number
|
||||
customer_id: number
|
||||
is_public: boolean
|
||||
product_id: number
|
||||
variant_id: number
|
||||
}[]
|
||||
token: string
|
||||
}
|
||||
|
@ -2,14 +2,14 @@ import { useCallback } from 'react'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import { CommerceError } from '@vercel/commerce/utils/errors'
|
||||
import useLogin, { UseLogin } from '@vercel/commerce/auth/use-login'
|
||||
import type { LoginHook } from '../types/login'
|
||||
import type { LoginHook } from '@vercel/commerce/types/login'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
export default useLogin as UseLogin<typeof handler>
|
||||
|
||||
export const handler: MutationHook<LoginHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/login',
|
||||
url: '/api/commerce/login',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({ input: { email, password }, options, fetch }) {
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import useLogout, { UseLogout } from '@vercel/commerce/auth/use-logout'
|
||||
import type { LogoutHook } from '../types/logout'
|
||||
import type { LogoutHook } from '@vercel/commerce/types/logout'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
export default useLogout as UseLogout<typeof handler>
|
||||
|
||||
export const handler: MutationHook<LogoutHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/logout',
|
||||
url: '/api/commerce/logout',
|
||||
method: 'GET',
|
||||
},
|
||||
useHook:
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import { CommerceError } from '@vercel/commerce/utils/errors'
|
||||
import useSignup, { UseSignup } from '@vercel/commerce/auth/use-signup'
|
||||
import type { SignupHook } from '../types/signup'
|
||||
import useSignup, { type UseSignup } from '@vercel/commerce/auth/use-signup'
|
||||
import type { SignupHook } from '@vercel/commerce/types/signup'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
export default useSignup as UseSignup<typeof handler>
|
||||
|
||||
export const handler: MutationHook<SignupHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/signup',
|
||||
url: '/api/commerce/signup',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({
|
||||
|
@ -9,7 +9,7 @@ export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<AddItemHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/cart',
|
||||
url: '/api/commerce/cart',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({ input: item, options, fetch }) {
|
||||
@ -33,7 +33,6 @@ export const handler: MutationHook<AddItemHook> = {
|
||||
({ fetch }) =>
|
||||
() => {
|
||||
const { mutate } = useCart()
|
||||
|
||||
return useCallback(
|
||||
async function addItem(input) {
|
||||
const data = await fetch({ input })
|
||||
|
@ -7,7 +7,7 @@ export default useCart as UseCart<typeof handler>
|
||||
|
||||
export const handler: SWRHook<GetCartHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/cart',
|
||||
url: '/api/commerce/cart',
|
||||
method: 'GET',
|
||||
},
|
||||
useHook:
|
||||
|
@ -4,8 +4,14 @@ import type {
|
||||
HookFetcherContext,
|
||||
} from '@vercel/commerce/utils/types'
|
||||
import { ValidationError } from '@vercel/commerce/utils/errors'
|
||||
import useRemoveItem, { UseRemoveItem } from '@vercel/commerce/cart/use-remove-item'
|
||||
import type { Cart, LineItem, RemoveItemHook } from '@vercel/commerce/types/cart'
|
||||
import useRemoveItem, {
|
||||
UseRemoveItem,
|
||||
} from '@vercel/commerce/cart/use-remove-item'
|
||||
import type {
|
||||
Cart,
|
||||
LineItem,
|
||||
RemoveItemHook,
|
||||
} from '@vercel/commerce/types/cart'
|
||||
import useCart from './use-cart'
|
||||
|
||||
export type RemoveItemFn<T = any> = T extends LineItem
|
||||
@ -20,7 +26,7 @@ export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||
|
||||
export const handler = {
|
||||
fetchOptions: {
|
||||
url: '/api/cart',
|
||||
url: '/api/commerce/cart',
|
||||
method: 'DELETE',
|
||||
},
|
||||
async fetcher({
|
||||
|
@ -5,7 +5,9 @@ import type {
|
||||
HookFetcherContext,
|
||||
} from '@vercel/commerce/utils/types'
|
||||
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 { handler as removeItemHandler } from './use-remove-item'
|
||||
import useCart from './use-cart'
|
||||
@ -18,7 +20,7 @@ export default useUpdateItem as UseUpdateItem<typeof handler>
|
||||
|
||||
export const handler = {
|
||||
fetchOptions: {
|
||||
url: '/api/cart',
|
||||
url: '/api/commerce/cart',
|
||||
method: 'PUT',
|
||||
},
|
||||
async fetcher({
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { SWRHook } from '@vercel/commerce/utils/types'
|
||||
import useCustomer, { UseCustomer } from '@vercel/commerce/customer/use-customer'
|
||||
import type { CustomerHook } from '../types/customer'
|
||||
import type { SWRHook } from '@vercel/commerce/utils/types'
|
||||
import useCustomer, {
|
||||
type UseCustomer,
|
||||
} from '@vercel/commerce/customer/use-customer'
|
||||
import type { CustomerHook } from '@vercel/commerce/types/customer'
|
||||
|
||||
export default useCustomer as UseCustomer<typeof handler>
|
||||
|
||||
export const handler: SWRHook<CustomerHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/customer',
|
||||
url: '/api/commerce/customer',
|
||||
method: 'GET',
|
||||
},
|
||||
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'
|
||||
|
||||
export { bigcommerceProvider }
|
||||
|
@ -1,10 +1,14 @@
|
||||
import type { Product } from '../types/product'
|
||||
import type { Cart, BigcommerceCart, LineItem } from '../types/cart'
|
||||
import type { Page } from '../types/page'
|
||||
import type { BCCategory, Category } from '../types/site'
|
||||
import { definitions } from '../api/definitions/store-content'
|
||||
import update from './immutability'
|
||||
import type { Page } from '@vercel/commerce/types/page'
|
||||
import type { Product } from '@vercel/commerce/types/product'
|
||||
import type { Cart, LineItem } from '@vercel/commerce/types/cart'
|
||||
import type { Category, Brand } from '@vercel/commerce/types/site'
|
||||
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 type { BCWishlist } from '../api/utils/types'
|
||||
|
||||
import getSlug from './get-slug'
|
||||
import { Wishlist } from '@vercel/commerce/types/wishlist'
|
||||
|
||||
function normalizeProductOption(productOption: any) {
|
||||
const {
|
||||
@ -12,61 +16,50 @@ function normalizeProductOption(productOption: any) {
|
||||
} = productOption
|
||||
|
||||
return {
|
||||
id: entityId,
|
||||
id: String(entityId),
|
||||
values: edges?.map(({ node }: any) => node),
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeProduct(productNode: any): Product {
|
||||
export function normalizeProduct(productNode: ProductNode): Product {
|
||||
const {
|
||||
entityId: id,
|
||||
productOptions,
|
||||
prices,
|
||||
path,
|
||||
id: _,
|
||||
options: _0,
|
||||
images,
|
||||
variants,
|
||||
} = productNode
|
||||
|
||||
return update(productNode, {
|
||||
id: { $set: String(id) },
|
||||
images: {
|
||||
$apply: ({ edges }: any) =>
|
||||
edges?.map(({ node: { urlOriginal, altText, ...rest } }: any) => ({
|
||||
url: urlOriginal,
|
||||
alt: altText,
|
||||
...rest,
|
||||
})),
|
||||
},
|
||||
variants: {
|
||||
$apply: ({ edges }: any) =>
|
||||
edges?.map(({ node: { entityId, productOptions, ...rest } }: any) => ({
|
||||
id: entityId,
|
||||
return {
|
||||
id: String(id),
|
||||
name: productNode.name,
|
||||
description: productNode.description,
|
||||
images:
|
||||
images.edges?.map(({ node: { urlOriginal, altText, ...rest } }: any) => ({
|
||||
url: urlOriginal,
|
||||
alt: altText,
|
||||
...rest,
|
||||
})) || [],
|
||||
path: `/${getSlug(path)}`,
|
||||
variants:
|
||||
variants.edges?.map(
|
||||
({ node: { entityId, productOptions, ...rest } }: any) => ({
|
||||
id: String(entityId),
|
||||
options: productOptions?.edges
|
||||
? productOptions.edges.map(normalizeProductOption)
|
||||
: [],
|
||||
...rest,
|
||||
})),
|
||||
},
|
||||
options: {
|
||||
$set: productOptions.edges
|
||||
? productOptions?.edges.map(normalizeProductOption)
|
||||
: [],
|
||||
},
|
||||
brand: {
|
||||
$apply: (brand: any) => (brand?.entityId ? brand?.entityId : null),
|
||||
},
|
||||
slug: {
|
||||
$set: path?.replace(/^\/+|\/+$/g, ''),
|
||||
},
|
||||
})
|
||||
) || [],
|
||||
options: productOptions?.edges?.map(normalizeProductOption) || [],
|
||||
slug: path?.replace(/^\/+|\/+$/g, ''),
|
||||
price: {
|
||||
$set: {
|
||||
value: prices?.price.value,
|
||||
currencyCode: prices?.price.currencyCode,
|
||||
},
|
||||
value: prices?.price.value,
|
||||
currencyCode: prices?.price.currencyCode,
|
||||
},
|
||||
$unset: ['entityId'],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizePage(page: definitions['page_Full']): Page {
|
||||
@ -75,7 +68,8 @@ export function normalizePage(page: definitions['page_Full']): Page {
|
||||
name: page.name,
|
||||
is_visible: page.is_visible,
|
||||
sort_order: page.sort_order,
|
||||
body: page.body,
|
||||
body: page.body ?? '',
|
||||
url: page.url,
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,7 +113,7 @@ function normalizeLineItem(item: any): LineItem {
|
||||
listPrice: item.list_price,
|
||||
},
|
||||
options: item.options,
|
||||
path: item.url.split('/')[3],
|
||||
path: `/${item.url.split('/')[3]}`,
|
||||
discounts: item.discounts.map((discount: any) => ({
|
||||
value: discount.discounted_amount,
|
||||
})),
|
||||
@ -134,3 +128,27 @@ export function normalizeCategory(category: BCCategory): Category {
|
||||
path: category.path,
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeBrand(brand: BCBrand): Brand {
|
||||
const path = brand.node.path.replace('/brands/', '')
|
||||
const slug = getSlug(path)
|
||||
return {
|
||||
id: `${brand.node.entityId}`,
|
||||
name: brand.node.name,
|
||||
slug,
|
||||
path: `/${slug}`,
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeWishlist(wishlist: BCWishlist): Wishlist {
|
||||
return {
|
||||
id: String(wishlist.id),
|
||||
token: wishlist.token,
|
||||
items: wishlist.items.map((item: any) => ({
|
||||
id: String(item.id),
|
||||
productId: String(item.product_id),
|
||||
variantId: String(item.variant_id),
|
||||
product: item.product,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,20 @@
|
||||
import { SWRHook } from '@vercel/commerce/utils/types'
|
||||
import useSearch, { UseSearch } from '@vercel/commerce/product/use-search'
|
||||
import type { SearchProductsHook } from '../types/product'
|
||||
import type { SearchProductsHook } from '@vercel/commerce/types/product'
|
||||
|
||||
export default useSearch as UseSearch<typeof handler>
|
||||
|
||||
export type SearchProductsInput = {
|
||||
search?: string
|
||||
categoryId?: number | string
|
||||
brandId?: number
|
||||
categoryId?: string
|
||||
brandId?: string
|
||||
sort?: string
|
||||
locale?: string
|
||||
}
|
||||
|
||||
export const handler: SWRHook<SearchProductsHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/catalog/products',
|
||||
url: '/api/commerce/catalog/products',
|
||||
method: 'GET',
|
||||
},
|
||||
fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) {
|
||||
@ -24,7 +24,7 @@ export const handler: SWRHook<SearchProductsHook> = {
|
||||
if (search) url.searchParams.set('search', search)
|
||||
if (Number.isInteger(Number(categoryId)))
|
||||
url.searchParams.set('categoryId', String(categoryId))
|
||||
if (Number.isInteger(brandId))
|
||||
if (Number.isInteger(Number(brandId)))
|
||||
url.searchParams.set('brandId', String(brandId))
|
||||
if (sort) url.searchParams.set('sort', sort)
|
||||
|
||||
|
32
packages/bigcommerce/src/types.ts
Normal file
32
packages/bigcommerce/src/types.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import type { GetSiteInfoQuery } from '../schema'
|
||||
|
||||
export type BCCategory = NonNullable<
|
||||
GetSiteInfoQuery['site']['categoryTree']
|
||||
>[0]
|
||||
|
||||
export type BCBrand = NonNullable<
|
||||
NonNullable<GetSiteInfoQuery['site']['brands']['edges']>[0]
|
||||
>
|
||||
|
||||
// TODO: this type should match:
|
||||
// https://developer.bigcommerce.com/api-reference/cart-checkout/server-server-cart-api/cart/getacart#responses
|
||||
export type BigcommerceCart = {
|
||||
id: string
|
||||
parent_id?: string
|
||||
customer_id: number
|
||||
email: string
|
||||
currency: { code: string }
|
||||
tax_included: boolean
|
||||
base_amount: number
|
||||
discount_amount: number
|
||||
cart_amount: number
|
||||
line_items: {
|
||||
custom_items: any[]
|
||||
digital_items: any[]
|
||||
gift_certificates: any[]
|
||||
physical_items: any[]
|
||||
}
|
||||
created_time: string
|
||||
discounts?: { id: number; discounted_amount: number }[]
|
||||
// TODO: add missing fields
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
import * as Core from '@vercel/commerce/types/cart'
|
||||
|
||||
export * from '@vercel/commerce/types/cart'
|
||||
|
||||
// TODO: this type should match:
|
||||
// https://developer.bigcommerce.com/api-reference/cart-checkout/server-server-cart-api/cart/getacart#responses
|
||||
export type BigcommerceCart = {
|
||||
id: string
|
||||
parent_id?: string
|
||||
customer_id: number
|
||||
email: string
|
||||
currency: { code: string }
|
||||
tax_included: boolean
|
||||
base_amount: number
|
||||
discount_amount: number
|
||||
cart_amount: number
|
||||
line_items: {
|
||||
custom_items: any[]
|
||||
digital_items: any[]
|
||||
gift_certificates: any[]
|
||||
physical_items: any[]
|
||||
}
|
||||
created_time: string
|
||||
discounts?: { id: number; discounted_amount: number }[]
|
||||
// TODO: add missing fields
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend core cart types
|
||||
*/
|
||||
|
||||
export type Cart = Core.Cart & {
|
||||
lineItems: Core.LineItem[]
|
||||
}
|
||||
|
||||
export type OptionSelections = {
|
||||
option_id: number
|
||||
option_value: number | string
|
||||
}
|
||||
|
||||
export type CartItemBody = Core.CartItemBody & {
|
||||
productId: string // The product id is always required for BC
|
||||
optionSelections?: OptionSelections[]
|
||||
}
|
||||
|
||||
export type CartTypes = {
|
||||
cart: Cart
|
||||
item: Core.LineItem
|
||||
itemBody: CartItemBody
|
||||
}
|
||||
|
||||
export type CartHooks = Core.CartHooks<CartTypes>
|
||||
|
||||
export type GetCartHook = CartHooks['getCart']
|
||||
export type AddItemHook = CartHooks['addItem']
|
||||
export type UpdateItemHook = CartHooks['updateItem']
|
||||
export type RemoveItemHook = CartHooks['removeItem']
|
||||
|
||||
export type CartSchema = Core.CartSchema<CartTypes>
|
||||
|
||||
export type CartHandlers = Core.CartHandlers<CartTypes>
|
||||
|
||||
export type GetCartHandler = CartHandlers['getCart']
|
||||
export type AddItemHandler = CartHandlers['addItem']
|
||||
export type UpdateItemHandler = CartHandlers['updateItem']
|
||||
export type RemoveItemHandler = CartHandlers['removeItem']
|
@ -1 +0,0 @@
|
||||
export * from '@vercel/commerce/types/checkout'
|
@ -1 +0,0 @@
|
||||
export * from '@vercel/commerce/types/common'
|
@ -1,5 +0,0 @@
|
||||
import * as Core from '@vercel/commerce/types/customer'
|
||||
|
||||
export * from '@vercel/commerce/types/customer'
|
||||
|
||||
export type CustomerSchema = Core.CustomerSchema
|
@ -1,25 +0,0 @@
|
||||
import * as Cart from './cart'
|
||||
import * as Checkout from './checkout'
|
||||
import * as Common from './common'
|
||||
import * as Customer from './customer'
|
||||
import * as Login from './login'
|
||||
import * as Logout from './logout'
|
||||
import * as Page from './page'
|
||||
import * as Product from './product'
|
||||
import * as Signup from './signup'
|
||||
import * as Site from './site'
|
||||
import * as Wishlist from './wishlist'
|
||||
|
||||
export type {
|
||||
Cart,
|
||||
Checkout,
|
||||
Common,
|
||||
Customer,
|
||||
Login,
|
||||
Logout,
|
||||
Page,
|
||||
Product,
|
||||
Signup,
|
||||
Site,
|
||||
Wishlist,
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import * as Core from '@vercel/commerce/types/login'
|
||||
import type { LoginMutationVariables } from '../../schema'
|
||||
|
||||
export * from '@vercel/commerce/types/login'
|
||||
|
||||
export type LoginOperation = Core.LoginOperation & {
|
||||
variables: LoginMutationVariables
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from '@vercel/commerce/types/logout'
|
@ -1,11 +0,0 @@
|
||||
import * as Core from '@vercel/commerce/types/page'
|
||||
export * from '@vercel/commerce/types/page'
|
||||
|
||||
export type Page = Core.Page
|
||||
|
||||
export type PageTypes = {
|
||||
page: Page
|
||||
}
|
||||
|
||||
export type GetAllPagesOperation = Core.GetAllPagesOperation<PageTypes>
|
||||
export type GetPageOperation = Core.GetPageOperation<PageTypes>
|
@ -1 +0,0 @@
|
||||
export * from '@vercel/commerce/types/product'
|
@ -1 +0,0 @@
|
||||
export * from '@vercel/commerce/types/signup'
|
@ -1,19 +0,0 @@
|
||||
import * as Core from '@vercel/commerce/types/site'
|
||||
import type { GetSiteInfoQuery, GetSiteInfoQueryVariables } from '../../schema'
|
||||
|
||||
export * from '@vercel/commerce/types/site'
|
||||
|
||||
export type BCCategory = NonNullable<
|
||||
GetSiteInfoQuery['site']['categoryTree']
|
||||
>[0]
|
||||
|
||||
export type Brand = NonNullable<
|
||||
NonNullable<GetSiteInfoQuery['site']['brands']['edges']>[0]
|
||||
>
|
||||
|
||||
export type SiteTypes = {
|
||||
category: Core.Category
|
||||
brand: Brand
|
||||
}
|
||||
|
||||
export type GetSiteInfoOperation = Core.GetSiteInfoOperation<SiteTypes>
|
@ -1,24 +0,0 @@
|
||||
import * as Core from '@vercel/commerce/types/wishlist'
|
||||
import { definitions } from '../api/definitions/wishlist'
|
||||
import type { ProductEdge } from '../api/operations/get-all-products'
|
||||
|
||||
export * from '@vercel/commerce/types/wishlist'
|
||||
|
||||
export type WishlistItem = NonNullable<
|
||||
definitions['wishlist_Full']['items']
|
||||
>[0] & {
|
||||
product?: ProductEdge['node']
|
||||
}
|
||||
|
||||
export type Wishlist = Omit<definitions['wishlist_Full'], 'items'> & {
|
||||
items?: WishlistItem[]
|
||||
}
|
||||
|
||||
export type WishlistTypes = {
|
||||
wishlist: Wishlist
|
||||
itemBody: Core.WishlistItemBody
|
||||
}
|
||||
|
||||
export type WishlistSchema = Core.WishlistSchema<WishlistTypes>
|
||||
export type GetCustomerWishlistOperation =
|
||||
Core.GetCustomerWishlistOperation<WishlistTypes>
|
@ -1,8 +1,10 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import { CommerceError } from '@vercel/commerce/utils/errors'
|
||||
import useAddItem, { UseAddItem } from '@vercel/commerce/wishlist/use-add-item'
|
||||
import type { AddItemHook } from '../types/wishlist'
|
||||
import useAddItem, {
|
||||
type UseAddItem,
|
||||
} from '@vercel/commerce/wishlist/use-add-item'
|
||||
import type { AddItemHook } from '@vercel/commerce/types/wishlist'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import useWishlist from './use-wishlist'
|
||||
|
||||
@ -10,7 +12,7 @@ export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<AddItemHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/wishlist',
|
||||
url: '/api/commerce/wishlist',
|
||||
method: 'POST',
|
||||
},
|
||||
useHook:
|
||||
|
@ -2,9 +2,9 @@ import { useCallback } from 'react'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import { CommerceError } from '@vercel/commerce/utils/errors'
|
||||
import useRemoveItem, {
|
||||
UseRemoveItem,
|
||||
type UseRemoveItem,
|
||||
} from '@vercel/commerce/wishlist/use-remove-item'
|
||||
import type { RemoveItemHook } from '../types/wishlist'
|
||||
import type { RemoveItemHook } from '@vercel/commerce/types/wishlist'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import useWishlist from './use-wishlist'
|
||||
|
||||
@ -12,7 +12,7 @@ export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<RemoveItemHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/wishlist',
|
||||
url: '/api/commerce/wishlist',
|
||||
method: 'DELETE',
|
||||
},
|
||||
useHook:
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { useMemo } from 'react'
|
||||
import { SWRHook } from '@vercel/commerce/utils/types'
|
||||
import useWishlist, {
|
||||
UseWishlist,
|
||||
type UseWishlist,
|
||||
} from '@vercel/commerce/wishlist/use-wishlist'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
import type { GetWishlistHook } from '../types/wishlist'
|
||||
import type { GetWishlistHook } from '@vercel/commerce/types/wishlist'
|
||||
|
||||
export default useWishlist as UseWishlist<typeof handler>
|
||||
export const handler: SWRHook<GetWishlistHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/wishlist',
|
||||
url: '/api/commerce/wishlist',
|
||||
method: 'GET',
|
||||
},
|
||||
async fetcher({ input: { customerId, includeProducts }, options, fetch }) {
|
||||
@ -32,7 +32,7 @@ export const handler: SWRHook<GetWishlistHook> = {
|
||||
const { data: customer } = useCustomer()
|
||||
const response = useData({
|
||||
input: [
|
||||
['customerId', customer?.entityId],
|
||||
['customerId', customer?.id],
|
||||
['includeProducts', input?.includeProducts],
|
||||
],
|
||||
swrOptions: {
|
||||
|
@ -70,7 +70,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:
|
||||
|
||||
```tsx
|
||||
import { getCommerceProvider, useCommerce as useCoreCommerce } from '@vercel/commerce'
|
||||
import {
|
||||
getCommerceProvider,
|
||||
useCommerce as useCoreCommerce,
|
||||
} from '@vercel/commerce'
|
||||
import { bigcommerceProvider, BigcommerceProvider } from './provider'
|
||||
|
||||
export { bigcommerceProvider }
|
||||
@ -136,7 +139,7 @@ export default useCart as UseCart<typeof handler>
|
||||
|
||||
export const handler: SWRHook<GetCartHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/cart',
|
||||
url: '/api/commerce/cart',
|
||||
method: 'GET',
|
||||
},
|
||||
useHook:
|
||||
@ -176,7 +179,7 @@ export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<AddItemHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/cart',
|
||||
url: '/api/commerce/cart',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({ input: item, options, fetch }) {
|
||||
@ -214,25 +217,26 @@ export const handler: MutationHook<AddItemHook> = {
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
**Status**
|
||||
|
||||
* [ ] CommerceProvider
|
||||
* [ ] Schema & TS types
|
||||
* [ ] API Operations - Get all collections
|
||||
* [ ] API Operations - Get all pages
|
||||
* [ ] API Operations - Get all products
|
||||
* [ ] API Operations - Get page
|
||||
* [ ] API Operations - Get product
|
||||
* [ ] API Operations - Get Shop Info (categories and vendors working — `vendors` query still a WIP PR on Reaction)
|
||||
* [ ] Hook - Add Item
|
||||
* [ ] Hook - Remove Item
|
||||
* [ ] Hook - Update Item
|
||||
* [ ] 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)
|
||||
* [ ] Customer information
|
||||
* [ ] Product attributes - Size, Colors
|
||||
* [ ] Custom checkout
|
||||
* [ ] Typing (in progress)
|
||||
* [ ] Tests
|
||||
- [ ] CommerceProvider
|
||||
- [ ] Schema & TS types
|
||||
- [ ] API Operations - Get all collections
|
||||
- [ ] API Operations - Get all pages
|
||||
- [ ] API Operations - Get all products
|
||||
- [ ] API Operations - Get page
|
||||
- [ ] API Operations - Get product
|
||||
- [ ] API Operations - Get Shop Info (categories and vendors working — `vendors` query still a WIP PR on Reaction)
|
||||
- [ ] Hook - Add Item
|
||||
- [ ] Hook - Remove Item
|
||||
- [ ] Hook - Update Item
|
||||
- [ ] 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)
|
||||
- [ ] Customer information
|
||||
- [ ] Product attributes - Size, Colors
|
||||
- [ ] Custom checkout
|
||||
- [ ] Typing (in progress)
|
||||
- [ ] Tests
|
||||
|
@ -47,16 +47,17 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/fetch": "^6.1.1",
|
||||
"@vercel/edge": "^0.0.4",
|
||||
"deepmerge": "^4.2.2",
|
||||
"import-cwd": "^3.0.0",
|
||||
"js-cookie": "^3.0.1",
|
||||
"swr": "^1.2.0"
|
||||
"swr": "^1.3.0",
|
||||
"zod": "^3.19.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": "^12",
|
||||
"react": "^17",
|
||||
"react-dom": "^17"
|
||||
"next": "^13",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@taskr/clear": "^1.1.0",
|
||||
@ -64,15 +65,16 @@
|
||||
"@taskr/watch": "^1.1.0",
|
||||
"@types/js-cookie": "^3.0.1",
|
||||
"@types/node": "^17.0.8",
|
||||
"@types/react": "^17.0.38",
|
||||
"@types/node-fetch": "2.6.2",
|
||||
"@types/react": "^18.0.14",
|
||||
"lint-staged": "^12.1.7",
|
||||
"next": "^12.0.8",
|
||||
"next": "^13.0.6",
|
||||
"prettier": "^2.5.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"taskr": "^1.1.0",
|
||||
"taskr-swc": "^0.0.1",
|
||||
"typescript": "^4.5.4"
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.{js,jsx,ts,tsx,json}": [
|
||||
|
@ -1,60 +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 { CartSchema } from '../../types/cart'
|
||||
|
||||
const cartEndpoint: GetAPISchema<any, CartSchema<any>>['endpoint']['handler'] =
|
||||
async (ctx) => {
|
||||
const { req, res, handlers, config } = ctx
|
||||
import { parse, getInput } from '../utils'
|
||||
import validateHandlers from '../utils/validate-handlers'
|
||||
|
||||
if (
|
||||
!isAllowedOperation(req, res, {
|
||||
GET: handlers['getCart'],
|
||||
POST: handlers['addItem'],
|
||||
PUT: handlers['updateItem'],
|
||||
DELETE: handlers['removeItem'],
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
import {
|
||||
getCartBodySchema,
|
||||
addItemBodySchema,
|
||||
updateItemBodySchema,
|
||||
removeItemBodySchema,
|
||||
cartSchema,
|
||||
} from '../../schemas/cart'
|
||||
|
||||
const { cookies } = req
|
||||
const cartId = cookies[config.cartCookie]
|
||||
const cartEndpoint: GetAPISchema<
|
||||
any,
|
||||
CartSchema
|
||||
>['endpoint']['handler'] = async (ctx) => {
|
||||
const { req, handlers, config } = ctx
|
||||
|
||||
try {
|
||||
// Return current cart info
|
||||
if (req.method === 'GET') {
|
||||
const body = { cartId }
|
||||
return await handlers['getCart']({ ...ctx, body })
|
||||
}
|
||||
validateHandlers(req, {
|
||||
GET: handlers['getCart'],
|
||||
POST: handlers['addItem'],
|
||||
PUT: handlers['updateItem'],
|
||||
DELETE: handlers['removeItem'],
|
||||
})
|
||||
|
||||
// Create or add an item to the cart
|
||||
if (req.method === 'POST') {
|
||||
const body = { ...req.body, cartId }
|
||||
return await handlers['addItem']({ ...ctx, body })
|
||||
}
|
||||
const input = await getInput(req)
|
||||
|
||||
// Update item in cart
|
||||
if (req.method === 'PUT') {
|
||||
const body = { ...req.body, cartId }
|
||||
return await handlers['updateItem']({ ...ctx, body })
|
||||
}
|
||||
let output
|
||||
const { cookies } = req
|
||||
const cartId = cookies.get(config.cartCookie)?.value
|
||||
|
||||
// Remove an item from the cart
|
||||
if (req.method === 'DELETE') {
|
||||
const body = { ...req.body, cartId }
|
||||
return await handlers['removeItem']({ ...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 }] })
|
||||
}
|
||||
// Return current cart info
|
||||
if (req.method === 'GET') {
|
||||
const body = getCartBodySchema.parse({ cartId })
|
||||
output = await handlers['getCart']({ ...ctx, body })
|
||||
}
|
||||
|
||||
// Create or add an item to the cart
|
||||
if (req.method === 'POST') {
|
||||
const body = addItemBodySchema.parse({ ...input, cartId })
|
||||
if (!body.item.quantity) {
|
||||
body.item.quantity = 1
|
||||
}
|
||||
output = await handlers['addItem']({ ...ctx, body })
|
||||
}
|
||||
|
||||
// Update item in cart
|
||||
if (req.method === 'PUT') {
|
||||
const body = updateItemBodySchema.parse({ ...input, cartId })
|
||||
output = await handlers['updateItem']({ ...ctx, body })
|
||||
}
|
||||
|
||||
// Remove an item from the cart
|
||||
if (req.method === 'DELETE') {
|
||||
const body = removeItemBodySchema.parse({ ...input, cartId })
|
||||
return await handlers['removeItem']({ ...ctx, body })
|
||||
}
|
||||
|
||||
return output ? parse(output, cartSchema.nullish()) : { status: 405 }
|
||||
}
|
||||
|
||||
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 { ProductsSchema } from '../../../types/product'
|
||||
|
||||
import validateHandlers from '../../utils/validate-handlers'
|
||||
import {
|
||||
searchProductBodySchema,
|
||||
searchProductsSchema,
|
||||
} from '../../../schemas/product'
|
||||
import { parse } from '../../utils'
|
||||
|
||||
const productsEndpoint: GetAPISchema<
|
||||
any,
|
||||
ProductsSchema
|
||||
>['endpoint']['handler'] = async (ctx) => {
|
||||
const { req, res, handlers } = ctx
|
||||
const { req, handlers } = ctx
|
||||
|
||||
if (!isAllowedOperation(req, res, { GET: handlers['getProducts'] })) {
|
||||
return
|
||||
validateHandlers(req, { GET: handlers['getProducts'] })
|
||||
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 {
|
||||
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 }] })
|
||||
}
|
||||
return parse(res, searchProductsSchema)
|
||||
}
|
||||
|
||||
export default productsEndpoint
|
||||
|
@ -1,49 +1,45 @@
|
||||
import type { CheckoutSchema } from '../../types/checkout'
|
||||
import type { GetAPISchema } from '..'
|
||||
import type { CheckoutSchema } from '../../types/checkout'
|
||||
|
||||
import { CommerceAPIError } from '../utils/errors'
|
||||
import isAllowedOperation from '../utils/is-allowed-operation'
|
||||
import {
|
||||
checkoutSchema,
|
||||
getCheckoutBodySchema,
|
||||
submitCheckoutBodySchema,
|
||||
} from '../../schemas/checkout'
|
||||
|
||||
import { parse, getInput } from '../utils'
|
||||
import validateHandlers from '../utils/validate-handlers'
|
||||
|
||||
const checkoutEndpoint: GetAPISchema<
|
||||
any,
|
||||
CheckoutSchema
|
||||
>['endpoint']['handler'] = async (ctx) => {
|
||||
const { req, res, handlers, config } = ctx
|
||||
const { req, handlers, config } = ctx
|
||||
|
||||
if (
|
||||
!isAllowedOperation(req, res, {
|
||||
GET: handlers['getCheckout'],
|
||||
POST: handlers['submitCheckout'],
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
validateHandlers(req, {
|
||||
GET: handlers['getCheckout'],
|
||||
POST: handlers['submitCheckout'],
|
||||
})
|
||||
|
||||
const { cookies } = req
|
||||
const cartId = cookies[config.cartCookie]
|
||||
const cartId = cookies.get(config.cartCookie)?.value
|
||||
const input = await getInput(req)
|
||||
|
||||
try {
|
||||
// Create checkout
|
||||
if (req.method === 'GET') {
|
||||
const body = { ...req.body, cartId }
|
||||
return await handlers['getCheckout']({ ...ctx, body })
|
||||
}
|
||||
|
||||
// Create checkout
|
||||
if (req.method === 'POST' && handlers['submitCheckout']) {
|
||||
const body = { ...req.body, cartId }
|
||||
return await handlers['submitCheckout']({ ...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 }] })
|
||||
// Get checkout
|
||||
if (req.method === 'GET') {
|
||||
const body = getCheckoutBodySchema.parse({ ...input, cartId })
|
||||
const res = await handlers['getCheckout']({ ...ctx, body })
|
||||
return parse(res, checkoutSchema.optional())
|
||||
}
|
||||
|
||||
// Create checkout
|
||||
if (req.method === 'POST' && handlers['submitCheckout']) {
|
||||
const body = submitCheckoutBodySchema.parse({ ...input, cartId })
|
||||
const res = await handlers['submitCheckout']({ ...ctx, body })
|
||||
return parse(res, checkoutSchema.optional())
|
||||
}
|
||||
|
||||
return { status: 405 }
|
||||
}
|
||||
|
||||
export default checkoutEndpoint
|
||||
|
@ -1,65 +1,68 @@
|
||||
import type { CustomerAddressSchema } from '../../../types/customer/address'
|
||||
import type { GetAPISchema } from '../..'
|
||||
|
||||
import { CommerceAPIError } from '../../utils/errors'
|
||||
import isAllowedOperation from '../../utils/is-allowed-operation'
|
||||
import validateHandlers from '../../utils/validate-handlers'
|
||||
|
||||
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<
|
||||
any,
|
||||
CustomerAddressSchema
|
||||
>['endpoint']['handler'] = async (ctx) => {
|
||||
const { req, res, handlers, config } = ctx
|
||||
const { req, handlers, config } = ctx
|
||||
|
||||
if (
|
||||
!isAllowedOperation(req, res, {
|
||||
GET: handlers['getAddresses'],
|
||||
POST: handlers['addItem'],
|
||||
PUT: handlers['updateItem'],
|
||||
DELETE: handlers['removeItem'],
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
validateHandlers(req, {
|
||||
GET: handlers['getAddresses'],
|
||||
POST: handlers['addItem'],
|
||||
PUT: handlers['updateItem'],
|
||||
DELETE: handlers['removeItem'],
|
||||
})
|
||||
|
||||
let output
|
||||
const input = await getInput(req)
|
||||
const { cookies } = req
|
||||
|
||||
// Cart id might be usefull for anonymous shopping
|
||||
const cartId = cookies[config.cartCookie]
|
||||
const cartId = cookies.get(config.cartCookie)?.value
|
||||
|
||||
try {
|
||||
// Return customer addresses
|
||||
if (req.method === 'GET') {
|
||||
const body = { cartId }
|
||||
return await handlers['getAddresses']({ ...ctx, body })
|
||||
}
|
||||
|
||||
// Create or add an item to customer addresses list
|
||||
if (req.method === 'POST') {
|
||||
const body = { ...req.body, cartId }
|
||||
return await handlers['addItem']({ ...ctx, body })
|
||||
}
|
||||
|
||||
// Update item in customer addresses list
|
||||
if (req.method === 'PUT') {
|
||||
const body = { ...req.body, cartId }
|
||||
return await handlers['updateItem']({ ...ctx, body })
|
||||
}
|
||||
|
||||
// Remove an item from customer addresses list
|
||||
if (req.method === 'DELETE') {
|
||||
const body = { ...req.body, cartId }
|
||||
return await handlers['removeItem']({ ...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 }] })
|
||||
// Return customer addresses
|
||||
if (req.method === 'GET') {
|
||||
const body = getCartBodySchema.parse({ cartId })
|
||||
return parse(
|
||||
await handlers['getAddresses']({ ...ctx, body }),
|
||||
addressSchema
|
||||
)
|
||||
}
|
||||
|
||||
// Create or add an item to customer addresses list
|
||||
if (req.method === 'POST') {
|
||||
const body = addAddressBodySchema.parse({ ...input, cartId })
|
||||
output = await handlers['addItem']({ ...ctx, body })
|
||||
}
|
||||
|
||||
// Update item in customer addresses list
|
||||
if (req.method === 'PUT') {
|
||||
const body = updateAddressBodySchema.parse({ ...input, cartId })
|
||||
output = await handlers['updateItem']({ ...ctx, body })
|
||||
}
|
||||
|
||||
// Remove an item from customer addresses list
|
||||
if (req.method === 'DELETE') {
|
||||
const body = deleteAddressBodySchema.parse({ ...input, cartId })
|
||||
return await handlers['removeItem']({ ...ctx, body })
|
||||
}
|
||||
|
||||
return output ? parse(output, addressSchema) : { status: 405 }
|
||||
}
|
||||
|
||||
export default customerShippingEndpoint
|
||||
|
@ -1,65 +1,67 @@
|
||||
import type { CustomerCardSchema } from '../../../types/customer/card'
|
||||
import type { GetAPISchema } from '../..'
|
||||
|
||||
import { CommerceAPIError } from '../../utils/errors'
|
||||
import isAllowedOperation from '../../utils/is-allowed-operation'
|
||||
import { z } from 'zod'
|
||||
|
||||
import {
|
||||
cardSchema,
|
||||
addCardBodySchema,
|
||||
deleteCardBodySchema,
|
||||
updateCardBodySchema,
|
||||
} from '../../../schemas/customer'
|
||||
import { parse, getInput } from '../../utils'
|
||||
|
||||
import validateHandlers from '../../utils/validate-handlers'
|
||||
|
||||
const customerCardEndpoint: GetAPISchema<
|
||||
any,
|
||||
CustomerCardSchema
|
||||
>['endpoint']['handler'] = async (ctx) => {
|
||||
const { req, res, handlers, config } = ctx
|
||||
const { req, handlers, config } = ctx
|
||||
|
||||
if (
|
||||
!isAllowedOperation(req, res, {
|
||||
GET: handlers['getCards'],
|
||||
POST: handlers['addItem'],
|
||||
PUT: handlers['updateItem'],
|
||||
DELETE: handlers['removeItem'],
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
validateHandlers(req, {
|
||||
GET: handlers['getCards'],
|
||||
POST: handlers['addItem'],
|
||||
PUT: handlers['updateItem'],
|
||||
DELETE: handlers['removeItem'],
|
||||
})
|
||||
|
||||
let output
|
||||
const input = await getInput(req)
|
||||
const { cookies } = req
|
||||
|
||||
// Cart id might be usefull for anonymous shopping
|
||||
const cartId = cookies[config.cartCookie]
|
||||
const cartId = cookies.get(config.cartCookie)?.value
|
||||
|
||||
try {
|
||||
// Create or add a card
|
||||
if (req.method === 'GET') {
|
||||
const body = { ...req.body }
|
||||
return await handlers['getCards']({ ...ctx, body })
|
||||
}
|
||||
|
||||
// Create or add an item to customer cards
|
||||
if (req.method === 'POST') {
|
||||
const body = { ...req.body, cartId }
|
||||
return await handlers['addItem']({ ...ctx, body })
|
||||
}
|
||||
|
||||
// Update item in customer cards
|
||||
if (req.method === 'PUT') {
|
||||
const body = { ...req.body, cartId }
|
||||
return await handlers['updateItem']({ ...ctx, body })
|
||||
}
|
||||
|
||||
// Remove an item from customer cards
|
||||
if (req.method === 'DELETE') {
|
||||
const body = { ...req.body, cartId }
|
||||
return await handlers['removeItem']({ ...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 }] })
|
||||
// Create or add a card
|
||||
if (req.method === 'GET') {
|
||||
const body = { ...input }
|
||||
return parse(
|
||||
await handlers['getCards']({ ...ctx, body }),
|
||||
z.array(cardSchema).optional()
|
||||
)
|
||||
}
|
||||
|
||||
// Create or add an item to customer cards
|
||||
if (req.method === 'POST') {
|
||||
const body = addCardBodySchema.parse({ ...input, cartId })
|
||||
output = await handlers['addItem']({ ...ctx, body })
|
||||
}
|
||||
|
||||
// Update item in customer cards
|
||||
if (req.method === 'PUT') {
|
||||
const body = updateCardBodySchema.parse({ ...input, cartId })
|
||||
output = await handlers['updateItem']({ ...ctx, body })
|
||||
}
|
||||
|
||||
// Remove an item from customer cards
|
||||
if (req.method === 'DELETE') {
|
||||
const body = deleteCardBodySchema.parse({ ...input, cartId })
|
||||
|
||||
return await handlers['removeItem']({ ...ctx, body })
|
||||
}
|
||||
|
||||
return output ? parse(output, cardSchema.nullish()) : { status: 405 }
|
||||
}
|
||||
|
||||
export default customerCardEndpoint
|
||||
|
@ -1,36 +1,32 @@
|
||||
import type { CustomerSchema } from '../../../types/customer'
|
||||
import type { GetAPISchema } from '../..'
|
||||
import { z } from 'zod'
|
||||
import { parse } from '../../utils'
|
||||
import validateHandlers from '../../utils/validate-handlers'
|
||||
|
||||
import { CommerceAPIError } from '../../utils/errors'
|
||||
import isAllowedOperation from '../../utils/is-allowed-operation'
|
||||
import { customerSchema } from '../../../schemas/customer'
|
||||
|
||||
const customerEndpoint: GetAPISchema<
|
||||
any,
|
||||
CustomerSchema<any>
|
||||
CustomerSchema
|
||||
>['endpoint']['handler'] = async (ctx) => {
|
||||
const { req, res, handlers } = ctx
|
||||
const { req, handlers } = ctx
|
||||
|
||||
if (
|
||||
!isAllowedOperation(req, res, {
|
||||
GET: handlers['getLoggedInCustomer'],
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
validateHandlers(req, {
|
||||
GET: handlers['getLoggedInCustomer'],
|
||||
})
|
||||
|
||||
try {
|
||||
const body = null
|
||||
return await handlers['getLoggedInCustomer']({ ...ctx, body })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
const body = null
|
||||
const output = await handlers['getLoggedInCustomer']({ ...ctx, body })
|
||||
|
||||
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 }] })
|
||||
}
|
||||
return output
|
||||
? parse(
|
||||
output,
|
||||
z.object({
|
||||
customer: customerSchema,
|
||||
})
|
||||
)
|
||||
: { status: 204 }
|
||||
}
|
||||
|
||||
export default customerEndpoint
|
||||
|
10
packages/commerce/src/api/endpoints/index.ts
Normal file
10
packages/commerce/src/api/endpoints/index.ts
Normal file
@ -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 { LoginSchema } from '../../types/login'
|
||||
|
||||
import { getInput } from '../utils'
|
||||
import validateHandlers from '../utils/validate-handlers'
|
||||
|
||||
import { loginBodySchema } from '../../schemas/auth'
|
||||
|
||||
const loginEndpoint: GetAPISchema<
|
||||
any,
|
||||
LoginSchema<any>
|
||||
LoginSchema
|
||||
>['endpoint']['handler'] = async (ctx) => {
|
||||
const { req, res, handlers } = ctx
|
||||
const { req, handlers } = ctx
|
||||
|
||||
if (
|
||||
!isAllowedOperation(req, res, {
|
||||
POST: handlers['login'],
|
||||
GET: handlers['login'],
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
validateHandlers(req, {
|
||||
POST: handlers['login'],
|
||||
GET: handlers['login'],
|
||||
})
|
||||
|
||||
try {
|
||||
const body = req.body ?? {}
|
||||
return await 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 }] })
|
||||
}
|
||||
const input = await getInput(req)
|
||||
const body = loginBodySchema.parse(input)
|
||||
return handlers['login']({ ...ctx, body })
|
||||
}
|
||||
|
||||
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 { LogoutSchema } from '../../types/logout'
|
||||
|
||||
const logoutEndpoint: GetAPISchema<any, LogoutSchema>['endpoint']['handler'] =
|
||||
async (ctx) => {
|
||||
const { req, res, handlers } = ctx
|
||||
import { logoutBodySchema } from '../../schemas/auth'
|
||||
import validateHandlers from '../utils/validate-handlers'
|
||||
|
||||
if (
|
||||
!isAllowedOperation(req, res, {
|
||||
GET: handlers['logout'],
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
const logoutEndpoint: GetAPISchema<
|
||||
any,
|
||||
LogoutSchema
|
||||
>['endpoint']['handler'] = async (ctx) => {
|
||||
const { req, handlers } = ctx
|
||||
|
||||
try {
|
||||
const redirectTo = req.query.redirect_to
|
||||
const body = typeof redirectTo === 'string' ? { redirectTo } : {}
|
||||
validateHandlers(req, {
|
||||
GET: handlers['logout'],
|
||||
})
|
||||
|
||||
return await handlers['logout']({ ...ctx, body })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
const redirectTo = new URL(req.url).searchParams.get('redirectTo')
|
||||
|
||||
const message =
|
||||
error instanceof CommerceAPIError
|
||||
? 'An unexpected error ocurred with the Commerce API'
|
||||
: 'An unexpected error ocurred'
|
||||
const body = logoutBodySchema.parse(
|
||||
typeof redirectTo === 'string' ? { redirectTo } : {}
|
||||
)
|
||||
|
||||
res.status(500).json({ data: null, errors: [{ message }] })
|
||||
}
|
||||
}
|
||||
return handlers['logout']({ ...ctx, body })
|
||||
}
|
||||
|
||||
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 { SignupSchema } from '../../types/signup'
|
||||
|
||||
const signupEndpoint: GetAPISchema<any, SignupSchema>['endpoint']['handler'] =
|
||||
async (ctx) => {
|
||||
const { req, res, handlers, config } = ctx
|
||||
import { getInput } from '../utils'
|
||||
import validateHandlers from '../utils/validate-handlers'
|
||||
|
||||
if (
|
||||
!isAllowedOperation(req, res, {
|
||||
POST: handlers['signup'],
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
import { signupBodySchema } from '../../schemas/auth'
|
||||
|
||||
const { cookies } = req
|
||||
const cartId = cookies[config.cartCookie]
|
||||
const signupEndpoint: GetAPISchema<
|
||||
any,
|
||||
SignupSchema
|
||||
>['endpoint']['handler'] = async (ctx) => {
|
||||
const { req, handlers, config } = ctx
|
||||
|
||||
try {
|
||||
const body = { ...req.body, cartId }
|
||||
return await handlers['signup']({ ...ctx, body })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
validateHandlers(req, {
|
||||
POST: handlers['signup'],
|
||||
})
|
||||
|
||||
const message =
|
||||
error instanceof CommerceAPIError
|
||||
? 'An unexpected error ocurred with the Commerce API'
|
||||
: 'An unexpected error ocurred'
|
||||
const input = await getInput(req)
|
||||
const { cookies } = req
|
||||
const cartId = cookies.get(config.cartCookie)?.value
|
||||
|
||||
res.status(500).json({ data: null, errors: [{ message }] })
|
||||
}
|
||||
}
|
||||
const body = signupBodySchema.parse({ ...input, cartId })
|
||||
return handlers['signup']({ ...ctx, body })
|
||||
}
|
||||
|
||||
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 { 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<
|
||||
any,
|
||||
WishlistSchema<any>
|
||||
WishlistSchema
|
||||
>['endpoint']['handler'] = async (ctx) => {
|
||||
const { req, res, handlers, config } = ctx
|
||||
const { req, handlers, config } = ctx
|
||||
|
||||
if (
|
||||
!isAllowedOperation(req, res, {
|
||||
GET: handlers['getWishlist'],
|
||||
POST: handlers['addItem'],
|
||||
DELETE: handlers['removeItem'],
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
validateHandlers(req, {
|
||||
GET: handlers['getWishlist'],
|
||||
POST: handlers['addItem'],
|
||||
DELETE: handlers['removeItem'],
|
||||
})
|
||||
|
||||
let output
|
||||
const { cookies } = req
|
||||
const customerToken = cookies[config.customerCookie]
|
||||
const input = await getInput(req)
|
||||
|
||||
try {
|
||||
// Return current wishlist info
|
||||
if (req.method === 'GET') {
|
||||
const body = {
|
||||
customerToken,
|
||||
includeProducts: req.query.products === '1',
|
||||
}
|
||||
return await handlers['getWishlist']({ ...ctx, body })
|
||||
}
|
||||
const customerToken = cookies.get(config.customerCookie)?.value
|
||||
const products = new URL(req.url).searchParams.get('products')
|
||||
|
||||
// Add an item to the wishlist
|
||||
if (req.method === 'POST') {
|
||||
const body = { ...req.body, customerToken }
|
||||
return await handlers['addItem']({ ...ctx, body })
|
||||
}
|
||||
|
||||
// Remove an item from the wishlist
|
||||
if (req.method === 'DELETE') {
|
||||
const body = { ...req.body, customerToken }
|
||||
return await handlers['removeItem']({ ...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 }] })
|
||||
// Return current wishlist info
|
||||
if (req.method === 'GET') {
|
||||
const body = getWishlistBodySchema.parse({
|
||||
customerToken,
|
||||
includeProducts: !!products,
|
||||
})
|
||||
output = await handlers['getWishlist']({ ...ctx, body })
|
||||
}
|
||||
|
||||
// Add an item to the wishlist
|
||||
if (req.method === 'POST') {
|
||||
const body = addItemBodySchema.parse({ ...input, customerToken })
|
||||
output = await handlers['addItem']({ ...ctx, body })
|
||||
}
|
||||
|
||||
// Remove an item from the wishlist
|
||||
if (req.method === 'DELETE') {
|
||||
const body = removeItemBodySchema.parse({ ...input, customerToken })
|
||||
output = await handlers['removeItem']({ ...ctx, body })
|
||||
}
|
||||
|
||||
return output ? parse(output, wishlistSchema.optional()) : { status: 405 }
|
||||
}
|
||||
|
||||
export default wishlistEndpoint
|
||||
|
@ -1,6 +1,5 @@
|
||||
import type { NextApiHandler } from 'next'
|
||||
import type { FetchOptions, Response } from '@vercel/fetch'
|
||||
import type { APIEndpoint, APIHandler } from './utils/types'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import type { APIEndpoint, APIHandler, APIResponse } from './utils/types'
|
||||
import type { CartSchema } from '../types/cart'
|
||||
import type { CustomerSchema } from '../types/customer'
|
||||
import type { LoginSchema } from '../types/login'
|
||||
@ -11,11 +10,14 @@ import type { WishlistSchema } from '../types/wishlist'
|
||||
import type { CheckoutSchema } from '../types/checkout'
|
||||
import type { CustomerCardSchema } from '../types/customer/card'
|
||||
import type { CustomerAddressSchema } from '../types/customer/address'
|
||||
|
||||
import { withOperationCallback } from './utils/with-operation-callback'
|
||||
|
||||
import {
|
||||
defaultOperations,
|
||||
OPERATIONS,
|
||||
AllOperations,
|
||||
APIOperations,
|
||||
defaultOperations,
|
||||
} from './operations'
|
||||
|
||||
export type APISchemas =
|
||||
@ -71,6 +73,12 @@ export type EndpointHandlers<
|
||||
>
|
||||
}
|
||||
|
||||
export type FetchOptions<Body = any> = {
|
||||
method?: string
|
||||
body?: Body
|
||||
headers?: HeadersInit
|
||||
}
|
||||
|
||||
export type APIProvider = {
|
||||
config: CommerceAPIConfig
|
||||
operations: APIOperations<any>
|
||||
@ -106,13 +114,18 @@ export function getCommerceApi<P extends APIProvider>(
|
||||
OPERATIONS.forEach((k) => {
|
||||
const op = ops[k]
|
||||
if (op) {
|
||||
commerce[k] = op({ commerce }) as AllOperations<P>[typeof k]
|
||||
commerce[k] = withOperationCallback(
|
||||
k,
|
||||
op({ commerce })
|
||||
) as AllOperations<P>[typeof k]
|
||||
}
|
||||
})
|
||||
|
||||
return commerce
|
||||
}
|
||||
|
||||
export type EndpointHandler = (req: NextRequest) => Promise<APIResponse>
|
||||
|
||||
export function getEndpoint<
|
||||
P extends APIProvider,
|
||||
T extends GetAPISchema<any, any>
|
||||
@ -122,13 +135,11 @@ export function getEndpoint<
|
||||
config?: P['config']
|
||||
options?: T['schema']['endpoint']['options']
|
||||
}
|
||||
): NextApiHandler {
|
||||
): EndpointHandler {
|
||||
const cfg = commerce.getConfig(context.config)
|
||||
|
||||
return function apiHandler(req, res) {
|
||||
return function apiHandler(req) {
|
||||
return context.handler({
|
||||
req,
|
||||
res,
|
||||
commerce,
|
||||
config: cfg,
|
||||
handlers: context.handlers,
|
||||
@ -145,7 +156,7 @@ export const createEndpoint =
|
||||
config?: P['config']
|
||||
options?: API['schema']['endpoint']['options']
|
||||
}
|
||||
): NextApiHandler => {
|
||||
): EndpointHandler => {
|
||||
return getEndpoint(commerce, { ...endpoint, ...context })
|
||||
}
|
||||
|
||||
@ -160,7 +171,7 @@ export interface CommerceAPIConfig {
|
||||
fetch<Data = any, Variables = any>(
|
||||
query: string,
|
||||
queryData?: CommerceAPIFetchOptions<Variables>,
|
||||
fetchOptions?: FetchOptions
|
||||
options?: FetchOptions
|
||||
): Promise<GraphQLFetcherResult<Data>>
|
||||
}
|
||||
|
||||
@ -169,8 +180,7 @@ export type GraphQLFetcher<
|
||||
Variables = any
|
||||
> = (
|
||||
query: string,
|
||||
queryData?: CommerceAPIFetchOptions<Variables>,
|
||||
fetchOptions?: FetchOptions
|
||||
queryData?: CommerceAPIFetchOptions<Variables>
|
||||
) => Promise<Data>
|
||||
|
||||
export interface GraphQLFetcherResult<Data = any> {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import type { ServerResponse } from 'http'
|
||||
import type { LoginOperation } from '../types/login'
|
||||
import type { GetAllPagesOperation, GetPageOperation } from '../types/page'
|
||||
import type { GetSiteInfoOperation } from '../types/site'
|
||||
@ -25,6 +24,13 @@ export const OPERATIONS = [
|
||||
'getProduct',
|
||||
] as const
|
||||
|
||||
export type Operation = {
|
||||
[O in AllowedOperations]: {
|
||||
name: O
|
||||
data: Awaited<ReturnType<Operations<APIProvider>[O]>>
|
||||
}
|
||||
}[AllowedOperations]
|
||||
|
||||
export const defaultOperations = OPERATIONS.reduce((ops, k) => {
|
||||
ops[k] = noop
|
||||
return ops
|
||||
@ -37,14 +43,14 @@ export type Operations<P extends APIProvider> = {
|
||||
<T extends LoginOperation>(opts: {
|
||||
variables: T['variables']
|
||||
config?: P['config']
|
||||
res: ServerResponse
|
||||
res: Response
|
||||
}): Promise<T['data']>
|
||||
|
||||
<T extends LoginOperation>(
|
||||
opts: {
|
||||
variables: T['variables']
|
||||
config?: P['config']
|
||||
res: ServerResponse
|
||||
res: Response
|
||||
} & OperationOptions
|
||||
): 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,6 +1,9 @@
|
||||
import type { Response } from '@vercel/fetch'
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
export class CommerceAPIError extends Error {
|
||||
import { CommerceError } from '../../utils/errors'
|
||||
import { ZodError } from 'zod'
|
||||
|
||||
export class CommerceAPIResponseError extends Error {
|
||||
status: number
|
||||
res: Response
|
||||
data: any
|
||||
@ -14,9 +17,85 @@ 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 {
|
||||
constructor(msg: string) {
|
||||
super(msg)
|
||||
this.name = 'CommerceNetworkError'
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeZodIssues = (issues: ZodError['issues']) =>
|
||||
issues.map(({ path, message }) =>
|
||||
path.length ? `${message} at "${path.join('.')}" field` : message
|
||||
)
|
||||
|
||||
export const getOperationError = (operation: string, error: unknown) => {
|
||||
if (error instanceof ZodError) {
|
||||
return new CommerceError({
|
||||
code: 'SCHEMA_VALIDATION_ERROR',
|
||||
message:
|
||||
`Validation ${
|
||||
error.issues.length === 1 ? 'error' : 'errors'
|
||||
} at "${operation}" operation: \n` +
|
||||
normalizeZodIssues(error.issues).join('\n'),
|
||||
})
|
||||
}
|
||||
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' }],
|
||||
}
|
||||
}
|
||||
|
91
packages/commerce/src/api/utils/index.ts
Normal file
91
packages/commerce/src/api/utils/index.ts
Normal file
@ -0,0 +1,91 @@
|
||||
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) => {
|
||||
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)
|
||||
}
|
||||
|
||||
// Get the url path & query string
|
||||
const url = new URL(req.url || '/', `https://${req.headers.host}`)
|
||||
|
||||
return new NextRequest(url, {
|
||||
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))
|
||||
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 '..'
|
||||
|
||||
export type ErrorData = { message: string; code?: string }
|
||||
|
||||
export type APIResponse<Data = any> =
|
||||
| { data: Data; errors?: ErrorData[] }
|
||||
// If `data` doesn't include `null`, then `null` is only allowed on errors
|
||||
| (Data extends null
|
||||
? { data: null; errors?: ErrorData[] }
|
||||
: { data: null; errors: ErrorData[] })
|
||||
export type APIResponse<Data = any> = {
|
||||
data?: Data
|
||||
errors?: ErrorData[]
|
||||
status?: number
|
||||
headers?: Record<string, number | string | string[]> | Headers
|
||||
/**
|
||||
* @type {string}
|
||||
* @example redirectTo: '/cart'
|
||||
*/
|
||||
redirectTo?: string
|
||||
}
|
||||
|
||||
export type APIHandlerContext<
|
||||
C extends CommerceAPI,
|
||||
@ -16,14 +21,10 @@ export type APIHandlerContext<
|
||||
Data = any,
|
||||
Options extends {} = {}
|
||||
> = {
|
||||
req: NextApiRequest
|
||||
res: NextApiResponse<APIResponse<Data>>
|
||||
req: NextRequest
|
||||
commerce: C
|
||||
config: C['provider']['config']
|
||||
handlers: H
|
||||
/**
|
||||
* Custom configs that may be used by a particular handler
|
||||
*/
|
||||
options: Options
|
||||
}
|
||||
|
||||
@ -35,7 +36,7 @@ export type APIHandler<
|
||||
Options extends {} = {}
|
||||
> = (
|
||||
context: APIHandlerContext<C, H, Data, Options> & { body: Body }
|
||||
) => void | Promise<void>
|
||||
) => Promise<APIResponse<Data>>
|
||||
|
||||
export type APIHandlers<C extends CommerceAPI> = {
|
||||
[k: string]: APIHandler<C, any, any, any, any>
|
||||
@ -46,4 +47,6 @@ export type APIEndpoint<
|
||||
H extends APIHandlers<C> = {},
|
||||
Data = any,
|
||||
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',
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
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