Add dynamic API endpoints

This commit is contained in:
Catalin Pinte 2022-09-28 15:40:22 +03:00
parent 11609a9e71
commit 17788b1eb0
176 changed files with 884 additions and 697 deletions

View File

@ -9,12 +9,6 @@ const addItem: CartEndpoint['handlers']['addItem'] = async ({
body: { cartId, item }, body: { cartId, item },
config, config,
}) => { }) => {
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
if (!item.quantity) item.quantity = 1 if (!item.quantity) item.quantity = 1
const options = { const options = {
@ -41,7 +35,7 @@ const addItem: CartEndpoint['handlers']['addItem'] = async ({
'Set-Cookie', 'Set-Cookie',
getCartCookie(config.cartCookie, data.id, config.cartCookieMaxAge) getCartCookie(config.cartCookie, data.id, config.cartCookieMaxAge)
) )
res.status(200).json({ data: normalizeCart(data) }) res.status(200).json({ data: data ? normalizeCart(data) : null })
} }
export default addItem export default addItem

View File

@ -19,6 +19,7 @@ export const handlers: CartEndpoint['handlers'] = {
} }
const cartApi = createEndpoint<CartAPI>({ const cartApi = createEndpoint<CartAPI>({
/* @ts-ignore */
handler: cartEndpoint, handler: cartEndpoint,
handlers, handlers,
}) })

View File

@ -7,13 +7,6 @@ const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
body: { cartId, itemId }, body: { cartId, itemId },
config, config,
}) => { }) => {
if (!cartId || !itemId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const result = await config.storeApiFetch<{ data: any } | null>( const result = await config.storeApiFetch<{ data: any } | null>(
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`, `/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
{ method: 'DELETE' } { method: 'DELETE' }
@ -28,7 +21,8 @@ const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
: // Remove the cart cookie if the cart was removed (empty items) : // Remove the cart cookie if the cart was removed (empty items)
getCartCookie(config.cartCookie) getCartCookie(config.cartCookie)
) )
res.status(200).json({ data: data && normalizeCart(data) })
res.status(200).json({ data: data ? normalizeCart(data) : null })
} }
export default removeItem export default removeItem

View File

@ -8,14 +8,7 @@ const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
body: { cartId, itemId, item }, body: { cartId, itemId, item },
config, config,
}) => { }) => {
if (!cartId || !itemId || !item) { const { data } = await config.storeApiFetch<{ data?: any }>(
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const { data } = await config.storeApiFetch(
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`, `/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
{ {
method: 'PUT', method: 'PUT',

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

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

View File

@ -9,7 +9,7 @@ export default useLogin as UseLogin<typeof handler>
export const handler: MutationHook<LoginHook> = { export const handler: MutationHook<LoginHook> = {
fetchOptions: { fetchOptions: {
url: '/api/login', url: '/api/commerce/login',
method: 'POST', method: 'POST',
}, },
async fetcher({ input: { email, password }, options, fetch }) { async fetcher({ input: { email, password }, options, fetch }) {

View File

@ -8,7 +8,7 @@ export default useLogout as UseLogout<typeof handler>
export const handler: MutationHook<LogoutHook> = { export const handler: MutationHook<LogoutHook> = {
fetchOptions: { fetchOptions: {
url: '/api/logout', url: '/api/commerce/logout',
method: 'GET', method: 'GET',
}, },
useHook: useHook:

View File

@ -9,7 +9,7 @@ export default useSignup as UseSignup<typeof handler>
export const handler: MutationHook<SignupHook> = { export const handler: MutationHook<SignupHook> = {
fetchOptions: { fetchOptions: {
url: '/api/signup', url: '/api/commerce/signup',
method: 'POST', method: 'POST',
}, },
async fetcher({ async fetcher({

View File

@ -9,7 +9,7 @@ export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = { export const handler: MutationHook<AddItemHook> = {
fetchOptions: { fetchOptions: {
url: '/api/cart', url: '/api/commerce/cart',
method: 'POST', method: 'POST',
}, },
async fetcher({ input: item, options, fetch }) { async fetcher({ input: item, options, fetch }) {

View File

@ -7,7 +7,7 @@ export default useCart as UseCart<typeof handler>
export const handler: SWRHook<GetCartHook> = { export const handler: SWRHook<GetCartHook> = {
fetchOptions: { fetchOptions: {
url: '/api/cart', url: '/api/commerce/cart',
method: 'GET', method: 'GET',
}, },
useHook: useHook:

View File

@ -4,8 +4,14 @@ import type {
HookFetcherContext, HookFetcherContext,
} from '@vercel/commerce/utils/types' } from '@vercel/commerce/utils/types'
import { ValidationError } from '@vercel/commerce/utils/errors' import { ValidationError } from '@vercel/commerce/utils/errors'
import useRemoveItem, { UseRemoveItem } from '@vercel/commerce/cart/use-remove-item' import useRemoveItem, {
import type { Cart, LineItem, RemoveItemHook } from '@vercel/commerce/types/cart' UseRemoveItem,
} from '@vercel/commerce/cart/use-remove-item'
import type {
Cart,
LineItem,
RemoveItemHook,
} from '@vercel/commerce/types/cart'
import useCart from './use-cart' import useCart from './use-cart'
export type RemoveItemFn<T = any> = T extends LineItem export type RemoveItemFn<T = any> = T extends LineItem
@ -20,7 +26,7 @@ export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler = { export const handler = {
fetchOptions: { fetchOptions: {
url: '/api/cart', url: '/api/commerce/cart',
method: 'DELETE', method: 'DELETE',
}, },
async fetcher({ async fetcher({

View File

@ -5,7 +5,9 @@ import type {
HookFetcherContext, HookFetcherContext,
} from '@vercel/commerce/utils/types' } from '@vercel/commerce/utils/types'
import { ValidationError } from '@vercel/commerce/utils/errors' import { ValidationError } from '@vercel/commerce/utils/errors'
import useUpdateItem, { UseUpdateItem } from '@vercel/commerce/cart/use-update-item' import useUpdateItem, {
UseUpdateItem,
} from '@vercel/commerce/cart/use-update-item'
import type { LineItem, UpdateItemHook } from '@vercel/commerce/types/cart' import type { LineItem, UpdateItemHook } from '@vercel/commerce/types/cart'
import { handler as removeItemHandler } from './use-remove-item' import { handler as removeItemHandler } from './use-remove-item'
import useCart from './use-cart' import useCart from './use-cart'
@ -18,7 +20,7 @@ export default useUpdateItem as UseUpdateItem<typeof handler>
export const handler = { export const handler = {
fetchOptions: { fetchOptions: {
url: '/api/cart', url: '/api/commerce/cart',
method: 'PUT', method: 'PUT',
}, },
async fetcher({ async fetcher({

View File

@ -6,7 +6,7 @@ export default useCustomer as UseCustomer<typeof handler>
export const handler: SWRHook<CustomerHook> = { export const handler: SWRHook<CustomerHook> = {
fetchOptions: { fetchOptions: {
url: '/api/customer', url: '/api/commerce/customer',
method: 'GET', method: 'GET',
}, },
async fetcher({ options, fetch }) { async fetcher({ options, fetch }) {

View File

@ -14,7 +14,7 @@ export type SearchProductsInput = {
export const handler: SWRHook<SearchProductsHook> = { export const handler: SWRHook<SearchProductsHook> = {
fetchOptions: { fetchOptions: {
url: '/api/catalog/products', url: '/api/commerce/catalog/products',
method: 'GET', method: 'GET',
}, },
fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) { fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) {

View File

@ -10,7 +10,7 @@ export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = { export const handler: MutationHook<AddItemHook> = {
fetchOptions: { fetchOptions: {
url: '/api/wishlist', url: '/api/commerce/wishlist',
method: 'POST', method: 'POST',
}, },
useHook: useHook:

View File

@ -12,7 +12,7 @@ export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler: MutationHook<RemoveItemHook> = { export const handler: MutationHook<RemoveItemHook> = {
fetchOptions: { fetchOptions: {
url: '/api/wishlist', url: '/api/commerce/wishlist',
method: 'DELETE', method: 'DELETE',
}, },
useHook: useHook:

View File

@ -10,7 +10,7 @@ import type { GetWishlistHook } from '../types/wishlist'
export default useWishlist as UseWishlist<typeof handler> export default useWishlist as UseWishlist<typeof handler>
export const handler: SWRHook<GetWishlistHook> = { export const handler: SWRHook<GetWishlistHook> = {
fetchOptions: { fetchOptions: {
url: '/api/wishlist', url: '/api/commerce/wishlist',
method: 'GET', method: 'GET',
}, },
async fetcher({ input: { customerId, includeProducts }, options, fetch }) { async fetcher({ input: { customerId, includeProducts }, options, fetch }) {

View File

@ -69,7 +69,10 @@ Then, open [/site/.env.template](/site/.env.template) and add the provider name
Using BigCommerce as an example. The first thing to do is export a `CommerceProvider` component that includes a `provider` object with all the handlers that can be used for hooks: Using BigCommerce as an example. The first thing to do is export a `CommerceProvider` component that includes a `provider` object with all the handlers that can be used for hooks:
```tsx ```tsx
import { getCommerceProvider, useCommerce as useCoreCommerce } from '@vercel/commerce' import {
getCommerceProvider,
useCommerce as useCoreCommerce,
} from '@vercel/commerce'
import { bigcommerceProvider, BigcommerceProvider } from './provider' import { bigcommerceProvider, BigcommerceProvider } from './provider'
export { bigcommerceProvider } export { bigcommerceProvider }
@ -135,7 +138,7 @@ export default useCart as UseCart<typeof handler>
export const handler: SWRHook<GetCartHook> = { export const handler: SWRHook<GetCartHook> = {
fetchOptions: { fetchOptions: {
url: '/api/cart', url: '/api/commerce/cart',
method: 'GET', method: 'GET',
}, },
useHook: useHook:
@ -175,7 +178,7 @@ export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = { export const handler: MutationHook<AddItemHook> = {
fetchOptions: { fetchOptions: {
url: '/api/cart', url: '/api/commerce/cart',
method: 'POST', method: 'POST',
}, },
async fetcher({ input: item, options, fetch }) { async fetcher({ input: item, options, fetch }) {
@ -213,25 +216,26 @@ export const handler: MutationHook<AddItemHook> = {
``` ```
## Showing progress and features ## Showing progress and features
When creating a PR for a new provider, include this list in the PR description and mark the progress as you push so we can organize the code review. Not all points are required (but advised) so make sure to keep the list up to date. When creating a PR for a new provider, include this list in the PR description and mark the progress as you push so we can organize the code review. Not all points are required (but advised) so make sure to keep the list up to date.
**Status** **Status**
* [ ] CommerceProvider - [ ] CommerceProvider
* [ ] Schema & TS types - [ ] Schema & TS types
* [ ] API Operations - Get all collections - [ ] API Operations - Get all collections
* [ ] API Operations - Get all pages - [ ] API Operations - Get all pages
* [ ] API Operations - Get all products - [ ] API Operations - Get all products
* [ ] API Operations - Get page - [ ] API Operations - Get page
* [ ] API Operations - Get product - [ ] API Operations - Get product
* [ ] API Operations - Get Shop Info (categories and vendors working — `vendors` query still a WIP PR on Reaction) - [ ] API Operations - Get Shop Info (categories and vendors working — `vendors` query still a WIP PR on Reaction)
* [ ] Hook - Add Item - [ ] Hook - Add Item
* [ ] Hook - Remove Item - [ ] Hook - Remove Item
* [ ] Hook - Update Item - [ ] Hook - Update Item
* [ ] Hook - Get Cart (account-tied carts working, anonymous carts working, cart reconciliation working) - [ ] Hook - Get Cart (account-tied carts working, anonymous carts working, cart reconciliation working)
* [ ] Auth (based on a WIP PR on Reaction - still need to implement refresh tokens) - [ ] Auth (based on a WIP PR on Reaction - still need to implement refresh tokens)
* [ ] Customer information - [ ] Customer information
* [ ] Product attributes - Size, Colors - [ ] Product attributes - Size, Colors
* [ ] Custom checkout - [ ] Custom checkout
* [ ] Typing (in progress) - [ ] Typing (in progress)
* [ ] Tests - [ ] Tests

View File

@ -1,60 +1,54 @@
import type { CartSchema } from '../../types/cart'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..' import type { GetAPISchema } from '..'
import type { CartSchema } from '../../types/cart'
const cartEndpoint: GetAPISchema<any, CartSchema<any>>['endpoint']['handler'] = import validateHandlers from '../utils/validate-handlers'
async (ctx) => {
import {
getCartBodySchema,
addItemBodySchema,
updateItemBodySchema,
removeItemBodySchema,
} from '../../schemas/cart'
const cartEndpoint: GetAPISchema<
any,
CartSchema
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers, config } = ctx const { req, res, handlers, config } = ctx
if ( validateHandlers(req, res, {
!isAllowedOperation(req, res, {
GET: handlers['getCart'], GET: handlers['getCart'],
POST: handlers['addItem'], POST: handlers['addItem'],
PUT: handlers['updateItem'], PUT: handlers['updateItem'],
DELETE: handlers['removeItem'], DELETE: handlers['removeItem'],
}) })
) {
return
}
const { cookies } = req const { cookies } = req
const cartId = cookies[config.cartCookie] const cartId = cookies[config.cartCookie]
try {
// Return current cart info // Return current cart info
if (req.method === 'GET') { if (req.method === 'GET') {
const body = { cartId } const body = getCartBodySchema.parse({ cartId })
return await handlers['getCart']({ ...ctx, body }) return handlers['getCart']({ ...ctx, body })
} }
// Create or add an item to the cart // Create or add an item to the cart
if (req.method === 'POST') { if (req.method === 'POST') {
const body = { ...req.body, cartId } const body = addItemBodySchema.parse({ ...req.body, cartId })
return await handlers['addItem']({ ...ctx, body }) return handlers['addItem']({ ...ctx, body })
} }
// Update item in cart // Update item in cart
if (req.method === 'PUT') { if (req.method === 'PUT') {
const body = { ...req.body, cartId } const body = updateItemBodySchema.parse({ ...req.body, cartId })
return await handlers['updateItem']({ ...ctx, body }) return handlers['updateItem']({ ...ctx, body })
} }
// Remove an item from the cart // Remove an item from the cart
if (req.method === 'DELETE') { if (req.method === 'DELETE') {
const body = { ...req.body, cartId } const body = removeItemBodySchema.parse({ ...req.body, cartId })
return await handlers['removeItem']({ ...ctx, body }) return 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 }] })
}
} }
}
export default cartEndpoint export default cartEndpoint

View File

@ -1,31 +1,26 @@
import type { ProductsSchema } from '../../../types/product' import { searchProductBodySchema } from '../../../schemas/product'
import { CommerceAPIError } from '../../utils/errors'
import isAllowedOperation from '../../utils/is-allowed-operation'
import type { GetAPISchema } from '../..' import type { GetAPISchema } from '../..'
import type { ProductsSchema } from '../../../types/product'
import validateHandlers from '../../utils/validate-handlers'
const productsEndpoint: GetAPISchema< const productsEndpoint: GetAPISchema<
any, any,
ProductsSchema ProductsSchema
>['endpoint']['handler'] = async (ctx) => { >['endpoint']['handler'] = (ctx) => {
const { req, res, handlers } = ctx const { req, res, handlers } = ctx
if (!isAllowedOperation(req, res, { GET: handlers['getProducts'] })) { validateHandlers(req, res, { GET: handlers['getProducts'] })
return
}
try { const body = searchProductBodySchema.parse({
const body = req.query search: req.query.search,
return await handlers['getProducts']({ ...ctx, body }) categoryId: req.query.categoryId,
} catch (error) { brandId: req.query.brandId,
console.error(error) sort: req.query.sort,
locale: req.query.locale,
})
const message = return handlers['getProducts']({ ...ctx, body })
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
} }
export default productsEndpoint export default productsEndpoint

View File

@ -1,48 +1,32 @@
import type { CheckoutSchema } from '../../types/checkout'
import type { GetAPISchema } from '..' import type { GetAPISchema } from '..'
import type { CheckoutSchema } from '../../types/checkout'
import { CommerceAPIError } from '../utils/errors' import validateHandlers from '../utils/validate-handlers'
import isAllowedOperation from '../utils/is-allowed-operation'
const checkoutEndpoint: GetAPISchema< const checkoutEndpoint: GetAPISchema<
any, any,
CheckoutSchema CheckoutSchema
>['endpoint']['handler'] = async (ctx) => { >['endpoint']['handler'] = (ctx) => {
const { req, res, handlers, config } = ctx const { req, res, handlers, config } = ctx
if ( validateHandlers(req, res, {
!isAllowedOperation(req, res, {
GET: handlers['getCheckout'], GET: handlers['getCheckout'],
POST: handlers['submitCheckout'], POST: handlers['submitCheckout'],
}) })
) {
return
}
const { cookies } = req const { cookies } = req
const cartId = cookies[config.cartCookie] const cartId = cookies[config.cartCookie]
try {
// Create checkout // Create checkout
if (req.method === 'GET') { if (req.method === 'GET') {
const body = { ...req.body, cartId } const body = { ...req.body, cartId }
return await handlers['getCheckout']({ ...ctx, body }) return handlers['getCheckout']({ ...ctx, body })
} }
// Create checkout // Create checkout
if (req.method === 'POST' && handlers['submitCheckout']) { if (req.method === 'POST' && handlers['submitCheckout']) {
const body = { ...req.body, cartId } const body = { ...req.body, cartId }
return await handlers['submitCheckout']({ ...ctx, body }) return 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 }] })
} }
} }

View File

@ -1,64 +1,47 @@
import type { CustomerAddressSchema } from '../../../types/customer/address' import type { CustomerAddressSchema } from '../../../types/customer/address'
import type { GetAPISchema } from '../..' import type { GetAPISchema } from '../..'
import { CommerceAPIError } from '../../utils/errors' import validateHandlers from '../../utils/validate-handlers'
import isAllowedOperation from '../../utils/is-allowed-operation'
const customerShippingEndpoint: GetAPISchema< const customerShippingEndpoint: GetAPISchema<
any, any,
CustomerAddressSchema CustomerAddressSchema
>['endpoint']['handler'] = async (ctx) => { >['endpoint']['handler'] = (ctx) => {
const { req, res, handlers, config } = ctx const { req, res, handlers, config } = ctx
if ( validateHandlers(req, res, {
!isAllowedOperation(req, res, {
GET: handlers['getAddresses'], GET: handlers['getAddresses'],
POST: handlers['addItem'], POST: handlers['addItem'],
PUT: handlers['updateItem'], PUT: handlers['updateItem'],
DELETE: handlers['removeItem'], DELETE: handlers['removeItem'],
}) })
) {
return
}
const { cookies } = req const { cookies } = req
// Cart id might be usefull for anonymous shopping // Cart id might be usefull for anonymous shopping
const cartId = cookies[config.cartCookie] const cartId = cookies[config.cartCookie]
try {
// Return customer addresses // Return customer addresses
if (req.method === 'GET') { if (req.method === 'GET') {
const body = { cartId } const body = { cartId }
return await handlers['getAddresses']({ ...ctx, body }) return handlers['getAddresses']({ ...ctx, body })
} }
// Create or add an item to customer addresses list // Create or add an item to customer addresses list
if (req.method === 'POST') { if (req.method === 'POST') {
const body = { ...req.body, cartId } const body = { ...req.body, cartId }
return await handlers['addItem']({ ...ctx, body }) return handlers['addItem']({ ...ctx, body })
} }
// Update item in customer addresses list // Update item in customer addresses list
if (req.method === 'PUT') { if (req.method === 'PUT') {
const body = { ...req.body, cartId } const body = { ...req.body, cartId }
return await handlers['updateItem']({ ...ctx, body }) return handlers['updateItem']({ ...ctx, body })
} }
// Remove an item from customer addresses list // Remove an item from customer addresses list
if (req.method === 'DELETE') { if (req.method === 'DELETE') {
const body = { ...req.body, cartId } const body = { ...req.body, cartId }
return await handlers['removeItem']({ ...ctx, body }) return 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 }] })
} }
} }

View File

@ -1,64 +1,47 @@
import type { CustomerCardSchema } from '../../../types/customer/card' import type { CustomerCardSchema } from '../../../types/customer/card'
import type { GetAPISchema } from '../..' import type { GetAPISchema } from '../..'
import { CommerceAPIError } from '../../utils/errors' import validateHandlers from '../../utils/validate-handlers'
import isAllowedOperation from '../../utils/is-allowed-operation'
const customerCardEndpoint: GetAPISchema< const customerCardEndpoint: GetAPISchema<
any, any,
CustomerCardSchema CustomerCardSchema
>['endpoint']['handler'] = async (ctx) => { >['endpoint']['handler'] = (ctx) => {
const { req, res, handlers, config } = ctx const { req, res, handlers, config } = ctx
if ( validateHandlers(req, res, {
!isAllowedOperation(req, res, {
GET: handlers['getCards'], GET: handlers['getCards'],
POST: handlers['addItem'], POST: handlers['addItem'],
PUT: handlers['updateItem'], PUT: handlers['updateItem'],
DELETE: handlers['removeItem'], DELETE: handlers['removeItem'],
}) })
) {
return
}
const { cookies } = req const { cookies } = req
// Cart id might be usefull for anonymous shopping // Cart id might be usefull for anonymous shopping
const cartId = cookies[config.cartCookie] const cartId = cookies[config.cartCookie]
try {
// Create or add a card // Create or add a card
if (req.method === 'GET') { if (req.method === 'GET') {
const body = { ...req.body } const body = { ...req.body }
return await handlers['getCards']({ ...ctx, body }) return handlers['getCards']({ ...ctx, body })
} }
// Create or add an item to customer cards // Create or add an item to customer cards
if (req.method === 'POST') { if (req.method === 'POST') {
const body = { ...req.body, cartId } const body = { ...req.body, cartId }
return await handlers['addItem']({ ...ctx, body }) return handlers['addItem']({ ...ctx, body })
} }
// Update item in customer cards // Update item in customer cards
if (req.method === 'PUT') { if (req.method === 'PUT') {
const body = { ...req.body, cartId } const body = { ...req.body, cartId }
return await handlers['updateItem']({ ...ctx, body }) return handlers['updateItem']({ ...ctx, body })
} }
// Remove an item from customer cards // Remove an item from customer cards
if (req.method === 'DELETE') { if (req.method === 'DELETE') {
const body = { ...req.body, cartId } const body = { ...req.body, cartId }
return await handlers['removeItem']({ ...ctx, body }) return 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 }] })
} }
} }

View File

@ -1,36 +1,20 @@
import type { CustomerSchema } from '../../../types/customer' import type { CustomerSchema } from '../../../types/customer'
import type { GetAPISchema } from '../..' import type { GetAPISchema } from '../..'
import { CommerceAPIError } from '../../utils/errors' import validateHandlers from '../../utils/validate-handlers'
import isAllowedOperation from '../../utils/is-allowed-operation'
const customerEndpoint: GetAPISchema< const customerEndpoint: GetAPISchema<
any, any,
CustomerSchema<any> CustomerSchema
>['endpoint']['handler'] = async (ctx) => { >['endpoint']['handler'] = (ctx) => {
const { req, res, handlers } = ctx const { req, res, handlers } = ctx
if ( validateHandlers(req, res, {
!isAllowedOperation(req, res, {
GET: handlers['getLoggedInCustomer'], GET: handlers['getLoggedInCustomer'],
}) })
) {
return
}
try {
const body = null const body = null
return await handlers['getLoggedInCustomer']({ ...ctx, body }) return handlers['getLoggedInCustomer']({ ...ctx, body })
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
} }
export default customerEndpoint export default customerEndpoint

View File

@ -0,0 +1,72 @@
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
import type { APIProvider, CommerceAPI } from '..'
import { getErrorResponse } from '../utils/errors'
/**
* Handles the catch-all api endpoint for the Commerce API.
* @param {CommerceAPI} commerce The Commerce API instance.
* @param endpoints An object containing the handlers for each endpoint.
*/
export default function createEndpoints<P extends APIProvider>(
commerce: CommerceAPI<P>,
endpoints: {
[key: string]: (commerce: CommerceAPI<P>) => NextApiHandler
}
) {
const paths = Object.keys(endpoints)
const handlers = paths.reduce<Record<string, NextApiHandler>>(
(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 data = await handlers[path](req, res)
/**
* If the handler returns a value but the response hasn't been sent yet, send it
*/
if (!res.headersSent) {
res.status(200).json({
data,
})
}
} catch (error) {
console.error(error)
const { status, data, errors } = getErrorResponse(error)
res.status(status).json({
data,
errors,
})
}
}
}

View File

@ -1,36 +1,20 @@
import type { LoginSchema } from '../../types/login'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..' import type { GetAPISchema } from '..'
import type { LoginSchema } from '../../types/login'
const loginEndpoint: GetAPISchema< import validateHandlers from '../utils/validate-handlers'
any,
LoginSchema<any> const loginEndpoint: GetAPISchema<any, LoginSchema>['endpoint']['handler'] = (
>['endpoint']['handler'] = async (ctx) => { ctx
) => {
const { req, res, handlers } = ctx const { req, res, handlers } = ctx
if ( validateHandlers(req, res, {
!isAllowedOperation(req, res, {
POST: handlers['login'], POST: handlers['login'],
GET: handlers['login'], GET: handlers['login'],
}) })
) {
return
}
try {
const body = req.body ?? {} const body = req.body ?? {}
return await handlers['login']({ ...ctx, body }) return handlers['login']({ ...ctx, body })
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
} }
export default loginEndpoint export default loginEndpoint

View File

@ -1,35 +1,20 @@
import type { LogoutSchema } from '../../types/logout'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..' import type { GetAPISchema } from '..'
import type { LogoutSchema } from '../../types/logout'
const logoutEndpoint: GetAPISchema<any, LogoutSchema>['endpoint']['handler'] = import validateHandlers from '../utils/validate-handlers'
async (ctx) => {
const logoutEndpoint: GetAPISchema<any, LogoutSchema>['endpoint']['handler'] = (
ctx
) => {
const { req, res, handlers } = ctx const { req, res, handlers } = ctx
if ( validateHandlers(req, res, {
!isAllowedOperation(req, res, {
GET: handlers['logout'], GET: handlers['logout'],
}) })
) {
return
}
try {
const redirectTo = req.query.redirect_to const redirectTo = req.query.redirect_to
const body = typeof redirectTo === 'string' ? { redirectTo } : {} const body = typeof redirectTo === 'string' ? { redirectTo } : {}
return await handlers['logout']({ ...ctx, body }) return handlers['logout']({ ...ctx, body })
} catch (error) { }
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default logoutEndpoint export default logoutEndpoint

View File

@ -1,36 +1,21 @@
import type { SignupSchema } from '../../types/signup'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..' import type { GetAPISchema } from '..'
import type { SignupSchema } from '../../types/signup'
const signupEndpoint: GetAPISchema<any, SignupSchema>['endpoint']['handler'] = import validateHandlers from '../utils/validate-handlers'
async (ctx) => {
const signupEndpoint: GetAPISchema<any, SignupSchema>['endpoint']['handler'] = (
ctx
) => {
const { req, res, handlers, config } = ctx const { req, res, handlers, config } = ctx
if ( validateHandlers(req, res, {
!isAllowedOperation(req, res, {
POST: handlers['signup'], POST: handlers['signup'],
}) })
) {
return
}
const { cookies } = req const { cookies } = req
const cartId = cookies[config.cartCookie] const cartId = cookies[config.cartCookie]
try {
const body = { ...req.body, cartId } const body = { ...req.body, cartId }
return await handlers['signup']({ ...ctx, body }) return handlers['signup']({ ...ctx, body })
} catch (error) { }
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default signupEndpoint export default signupEndpoint

View File

@ -1,57 +1,42 @@
import type { WishlistSchema } from '../../types/wishlist'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..' import type { GetAPISchema } from '..'
import type { WishlistSchema } from '../../types/wishlist'
import validateHandlers from '../utils/validate-handlers'
const wishlistEndpoint: GetAPISchema< const wishlistEndpoint: GetAPISchema<
any, any,
WishlistSchema<any> WishlistSchema
>['endpoint']['handler'] = async (ctx) => { >['endpoint']['handler'] = (ctx) => {
const { req, res, handlers, config } = ctx const { req, res, handlers, config } = ctx
if ( validateHandlers(req, res, {
!isAllowedOperation(req, res, {
GET: handlers['getWishlist'], GET: handlers['getWishlist'],
POST: handlers['addItem'], POST: handlers['addItem'],
DELETE: handlers['removeItem'], DELETE: handlers['removeItem'],
}) })
) {
return
}
const { cookies } = req const { cookies } = req
const customerToken = cookies[config.customerCookie] const customerToken = cookies[config.customerCookie]
try {
// Return current wishlist info // Return current wishlist info
if (req.method === 'GET') { if (req.method === 'GET') {
const body = { const body = {
customerToken, customerToken,
includeProducts: req.query.products === '1', includeProducts: !!req.query.products,
} }
return await handlers['getWishlist']({ ...ctx, body }) return handlers['getWishlist']({ ...ctx, body })
} }
// Add an item to the wishlist // Add an item to the wishlist
if (req.method === 'POST') { if (req.method === 'POST') {
const body = { ...req.body, customerToken } const body = { ...req.body, customerToken }
return await handlers['addItem']({ ...ctx, body }) return handlers['addItem']({ ...ctx, body })
} }
// Remove an item from the wishlist // Remove an item from the wishlist
if (req.method === 'DELETE') { if (req.method === 'DELETE') {
const body = { ...req.body, customerToken } const body = { ...req.body, customerToken }
return await handlers['removeItem']({ ...ctx, body }) return 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 }] })
} }
} }

View File

@ -1,4 +1,7 @@
import { ZodError } from 'zod'
import type { Response } from '@vercel/fetch' import type { Response } from '@vercel/fetch'
import { CommerceError } from '../../utils/errors'
export class CommerceAPIError extends Error { export class CommerceAPIError extends Error {
status: number status: number
@ -20,3 +23,51 @@ export class CommerceNetworkError extends Error {
this.name = 'CommerceNetworkError' this.name = 'CommerceNetworkError'
} }
} }
export const getErrorResponse = (error: unknown) => {
if (error instanceof CommerceAPIError) {
return {
status: error.status || 500,
data: error.data || null,
errors: [
{ message: 'An unexpected error ocurred with the Commerce API' },
],
}
}
if (error instanceof ZodError) {
return {
status: 400,
data: null,
errors: error.issues,
}
}
return {
status: 500,
data: null,
errors: [{ message: 'An unexpected error ocurred' }],
}
}
export const getOperationError = (operation: string, error: unknown) => {
if (error instanceof ZodError) {
return new CommerceError({
code: 'SCHEMA_VALIDATION_ERROR',
message:
`The ${operation} operation returned invalid data and has ${
error.issues.length
} parse ${error.issues.length === 1 ? 'error' : 'errors'}: \n` +
error.issues
.map(
(e, index) =>
`Error #${index + 1} ${
e.path.length > 0 ? `Path: ${e.path.join('.')}, ` : ''
}Code: ${e.code}, Message: ${e.message}`
)
.join('\n'),
})
}
return error
}

View File

@ -2,7 +2,11 @@ import type { NextApiRequest, NextApiResponse } from 'next'
import isAllowedMethod, { HTTP_METHODS } from './is-allowed-method' import isAllowedMethod, { HTTP_METHODS } from './is-allowed-method'
import { APIHandler } from './types' import { APIHandler } from './types'
export default function isAllowedOperation( /**
* Checks if the request method is allowed
* @throws Error if the method is not allowed
*/
export default function validateHandlers(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse, res: NextApiResponse,
allowedOperations: { [k in HTTP_METHODS]?: APIHandler<any, any> } allowedOperations: { [k in HTTP_METHODS]?: APIHandler<any, any> }
@ -15,5 +19,7 @@ export default function isAllowedOperation(
return arr return arr
}, []) }, [])
return isAllowedMethod(req, res, allowedMethods) if (!isAllowedMethod(req, res, allowedMethods)) {
throw new Error(`Method ${req.method} Not Allowed for this url: ${req.url}`)
}
} }

View File

@ -0,0 +1,27 @@
import { z } from 'zod'
export const getCartBodySchema = z.object({
cartId: z.string(),
})
export const cartItemBodySchema = z.object({
variantId: z.string(),
productId: z.string().optional(),
quantity: z.number().min(1).optional(),
})
export const addItemBodySchema = z.object({
cartId: z.string().optional(),
item: cartItemBodySchema,
})
export const updateItemBodySchema = z.object({
cartId: z.string(),
itemId: z.string(),
item: cartItemBodySchema,
})
export const removeItemBodySchema = z.object({
cartId: z.string(),
itemId: z.string(),
})

View File

@ -0,0 +1,18 @@
import { z } from 'zod'
export const pageSchema = z.object({
id: z.string(),
name: z.string(),
url: z.string().startsWith('/').optional(),
body: z.string(),
is_visible: z.boolean().optional(),
sort_order: z.number().optional(),
})
export const pagesPathsSchema = z.array(
z.object({
page: z.object({
path: z.string().startsWith('/'),
}),
})
)

View File

@ -0,0 +1,60 @@
import { z } from 'zod'
export const productPriceSchema = z.object({
value: z.number(),
currencyCode: z.string().max(3).optional(),
retailPrice: z.number().optional(),
})
export const productOptionSchema = z.object({
id: z.string(),
displayName: z.string(),
values: z.array(
z.object({
label: z.string(),
hexColors: z.array(z.string()).optional(),
})
),
})
export const productImageSchema = z.object({
url: z.string().url().or(z.string().startsWith('/')),
alt: z.string().optional(),
width: z.number().optional(),
height: z.number().optional(),
})
export const productVariantSchema = z.object({
id: z.string(),
sku: z.string().nullish(),
name: z.string().optional(),
options: z.array(productOptionSchema),
image: productImageSchema.optional(),
})
export const productSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string(),
descriptionHtml: z.string().optional(),
sku: z.string().nullish(),
slug: z.string(),
path: z.string().startsWith('/'),
images: z.array(productImageSchema),
variants: z.array(productVariantSchema),
price: productPriceSchema,
options: z.array(productOptionSchema),
vendor: z.string().optional(),
})
export const productsPathsSchema = z.array(
z.object({ path: z.string().startsWith('/') })
)
export const searchProductBodySchema = z.object({
search: z.string().optional(),
categoryId: z.string().optional(),
brandId: z.string().optional(),
sort: z.string().optional(),
locale: z.string().optional(),
})

View File

@ -0,0 +1,18 @@
import { z } from 'zod'
export const siteInfoSchema = z.object({
categories: z.array(
z.object({
id: z.string(),
name: z.string(),
path: z.string().startsWith('/'),
})
),
brands: z.array(
z.object({
id: z.string(),
name: z.string(),
path: z.string().startsWith('/'),
})
),
})

View File

@ -158,11 +158,11 @@ export type CartHandlers<T extends CartTypes = CartTypes> = {
} }
export type GetCartHandler<T extends CartTypes = CartTypes> = GetCartHook<T> & { export type GetCartHandler<T extends CartTypes = CartTypes> = GetCartHook<T> & {
body: { cartId?: string } body: { cartId: string }
} }
export type AddItemHandler<T extends CartTypes = CartTypes> = AddItemHook<T> & { export type AddItemHandler<T extends CartTypes = CartTypes> = AddItemHook<T> & {
body: { cartId: string } body: { cartId?: string }
} }
export type UpdateItemHandler<T extends CartTypes = CartTypes> = export type UpdateItemHandler<T extends CartTypes = CartTypes> =

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -0,0 +1,16 @@
import type { CommercejsAPI } from '..'
import handleEndpoints from '@vercel/commerce/api/endpoints'
import login from './login'
import checkout from './checkout'
const endpoints = {
login,
checkout,
}
const handler = (commerce: CommercejsAPI) =>
handleEndpoints(commerce, endpoints)
export default handler

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -11,7 +11,7 @@ export default useSubmitCheckout as UseSubmitCheckout<typeof handler>
export const handler: MutationHook<SubmitCheckoutHook> = { export const handler: MutationHook<SubmitCheckoutHook> = {
fetchOptions: { fetchOptions: {
url: '/api/checkout', url: '/api/commerce/checkout',
method: 'POST', method: 'POST',
}, },
async fetcher({ input: item, options, fetch }) { async fetcher({ input: item, options, fetch }) {

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -0,0 +1,26 @@
import type { KiboCommerceAPI } from '..'
import handleEndpoints from '@vercel/commerce/api/endpoints'
import cart from './cart'
import login from './login'
import logout from './logout'
import signup from './signup'
import customer from './customer'
import wishlist from './wishlist'
import products from './catalog/products'
const endpoints = {
cart,
login,
logout,
signup,
wishlist,
customer,
'catalog/products': products,
}
const handler = (commerce: KiboCommerceAPI) =>
handleEndpoints(commerce, endpoints)
export default handler

View File

@ -10,7 +10,7 @@ export default useLogin as UseLogin<typeof handler>
export const handler: MutationHook<LoginHook> = { export const handler: MutationHook<LoginHook> = {
fetchOptions: { fetchOptions: {
url: '/api/login', url: '/api/commerce/login',
method: 'POST', method: 'POST',
}, },
async fetcher({ input: { email, password }, options, fetch }) { async fetcher({ input: { email, password }, options, fetch }) {

View File

@ -9,7 +9,7 @@ export default useLogout as UseLogout<typeof handler>
export const handler: MutationHook<LogoutHook> = { export const handler: MutationHook<LogoutHook> = {
fetchOptions: { fetchOptions: {
url: '/api/logout', url: '/api/commerce/logout',
method: 'GET', method: 'GET',
}, },
useHook: ({ fetch }) => () => { useHook: ({ fetch }) => () => {

View File

@ -9,7 +9,7 @@ export default useSignup as UseSignup<typeof handler>
export const handler: MutationHook<SignupHook> = { export const handler: MutationHook<SignupHook> = {
fetchOptions: { fetchOptions: {
url: '/api/signup', url: '/api/commerce/signup',
method: 'POST', method: 'POST',
}, },
async fetcher({ async fetcher({

View File

@ -9,7 +9,7 @@ export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = { export const handler: MutationHook<AddItemHook> = {
fetchOptions: { fetchOptions: {
url: '/api/cart', url: '/api/commerce/cart',
method: 'POST', method: 'POST',
}, },
async fetcher({ input: item, options, fetch }) { async fetcher({ input: item, options, fetch }) {
@ -29,7 +29,9 @@ export const handler: MutationHook<AddItemHook> = {
return data return data
}, },
useHook: ({ fetch }) => () => { useHook:
({ fetch }) =>
() => {
const { mutate } = useCart() const { mutate } = useCart()
return useCallback( return useCallback(

View File

@ -7,12 +7,14 @@ export default useCart as UseCart<typeof handler>
export const handler: SWRHook<any> = { export const handler: SWRHook<any> = {
fetchOptions: { fetchOptions: {
method: 'GET', method: 'GET',
url: '/api/cart', url: '/api/commerce/cart',
}, },
async fetcher({ options, fetch }) { async fetcher({ options, fetch }) {
return await fetch({ ...options }) return await fetch({ ...options })
}, },
useHook: ({ useData }) => (input) => { useHook:
({ useData }) =>
(input) => {
const response = useData({ const response = useData({
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions }, swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
}) })

View File

@ -4,8 +4,14 @@ import type {
HookFetcherContext, HookFetcherContext,
} from '@vercel/commerce/utils/types' } from '@vercel/commerce/utils/types'
import { ValidationError } from '@vercel/commerce/utils/errors' import { ValidationError } from '@vercel/commerce/utils/errors'
import useRemoveItem, { UseRemoveItem } from '@vercel/commerce/cart/use-remove-item' import useRemoveItem, {
import type { Cart, LineItem, RemoveItemHook } from '@vercel/commerce/types/cart' UseRemoveItem,
} from '@vercel/commerce/cart/use-remove-item'
import type {
Cart,
LineItem,
RemoveItemHook,
} from '@vercel/commerce/types/cart'
import useCart from './use-cart' import useCart from './use-cart'
export type RemoveItemFn<T = any> = T extends LineItem export type RemoveItemFn<T = any> = T extends LineItem
@ -20,7 +26,7 @@ export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler = { export const handler = {
fetchOptions: { fetchOptions: {
url: '/api/cart', url: '/api/commerce/cart',
method: 'DELETE', method: 'DELETE',
}, },
async fetcher({ async fetcher({
@ -30,11 +36,9 @@ export const handler = {
}: HookFetcherContext<RemoveItemHook>) { }: HookFetcherContext<RemoveItemHook>) {
return await fetch({ ...options, body: { itemId } }) return await fetch({ ...options, body: { itemId } })
}, },
useHook: ({ fetch }: MutationHookContext<RemoveItemHook>) => < useHook:
T extends LineItem | undefined = undefined ({ fetch }: MutationHookContext<RemoveItemHook>) =>
>( <T extends LineItem | undefined = undefined>(ctx: { item?: T } = {}) => {
ctx: { item?: T } = {}
) => {
const { item } = ctx const { item } = ctx
const { mutate } = useCart() const { mutate } = useCart()
const removeItem: RemoveItemFn<LineItem> = async (input) => { const removeItem: RemoveItemFn<LineItem> = async (input) => {

View File

@ -5,7 +5,9 @@ import type {
HookFetcherContext, HookFetcherContext,
} from '@vercel/commerce/utils/types' } from '@vercel/commerce/utils/types'
import { ValidationError } from '@vercel/commerce/utils/errors' import { ValidationError } from '@vercel/commerce/utils/errors'
import useUpdateItem, { UseUpdateItem } from '@vercel/commerce/cart/use-update-item' import useUpdateItem, {
UseUpdateItem,
} from '@vercel/commerce/cart/use-update-item'
import type { LineItem, UpdateItemHook } from '@vercel/commerce/types/cart' import type { LineItem, UpdateItemHook } from '@vercel/commerce/types/cart'
import { handler as removeItemHandler } from './use-remove-item' import { handler as removeItemHandler } from './use-remove-item'
import useCart from './use-cart' import useCart from './use-cart'
@ -18,7 +20,7 @@ export default useUpdateItem as UseUpdateItem<typeof handler>
export const handler = { export const handler = {
fetchOptions: { fetchOptions: {
url: '/api/cart', url: '/api/commerce/cart',
method: 'PUT', method: 'PUT',
}, },
async fetcher({ async fetcher({
@ -46,9 +48,9 @@ export const handler = {
body: { itemId, item }, body: { itemId, item },
}) })
}, },
useHook: ({ fetch }: MutationHookContext<UpdateItemHook>) => < useHook:
T extends LineItem | undefined = undefined ({ fetch }: MutationHookContext<UpdateItemHook>) =>
>( <T extends LineItem | undefined = undefined>(
ctx: { ctx: {
item?: T item?: T
wait?: number wait?: number

View File

@ -6,7 +6,7 @@ export default useCustomer as UseCustomer<typeof handler>
export const handler: SWRHook<CustomerHook> = { export const handler: SWRHook<CustomerHook> = {
fetchOptions: { fetchOptions: {
url: '/api/customer', url: '/api/commerce/customer',
method: 'GET', method: 'GET',
}, },
async fetcher({ options, fetch }) { async fetcher({ options, fetch }) {

View File

@ -5,7 +5,7 @@ export default useSearch as UseSearch<typeof handler>
export const handler: SWRHook<any> = { export const handler: SWRHook<any> = {
fetchOptions: { fetchOptions: {
method: 'GET', method: 'GET',
url: '/api/catalog/products', url: '/api/commerce/catalog/products',
}, },
fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) { fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) {
// Use a dummy base as we only care about the relative path // Use a dummy base as we only care about the relative path
@ -23,7 +23,9 @@ export const handler: SWRHook<any> = {
method: options.method, method: options.method,
}) })
}, },
useHook: ({ useData }) => (input) => { useHook:
({ useData }) =>
(input) => {
return useData({ return useData({
input: [ input: [
['search', input.search], ['search', input.search],

View File

@ -10,7 +10,7 @@ export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = { export const handler: MutationHook<AddItemHook> = {
fetchOptions: { fetchOptions: {
url: '/api/wishlist', url: '/api/commerce/wishlist',
method: 'POST', method: 'POST',
}, },
useHook: useHook:

View File

@ -12,7 +12,7 @@ export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler: MutationHook<RemoveItemHook> = { export const handler: MutationHook<RemoveItemHook> = {
fetchOptions: { fetchOptions: {
url: '/api/wishlist', url: '/api/commerce/wishlist',
method: 'DELETE', method: 'DELETE',
}, },
useHook: useHook:

View File

@ -1,6 +1,8 @@
import { useMemo } from 'react' import { useMemo } from 'react'
import { SWRHook } from '@vercel/commerce/utils/types' import { SWRHook } from '@vercel/commerce/utils/types'
import useWishlist, { UseWishlist } from '@vercel/commerce/wishlist/use-wishlist' import useWishlist, {
UseWishlist,
} from '@vercel/commerce/wishlist/use-wishlist'
import type { GetWishlistHook } from '@vercel/commerce/types/wishlist' import type { GetWishlistHook } from '@vercel/commerce/types/wishlist'
import useCustomer from '../customer/use-customer' import useCustomer from '../customer/use-customer'
@ -8,23 +10,25 @@ export default useWishlist as UseWishlist<typeof handler>
export const handler: SWRHook<any> = { export const handler: SWRHook<any> = {
fetchOptions: { fetchOptions: {
url: '/api/wishlist', url: '/api/commerce/wishlist',
method: 'GET', method: 'GET',
}, },
fetcher({ input: { customerId, includeProducts}, options, fetch }) { fetcher({ input: { customerId, includeProducts }, options, fetch }) {
if (!customerId) return null if (!customerId) return null
// Use a dummy base as we only care about the relative path // Use a dummy base as we only care about the relative path
const url = new URL(options.url!, 'http://a') const url = new URL(options.url!, 'http://a')
if (includeProducts) url.searchParams.set('products', '1') if (includeProducts) url.searchParams.set('products', '1')
if(customerId) url.searchParams.set('customerId', customerId) if (customerId) url.searchParams.set('customerId', customerId)
return fetch({ return fetch({
url: url.pathname + url.search, url: url.pathname + url.search,
method: options.method, method: options.method,
}) })
}, },
useHook: ({ useData }) => (input) => { useHook:
({ useData }) =>
(input) => {
const { data: customer } = useCustomer() const { data: customer } = useCustomer()
const response = useData({ const response = useData({
input: [ input: [

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -11,14 +11,6 @@ const addItem: CartEndpoint['handlers']['addItem'] = async ({
body: { cartId, item }, body: { cartId, item },
config: { restBuyerFetch, cartCookie, tokenCookie }, config: { restBuyerFetch, cartCookie, tokenCookie },
}) => { }) => {
// Return an error if no item is present
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
// Store token // Store token
let token let token
@ -46,7 +38,7 @@ const addItem: CartEndpoint['handlers']['addItem'] = async ({
path: '/', path: '/',
sameSite: 'lax', sameSite: 'lax',
}), }),
serialize(cartCookie, cartId, { serialize(cartCookie, cartId!, {
maxAge: 60 * 60 * 24 * 30, maxAge: 60 * 60 * 24 * 30,
expires: new Date(Date.now() + 60 * 60 * 24 * 30 * 1000), expires: new Date(Date.now() + 60 * 60 * 24 * 30 * 1000),
secure: process.env.NODE_ENV === 'production', secure: process.env.NODE_ENV === 'production',

View File

@ -1,5 +1,5 @@
import type { CartSchema } from '../../../types/cart'
import type { OrdercloudAPI } from '../..' import type { OrdercloudAPI } from '../..'
import type { CartSchema } from '@vercel/commerce/types/cart'
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api' import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
import cartEndpoint from '@vercel/commerce/api/endpoints/cart' import cartEndpoint from '@vercel/commerce/api/endpoints/cart'
@ -9,9 +9,8 @@ import addItem from './add-item'
import updateItem from './update-item' import updateItem from './update-item'
import removeItem from './remove-item' import removeItem from './remove-item'
export type CartAPI = GetAPISchema<OrdercloudAPI, CartSchema>
export type CartEndpoint = CartAPI['endpoint'] export type CartEndpoint = CartAPI['endpoint']
export type CartAPI = GetAPISchema<OrdercloudAPI, CartSchema>
export const handlers: CartEndpoint['handlers'] = { export const handlers: CartEndpoint['handlers'] = {
getCart, getCart,

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -0,0 +1,22 @@
import type { OrdercloudAPI } from '..'
import handleEndpoints from '@vercel/commerce/api/endpoints'
import cart from './cart'
import checkout from './checkout'
import products from './catalog/products'
import customerCard from './customer/card'
import customerAddress from './customer/address'
const endpoints = {
cart,
checkout,
'customer/card': customerCard,
'customer/address': customerAddress,
'catalog/products': products,
}
const handler = (commerce: OrdercloudAPI) =>
handleEndpoints(commerce, endpoints)
export default handler

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1 +0,0 @@
export default function noopApi(...args: any[]): void {}

View File

@ -10,7 +10,7 @@ export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = { export const handler: MutationHook<AddItemHook> = {
fetchOptions: { fetchOptions: {
url: '/api/cart', url: '/api/commerce/cart',
method: 'POST', method: 'POST',
}, },
async fetcher({ input: item, options, fetch }) { async fetcher({ input: item, options, fetch }) {

View File

@ -8,7 +8,7 @@ export default useCart as UseCart<typeof handler>
export const handler: SWRHook<GetCartHook> = { export const handler: SWRHook<GetCartHook> = {
fetchOptions: { fetchOptions: {
url: '/api/cart', url: '/api/commerce/cart',
method: 'GET', method: 'GET',
}, },
useHook: ({ useData }) => useHook: ({ useData }) =>

View File

@ -2,12 +2,18 @@ import type {
MutationHookContext, MutationHookContext,
HookFetcherContext, HookFetcherContext,
} from '@vercel/commerce/utils/types' } from '@vercel/commerce/utils/types'
import type { Cart, LineItem, RemoveItemHook } from '@vercel/commerce/types/cart' import type {
Cart,
LineItem,
RemoveItemHook,
} from '@vercel/commerce/types/cart'
import { useCallback } from 'react' import { useCallback } from 'react'
import { ValidationError } from '@vercel/commerce/utils/errors' import { ValidationError } from '@vercel/commerce/utils/errors'
import useRemoveItem, { UseRemoveItem } from '@vercel/commerce/cart/use-remove-item' import useRemoveItem, {
UseRemoveItem,
} from '@vercel/commerce/cart/use-remove-item'
import useCart from './use-cart' import useCart from './use-cart'
@ -23,7 +29,7 @@ export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler = { export const handler = {
fetchOptions: { fetchOptions: {
url: '/api/cart', url: '/api/commerce/cart',
method: 'DELETE', method: 'DELETE',
}, },
async fetcher({ async fetcher({

View File

@ -9,7 +9,9 @@ import debounce from 'lodash.debounce'
import { MutationHook } from '@vercel/commerce/utils/types' import { MutationHook } from '@vercel/commerce/utils/types'
import { ValidationError } from '@vercel/commerce/utils/errors' import { ValidationError } from '@vercel/commerce/utils/errors'
import useUpdateItem, { UseUpdateItem } from '@vercel/commerce/cart/use-update-item' import useUpdateItem, {
UseUpdateItem,
} from '@vercel/commerce/cart/use-update-item'
import { handler as removeItemHandler } from './use-remove-item' import { handler as removeItemHandler } from './use-remove-item'
import useCart from './use-cart' import useCart from './use-cart'
@ -22,7 +24,7 @@ export default useUpdateItem as UseUpdateItem<any>
export const handler: MutationHook<any> = { export const handler: MutationHook<any> = {
fetchOptions: { fetchOptions: {
url: '/api/cart', url: '/api/commerce/cart',
method: 'PUT', method: 'PUT',
}, },
async fetcher({ async fetcher({

View File

@ -9,7 +9,7 @@ export default useCheckout as UseCheckout<typeof handler>
export const handler: SWRHook<GetCheckoutHook> = { export const handler: SWRHook<GetCheckoutHook> = {
fetchOptions: { fetchOptions: {
url: '/api/checkout', url: '/api/commerce/checkout',
method: 'GET', method: 'GET',
}, },
useHook: ({ useData }) => useHook: ({ useData }) =>

View File

@ -10,7 +10,7 @@ export default useSubmitCheckout as UseSubmitCheckout<typeof handler>
export const handler: MutationHook<SubmitCheckoutHook> = { export const handler: MutationHook<SubmitCheckoutHook> = {
fetchOptions: { fetchOptions: {
url: '/api/checkout', url: '/api/commerce/checkout',
method: 'POST', method: 'POST',
}, },
async fetcher({ input: item, options, fetch }) { async fetcher({ input: item, options, fetch }) {

View File

@ -2,14 +2,16 @@ import type { AddItemHook } from '@vercel/commerce/types/customer/address'
import type { MutationHook } from '@vercel/commerce/utils/types' import type { MutationHook } from '@vercel/commerce/utils/types'
import { useCallback } from 'react' import { useCallback } from 'react'
import useAddItem, { UseAddItem } from '@vercel/commerce/customer/address/use-add-item' import useAddItem, {
UseAddItem,
} from '@vercel/commerce/customer/address/use-add-item'
import useAddresses from './use-addresses' import useAddresses from './use-addresses'
export default useAddItem as UseAddItem<typeof handler> export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = { export const handler: MutationHook<AddItemHook> = {
fetchOptions: { fetchOptions: {
url: '/api/customer/address', url: '/api/commerce/customer/address',
method: 'POST', method: 'POST',
}, },
async fetcher({ input: item, options, fetch }) { async fetcher({ input: item, options, fetch }) {

View File

@ -10,7 +10,7 @@ export default useAddresses as UseAddresses<typeof handler>
export const handler: SWRHook<GetAddressesHook> = { export const handler: SWRHook<GetAddressesHook> = {
fetchOptions: { fetchOptions: {
url: '/api/customer/address', url: '/api/commerce/customer/address',
method: 'GET', method: 'GET',
}, },
useHook: ({ useData }) => useHook: ({ useData }) =>

View File

@ -2,7 +2,10 @@ import type {
MutationHookContext, MutationHookContext,
HookFetcherContext, HookFetcherContext,
} from '@vercel/commerce/utils/types' } from '@vercel/commerce/utils/types'
import type { Address, RemoveItemHook } from '@vercel/commerce/types/customer/address' import type {
Address,
RemoveItemHook,
} from '@vercel/commerce/types/customer/address'
import { useCallback } from 'react' import { useCallback } from 'react'
@ -25,7 +28,7 @@ export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler = { export const handler = {
fetchOptions: { fetchOptions: {
url: '/api/customer/address', url: '/api/commerce/customer/address',
method: 'DELETE', method: 'DELETE',
}, },
async fetcher({ async fetcher({

View File

@ -2,7 +2,10 @@ import type {
HookFetcherContext, HookFetcherContext,
MutationHookContext, MutationHookContext,
} from '@vercel/commerce/utils/types' } from '@vercel/commerce/utils/types'
import type { UpdateItemHook, Address } from '@vercel/commerce/types/customer/address' import type {
UpdateItemHook,
Address,
} from '@vercel/commerce/types/customer/address'
import { useCallback } from 'react' import { useCallback } from 'react'
@ -21,7 +24,7 @@ export default useUpdateItem as UseUpdateItem<any>
export const handler: MutationHook<any> = { export const handler: MutationHook<any> = {
fetchOptions: { fetchOptions: {
url: '/api/customer/address', url: '/api/commerce/customer/address',
method: 'PUT', method: 'PUT',
}, },
async fetcher({ async fetcher({

View File

@ -2,14 +2,16 @@ import type { AddItemHook } from '@vercel/commerce/types/customer/card'
import type { MutationHook } from '@vercel/commerce/utils/types' import type { MutationHook } from '@vercel/commerce/utils/types'
import { useCallback } from 'react' import { useCallback } from 'react'
import useAddItem, { UseAddItem } from '@vercel/commerce/customer/card/use-add-item' import useAddItem, {
UseAddItem,
} from '@vercel/commerce/customer/card/use-add-item'
import useCards from './use-cards' import useCards from './use-cards'
export default useAddItem as UseAddItem<typeof handler> export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = { export const handler: MutationHook<AddItemHook> = {
fetchOptions: { fetchOptions: {
url: '/api/customer/card', url: '/api/commerce/customer/card',
method: 'POST', method: 'POST',
}, },
async fetcher({ input: item, options, fetch }) { async fetcher({ input: item, options, fetch }) {

View File

@ -8,7 +8,7 @@ export default useCard as UseCards<typeof handler>
export const handler: SWRHook<GetCardsHook> = { export const handler: SWRHook<GetCardsHook> = {
fetchOptions: { fetchOptions: {
url: '/api/customer/card', url: '/api/commerce/customer/card',
method: 'GET', method: 'GET',
}, },
useHook: ({ useData }) => useHook: ({ useData }) =>

View File

@ -25,7 +25,7 @@ export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler = { export const handler = {
fetchOptions: { fetchOptions: {
url: '/api/customer/card', url: '/api/commerce/customer/card',
method: 'DELETE', method: 'DELETE',
}, },
async fetcher({ async fetcher({

View File

@ -21,7 +21,7 @@ export default useUpdateItem as UseUpdateItem<any>
export const handler: MutationHook<any> = { export const handler: MutationHook<any> = {
fetchOptions: { fetchOptions: {
url: '/api/customer/card', url: '/api/commerce/customer/card',
method: 'PUT', method: 'PUT',
}, },
async fetcher({ async fetcher({

View File

@ -5,7 +5,7 @@ export default useSearch as UseSearch<typeof handler>
export const handler: SWRHook<SearchProductsHook> = { export const handler: SWRHook<SearchProductsHook> = {
fetchOptions: { fetchOptions: {
url: '/api/catalog/products', url: '/api/commerce/catalog/products',
method: 'GET', method: 'GET',
}, },
fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) { fetcher({ input: { search, categoryId, brandId, sort }, options, fetch }) {

View File

@ -1 +0,0 @@
export default function (_commerce: any) {}

Some files were not shown because too many files have changed in this diff Show More