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 },
config,
}) => {
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
if (!item.quantity) item.quantity = 1
const options = {
@ -41,7 +35,7 @@ const addItem: CartEndpoint['handlers']['addItem'] = async ({
'Set-Cookie',
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

View File

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

View File

@ -7,13 +7,6 @@ const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
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' }
@ -28,7 +21,8 @@ const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
: // Remove the cart cookie if the cart was removed (empty items)
getCartCookie(config.cartCookie)
)
res.status(200).json({ data: data && normalizeCart(data) })
res.status(200).json({ data: data ? normalizeCart(data) : null })
}
export default removeItem

View File

@ -8,14 +8,7 @@ const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
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?: any }>(
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
{
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> = {
fetchOptions: {
url: '/api/login',
url: '/api/commerce/login',
method: 'POST',
},
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> = {
fetchOptions: {
url: '/api/logout',
url: '/api/commerce/logout',
method: 'GET',
},
useHook:

View File

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

View File

@ -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 }) {

View File

@ -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:

View File

@ -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({

View File

@ -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({

View File

@ -6,7 +6,7 @@ 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 }) {

View File

@ -14,7 +14,7 @@ export type SearchProductsInput = {
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 }) {

View File

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

View File

@ -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:

View File

@ -10,7 +10,7 @@ import type { GetWishlistHook } from '../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 }) {

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:
```tsx
import { getCommerceProvider, useCommerce as useCoreCommerce } from '@vercel/commerce'
import {
getCommerceProvider,
useCommerce as useCoreCommerce,
} from '@vercel/commerce'
import { bigcommerceProvider, BigcommerceProvider } from './provider'
export { bigcommerceProvider }
@ -135,7 +138,7 @@ export default useCart as UseCart<typeof handler>
export const handler: SWRHook<GetCartHook> = {
fetchOptions: {
url: '/api/cart',
url: '/api/commerce/cart',
method: 'GET',
},
useHook:
@ -175,7 +178,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 }) {
@ -213,25 +216,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

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

View File

@ -1,31 +1,26 @@
import type { ProductsSchema } from '../../../types/product'
import { CommerceAPIError } from '../../utils/errors'
import isAllowedOperation from '../../utils/is-allowed-operation'
import { searchProductBodySchema } from '../../../schemas/product'
import type { GetAPISchema } from '../..'
import type { ProductsSchema } from '../../../types/product'
import validateHandlers from '../../utils/validate-handlers'
const productsEndpoint: GetAPISchema<
any,
ProductsSchema
>['endpoint']['handler'] = async (ctx) => {
>['endpoint']['handler'] = (ctx) => {
const { req, res, handlers } = ctx
if (!isAllowedOperation(req, res, { GET: handlers['getProducts'] })) {
return
}
validateHandlers(req, res, { GET: handlers['getProducts'] })
try {
const body = req.query
return await handlers['getProducts']({ ...ctx, body })
} catch (error) {
console.error(error)
const body = searchProductBodySchema.parse({
search: req.query.search,
categoryId: req.query.categoryId,
brandId: req.query.brandId,
sort: req.query.sort,
locale: req.query.locale,
})
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 handlers['getProducts']({ ...ctx, body })
}
export default productsEndpoint

View File

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

View File

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

View File

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

View File

@ -1,36 +1,20 @@
import type { CustomerSchema } from '../../../types/customer'
import type { GetAPISchema } from '../..'
import { CommerceAPIError } from '../../utils/errors'
import isAllowedOperation from '../../utils/is-allowed-operation'
import validateHandlers from '../../utils/validate-handlers'
const customerEndpoint: GetAPISchema<
any,
CustomerSchema<any>
>['endpoint']['handler'] = async (ctx) => {
CustomerSchema
>['endpoint']['handler'] = (ctx) => {
const { req, res, handlers } = ctx
if (
!isAllowedOperation(req, res, {
GET: handlers['getLoggedInCustomer'],
})
) {
return
}
validateHandlers(req, res, {
GET: handlers['getLoggedInCustomer'],
})
try {
const body = null
return await 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 }] })
}
const body = null
return handlers['getLoggedInCustomer']({ ...ctx, body })
}
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 { LoginSchema } from '../../types/login'
const loginEndpoint: GetAPISchema<
any,
LoginSchema<any>
>['endpoint']['handler'] = async (ctx) => {
import validateHandlers from '../utils/validate-handlers'
const loginEndpoint: GetAPISchema<any, LoginSchema>['endpoint']['handler'] = (
ctx
) => {
const { req, res, handlers } = ctx
if (
!isAllowedOperation(req, res, {
POST: handlers['login'],
GET: handlers['login'],
})
) {
return
}
validateHandlers(req, res, {
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 body = req.body ?? {}
return handlers['login']({ ...ctx, body })
}
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 { LogoutSchema } from '../../types/logout'
const logoutEndpoint: GetAPISchema<any, LogoutSchema>['endpoint']['handler'] =
async (ctx) => {
const { req, res, handlers } = ctx
import validateHandlers from '../utils/validate-handlers'
if (
!isAllowedOperation(req, res, {
GET: handlers['logout'],
})
) {
return
}
const logoutEndpoint: GetAPISchema<any, LogoutSchema>['endpoint']['handler'] = (
ctx
) => {
const { req, res, handlers } = ctx
try {
const redirectTo = req.query.redirect_to
const body = typeof redirectTo === 'string' ? { redirectTo } : {}
validateHandlers(req, res, {
GET: handlers['logout'],
})
const redirectTo = req.query.redirect_to
const body = typeof redirectTo === 'string' ? { redirectTo } : {}
return await 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 }] })
}
}
return handlers['logout']({ ...ctx, body })
}
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 { SignupSchema } from '../../types/signup'
const signupEndpoint: GetAPISchema<any, SignupSchema>['endpoint']['handler'] =
async (ctx) => {
const { req, res, handlers, config } = ctx
import validateHandlers from '../utils/validate-handlers'
if (
!isAllowedOperation(req, res, {
POST: handlers['signup'],
})
) {
return
}
const signupEndpoint: GetAPISchema<any, SignupSchema>['endpoint']['handler'] = (
ctx
) => {
const { req, res, handlers, config } = ctx
const { cookies } = req
const cartId = cookies[config.cartCookie]
validateHandlers(req, res, {
POST: handlers['signup'],
})
const { cookies } = req
const cartId = cookies[config.cartCookie]
try {
const body = { ...req.body, cartId }
return await handlers['signup']({ ...ctx, body })
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
const body = { ...req.body, cartId }
return handlers['signup']({ ...ctx, body })
}
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 { WishlistSchema } from '../../types/wishlist'
import validateHandlers from '../utils/validate-handlers'
const wishlistEndpoint: GetAPISchema<
any,
WishlistSchema<any>
>['endpoint']['handler'] = async (ctx) => {
WishlistSchema
>['endpoint']['handler'] = (ctx) => {
const { req, res, handlers, config } = ctx
if (
!isAllowedOperation(req, res, {
GET: handlers['getWishlist'],
POST: handlers['addItem'],
DELETE: handlers['removeItem'],
})
) {
return
}
validateHandlers(req, res, {
GET: handlers['getWishlist'],
POST: handlers['addItem'],
DELETE: handlers['removeItem'],
})
const { cookies } = req
const customerToken = cookies[config.customerCookie]
try {
// Return current wishlist info
if (req.method === 'GET') {
const body = {
customerToken,
includeProducts: req.query.products === '1',
}
return await handlers['getWishlist']({ ...ctx, body })
// Return current wishlist info
if (req.method === 'GET') {
const body = {
customerToken,
includeProducts: !!req.query.products,
}
return handlers['getWishlist']({ ...ctx, body })
}
// Add an item to the wishlist
if (req.method === 'POST') {
const body = { ...req.body, customerToken }
return await handlers['addItem']({ ...ctx, body })
}
// Add an item to the wishlist
if (req.method === 'POST') {
const body = { ...req.body, customerToken }
return 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 }] })
// Remove an item from the wishlist
if (req.method === 'DELETE') {
const body = { ...req.body, customerToken }
return handlers['removeItem']({ ...ctx, body })
}
}

View File

@ -1,4 +1,7 @@
import { ZodError } from 'zod'
import type { Response } from '@vercel/fetch'
import { CommerceError } from '../../utils/errors'
export class CommerceAPIError extends Error {
status: number
@ -20,3 +23,51 @@ export class CommerceNetworkError extends Error {
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 { 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,
res: NextApiResponse,
allowedOperations: { [k in HTTP_METHODS]?: APIHandler<any, any> }
@ -15,5 +19,7 @@ export default function isAllowedOperation(
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> & {
body: { cartId?: string }
body: { cartId: string }
}
export type AddItemHandler<T extends CartTypes = CartTypes> = AddItemHook<T> & {
body: { cartId: string }
body: { cartId?: string }
}
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> = {
fetchOptions: {
url: '/api/checkout',
url: '/api/commerce/checkout',
method: 'POST',
},
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> = {
fetchOptions: {
url: '/api/login',
url: '/api/commerce/login',
method: 'POST',
},
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> = {
fetchOptions: {
url: '/api/logout',
url: '/api/commerce/logout',
method: 'GET',
},
useHook: ({ fetch }) => () => {

View File

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

View File

@ -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 }) {
@ -29,16 +29,18 @@ export const handler: MutationHook<AddItemHook> = {
return data
},
useHook: ({ fetch }) => () => {
const { mutate } = useCart()
useHook:
({ fetch }) =>
() => {
const { mutate } = useCart()
return useCallback(
async function addItem(input) {
const data = await fetch({ input })
await mutate(data, false)
return data
},
[fetch, mutate]
)
},
return useCallback(
async function addItem(input) {
const data = await fetch({ input })
await mutate(data, false)
return data
},
[fetch, mutate]
)
},
}

View File

@ -7,27 +7,29 @@ export default useCart as UseCart<typeof handler>
export const handler: SWRHook<any> = {
fetchOptions: {
method: 'GET',
url: '/api/cart',
url: '/api/commerce/cart',
},
async fetcher({ options, fetch }) {
return await fetch({ ...options })
},
useHook: ({ useData }) => (input) => {
const response = useData({
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
})
useHook:
({ useData }) =>
(input) => {
const response = useData({
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
})
return useMemo(
() =>
Object.create(response, {
isEmpty: {
get() {
return (response.data?.lineItems.length ?? 0) <= 0
return useMemo(
() =>
Object.create(response, {
isEmpty: {
get() {
return (response.data?.lineItems.length ?? 0) <= 0
},
enumerable: true,
},
enumerable: true,
},
}),
[response]
)
},
}),
[response]
)
},
}

View File

@ -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({
@ -30,27 +36,25 @@ export const handler = {
}: HookFetcherContext<RemoveItemHook>) {
return await fetch({ ...options, body: { itemId } })
},
useHook: ({ fetch }: MutationHookContext<RemoveItemHook>) => <
T extends LineItem | undefined = undefined
>(
ctx: { item?: T } = {}
) => {
const { item } = ctx
const { mutate } = useCart()
const removeItem: RemoveItemFn<LineItem> = async (input) => {
const itemId = input?.id ?? item?.id
useHook:
({ fetch }: MutationHookContext<RemoveItemHook>) =>
<T extends LineItem | undefined = undefined>(ctx: { item?: T } = {}) => {
const { item } = ctx
const { mutate } = useCart()
const removeItem: RemoveItemFn<LineItem> = async (input) => {
const itemId = input?.id ?? item?.id
if (!itemId) {
throw new ValidationError({
message: 'Invalid input used for this operation',
})
if (!itemId) {
throw new ValidationError({
message: 'Invalid input used for this operation',
})
}
const data = await fetch({ input: { itemId } })
await mutate(data, false)
return data
}
const data = await fetch({ input: { itemId } })
await mutate(data, false)
return data
}
return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
},
return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
},
}

View File

@ -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({
@ -46,39 +48,39 @@ export const handler = {
body: { itemId, item },
})
},
useHook: ({ fetch }: MutationHookContext<UpdateItemHook>) => <
T extends LineItem | undefined = undefined
>(
ctx: {
item?: T
wait?: number
} = {}
) => {
const { item } = ctx
const { mutate } = useCart() as any
useHook:
({ fetch }: MutationHookContext<UpdateItemHook>) =>
<T extends LineItem | undefined = undefined>(
ctx: {
item?: T
wait?: number
} = {}
) => {
const { item } = ctx
const { mutate } = useCart() as any
return useCallback(
debounce(async (input: UpdateItemActionInput<T>) => {
const itemId = input.id ?? item?.id
const productId = input.productId ?? item?.productId
const variantId = input.productId ?? item?.variantId
return useCallback(
debounce(async (input: UpdateItemActionInput<T>) => {
const itemId = input.id ?? item?.id
const productId = input.productId ?? item?.productId
const variantId = input.productId ?? item?.variantId
if (!itemId || !productId || !variantId) {
throw new ValidationError({
message: 'Invalid input used for this operation',
if (!itemId || !productId || !variantId) {
throw new ValidationError({
message: 'Invalid input used for this operation',
})
}
const data = await fetch({
input: {
itemId,
item: { productId, variantId, quantity: input.quantity },
},
})
}
const data = await fetch({
input: {
itemId,
item: { productId, variantId, quantity: input.quantity },
},
})
await mutate(data, false)
return data
}, ctx.wait ?? 500),
[fetch, mutate]
)
},
await mutate(data, false)
return data
}, ctx.wait ?? 500),
[fetch, mutate]
)
},
}

View File

@ -6,7 +6,7 @@ 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 }) {

View File

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

View File

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

View File

@ -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:

View File

@ -1,6 +1,8 @@
import { useMemo } from 'react'
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 useCustomer from '../customer/use-customer'
@ -8,45 +10,47 @@ export default useWishlist as UseWishlist<typeof handler>
export const handler: SWRHook<any> = {
fetchOptions: {
url: '/api/wishlist',
url: '/api/commerce/wishlist',
method: 'GET',
},
fetcher({ input: { customerId, includeProducts}, options, fetch }) {
fetcher({ input: { customerId, includeProducts }, options, fetch }) {
if (!customerId) return null
// Use a dummy base as we only care about the relative path
const url = new URL(options.url!, 'http://a')
if (includeProducts) url.searchParams.set('products', '1')
if(customerId) url.searchParams.set('customerId', customerId)
if (customerId) url.searchParams.set('customerId', customerId)
return fetch({
url: url.pathname + url.search,
method: options.method,
})
},
useHook: ({ useData }) => (input) => {
const { data: customer } = useCustomer()
const response = useData({
input: [
['customerId', customer?.id],
['includeProducts', input?.includeProducts],
],
swrOptions: {
revalidateOnFocus: false,
...input?.swrOptions,
},
})
return useMemo(
() =>
Object.create(response, {
isEmpty: {
get() {
return (response.data?.items?.length || 0) <= 0
useHook:
({ useData }) =>
(input) => {
const { data: customer } = useCustomer()
const response = useData({
input: [
['customerId', customer?.id],
['includeProducts', input?.includeProducts],
],
swrOptions: {
revalidateOnFocus: false,
...input?.swrOptions,
},
})
return useMemo(
() =>
Object.create(response, {
isEmpty: {
get() {
return (response.data?.items?.length || 0) <= 0
},
enumerable: true,
},
enumerable: true,
},
}),
[response]
)
},
}),
[response]
)
},
}

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 },
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
let token
@ -46,7 +38,7 @@ const addItem: CartEndpoint['handlers']['addItem'] = async ({
path: '/',
sameSite: 'lax',
}),
serialize(cartCookie, cartId, {
serialize(cartCookie, cartId!, {
maxAge: 60 * 60 * 24 * 30,
expires: new Date(Date.now() + 60 * 60 * 24 * 30 * 1000),
secure: process.env.NODE_ENV === 'production',

View File

@ -1,5 +1,5 @@
import type { CartSchema } from '../../../types/cart'
import type { OrdercloudAPI } from '../..'
import type { CartSchema } from '@vercel/commerce/types/cart'
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
import cartEndpoint from '@vercel/commerce/api/endpoints/cart'
@ -9,9 +9,8 @@ import addItem from './add-item'
import updateItem from './update-item'
import removeItem from './remove-item'
export type CartAPI = GetAPISchema<OrdercloudAPI, CartSchema>
export type CartEndpoint = CartAPI['endpoint']
export type CartAPI = GetAPISchema<OrdercloudAPI, CartSchema>
export const handlers: CartEndpoint['handlers'] = {
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> = {
fetchOptions: {
url: '/api/cart',
url: '/api/commerce/cart',
method: 'POST',
},
async fetcher({ input: item, options, fetch }) {

View File

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

View File

@ -2,12 +2,18 @@ import type {
MutationHookContext,
HookFetcherContext,
} 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 { 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'
@ -23,7 +29,7 @@ export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler = {
fetchOptions: {
url: '/api/cart',
url: '/api/commerce/cart',
method: 'DELETE',
},
async fetcher({

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,10 @@ import type {
MutationHookContext,
HookFetcherContext,
} 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'
@ -25,7 +28,7 @@ export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler = {
fetchOptions: {
url: '/api/customer/address',
url: '/api/commerce/customer/address',
method: 'DELETE',
},
async fetcher({

View File

@ -2,7 +2,10 @@ import type {
HookFetcherContext,
MutationHookContext,
} 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'
@ -21,7 +24,7 @@ export default useUpdateItem as UseUpdateItem<any>
export const handler: MutationHook<any> = {
fetchOptions: {
url: '/api/customer/address',
url: '/api/commerce/customer/address',
method: 'PUT',
},
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 { 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'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = {
fetchOptions: {
url: '/api/customer/card',
url: '/api/commerce/customer/card',
method: 'POST',
},
async fetcher({ input: item, options, fetch }) {

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ export default useSearch as UseSearch<typeof handler>
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 }) {

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