Dynamic API routes (#836)

* Add dynamic API endpoints

* Add missing dependency

* Update api handlers

* Updates

* Fix build errors

* Update package.json

* Add checkout endpoint parser & update errors

* Update tsconfig.json

* Update cart.ts

* Update parser

* Update errors.ts

* Update errors.ts

* Move to Edge runtime

* Revert to local

* Fix switchable runtimes

* Make nodejs default runtime

* Update pnpm-lock.yaml

* Update handlers

* Fix build errors

* Change headers
This commit is contained in:
Catalin Pinte
2022-10-30 20:41:21 +02:00
committed by GitHub
parent a5b367a747
commit c75b0fc001
316 changed files with 2482 additions and 2176 deletions

View File

@@ -50,9 +50,7 @@
},
"dependencies": {
"@vercel/commerce": "workspace:*",
"@vercel/fetch": "^6.2.0",
"lodash.debounce": "^4.0.8",
"node-fetch": "^2.6.7"
"lodash.debounce": "^4.0.8"
},
"peerDependencies": {
"next": "^12",

View File

@@ -53,28 +53,19 @@ const buildAddToCartVariables = ({
const addItem: CartEndpoint['handlers']['addItem'] = async ({
req,
res,
body: { cartId, item },
body: { item },
config,
}) => {
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
if (!item.quantity) item.quantity = 1
const productResponse = await config.fetch(getProductQuery, {
variables: { productCode: item?.productId },
})
const cookieHandler = new CookieHandler(config, req, res)
const cookieHandler = new CookieHandler(config, req)
let accessToken = null
if (!cookieHandler.getAccessToken()) {
let anonymousShopperTokenResponse = await cookieHandler.getAnonymousToken()
accessToken = anonymousShopperTokenResponse.accessToken;
accessToken = anonymousShopperTokenResponse.accessToken
} else {
accessToken = cookieHandler.getAccessToken()
}
@@ -95,7 +86,8 @@ const addItem: CartEndpoint['handlers']['addItem'] = async ({
)
currentCart = result?.data?.currentCart
}
res.status(200).json({ data: normalizeCart(currentCart) })
return { data: normalizeCart(currentCart) }
}
export default addItem

View File

@@ -6,17 +6,17 @@ import { getCartQuery } from '../../queries/get-cart-query'
const getCart: CartEndpoint['handlers']['getCart'] = async ({
req,
res,
body: { cartId },
config,
}) => {
let currentCart: Cart = {}
let headers
try {
const cookieHandler = new CookieHandler(config, req, res)
const cookieHandler = new CookieHandler(config, req)
let accessToken = null
if (!cookieHandler.getAccessToken()) {
let anonymousShopperTokenResponse = await cookieHandler.getAnonymousToken()
let anonymousShopperTokenResponse =
await cookieHandler.getAnonymousToken()
const response = anonymousShopperTokenResponse.response
accessToken = anonymousShopperTokenResponse.accessToken
cookieHandler.setAnonymousShopperCookie(response)
@@ -30,12 +30,14 @@ const getCart: CartEndpoint['handlers']['getCart'] = async ({
{ headers: { 'x-vol-user-claims': accessToken } }
)
currentCart = result?.data?.currentCart
headers = cookieHandler.headers
} catch (error) {
throw error
}
res.status(200).json({
return {
data: currentCart ? normalizeCart(currentCart) : null,
})
}
}
export default getCart

View File

@@ -1,8 +1,8 @@
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
import cartEndpoint from '@vercel/commerce/api/endpoints/cart'
import type { KiboCommerceAPI } from '../..'
import getCart from './get-cart';
import addItem from './add-item';
import getCart from './get-cart'
import addItem from './add-item'
import updateItem from './update-item'
import removeItem from './remove-item'

View File

@@ -5,17 +5,10 @@ import { getCartQuery } from '../../../api/queries/get-cart-query'
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
req,
res,
body: { cartId, itemId },
body: { itemId },
config,
}) => {
if (!itemId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const encodedToken = req.cookies[config.customerCookie]
const encodedToken = req.cookies.get(config.customerCookie)
const token = encodedToken
? Buffer.from(encodedToken, 'base64').toString('ascii')
: null
@@ -39,7 +32,10 @@ const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
)
currentCart = result?.data?.currentCart
}
res.status(200).json({ data: normalizeCart(currentCart) })
return {
data: normalizeCart(currentCart),
}
}
export default removeItem

View File

@@ -5,17 +5,10 @@ import updateCartItemQuantityMutation from '../../../api/mutations/updateCartIte
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
req,
res,
body: { cartId, itemId, item },
body: { itemId, item },
config,
}) => {
if (!itemId || !item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const encodedToken = req.cookies[config.customerCookie]
const encodedToken = req.cookies.get(config.cartCookie)
const token = encodedToken
? Buffer.from(encodedToken, 'base64').toString('ascii')
: null
@@ -39,7 +32,8 @@ const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
)
currentCart = result?.data?.currentCart
}
res.status(200).json({ data: normalizeCart(currentCart) })
return { data: normalizeCart(currentCart) }
}
export default updateItem

View File

@@ -2,16 +2,15 @@ import { Product } from '@vercel/commerce/types/product'
import { ProductsEndpoint } from '.'
import productSearchQuery from '../../../queries/product-search-query'
import { buildProductSearchVars } from '../../../../lib/product-search-vars'
import {normalizeProduct} from '../../../../lib/normalize'
import { normalizeProduct } from '../../../../lib/normalize'
const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
res,
body: { search, categoryId, brandId, sort },
config,
}) => {
const pageSize = 100;
const filters = {};
const startIndex = 0;
const pageSize = 100
const filters = {}
const startIndex = 0
const variables = buildProductSearchVars({
categoryCode: categoryId,
pageSize,
@@ -20,12 +19,14 @@ const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({
filters,
startIndex,
})
const {data} = await config.fetch(productSearchQuery, { variables });
const found = data?.products?.items?.length > 0 ? true : false;
let productsResponse= data?.products?.items.map((item: any) =>normalizeProduct(item,config));
const products: Product[] = found ? productsResponse : [];
const { data } = await config.fetch(productSearchQuery, { variables })
const found = data?.products?.items?.length > 0 ? true : false
let productsResponse = data?.products?.items.map((item: any) =>
normalizeProduct(item, config)
)
const products: Product[] = found ? productsResponse : []
res.status(200).json({ data: { products, found } });
return { data: { products, found } }
}
export default getProducts

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

@@ -2,35 +2,32 @@ import CookieHandler from '../../../api/utils/cookie-handler'
import type { CustomerEndpoint } from '.'
import { getCustomerAccountQuery } from '../../queries/get-customer-account-query'
import { normalizeCustomer } from '../../../lib/normalize'
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] = async ({
req,
res,
config,
}) => {
const cookieHandler = new CookieHandler(config, req, res)
let accessToken = cookieHandler.getAccessToken();
const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] =
async ({ req, config }) => {
const cookieHandler = new CookieHandler(config, req)
let accessToken = cookieHandler.getAccessToken()
if (!cookieHandler.isShopperCookieAnonymous()) {
const { data } = await config.fetch(getCustomerAccountQuery, undefined, {
headers: {
'x-vol-user-claims': accessToken,
},
})
const customer = normalizeCustomer(data?.customerAccount)
if (!customer.id) {
return res.status(400).json({
data: null,
errors: [{ message: 'Customer not found', code: 'not_found' }],
if (!cookieHandler.isShopperCookieAnonymous()) {
const { data } = await config.fetch(getCustomerAccountQuery, undefined, {
headers: {
'x-vol-user-claims': accessToken,
},
})
const customer = normalizeCustomer(data?.customerAccount)
if (!customer.id) {
throw new CommerceAPIError('Customer not found', {
status: 404,
})
}
return { data: { customer } }
}
return res.status(200).json({ data: { customer } })
return { data: null }
}
res.status(200).json({ data: null })
}
export default getLoggedInCustomer

View File

@@ -0,0 +1,25 @@
import type { KiboCommerceAPI } 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 customer from './customer'
import wishlist from './wishlist'
import products from './catalog/products'
const endpoints = {
cart,
login,
logout,
signup,
wishlist,
customer,
'catalog/products': products,
}
export default function kiboCommerceAPI(commerce: KiboCommerceAPI) {
return createEndpoints(commerce, endpoints)
}

View File

@@ -1,66 +1,53 @@
import { FetcherError } from '@vercel/commerce/utils/errors'
import type { LoginEndpoint } from '.'
import { FetcherError } from '@vercel/commerce/utils/errors'
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
import { loginMutation } from '../../mutations/login-mutation'
import { prepareSetCookie } from '../../../lib/prepare-set-cookie';
import { setCookies } from '../../../lib/set-cookie'
import { prepareSetCookie } from '../../../lib/prepare-set-cookie'
import { getCookieExpirationDate } from '../../../lib/get-cookie-expiration-date'
const invalidCredentials = /invalid credentials/i
const login: LoginEndpoint['handlers']['login'] = async ({
req,
res,
body: { email, password },
config,
commerce,
}) => {
if (!(email && password)) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
let response;
let response
try {
const variables = { loginInput : { username: email, password }};
response = await config.fetch(loginMutation, { variables })
const { account: token } = response.data;
const variables = { loginInput: { username: email, password } }
response = await config.fetch(loginMutation, { variables })
const { account: token } = response.data
// Set Cookie
const cookieExpirationDate = getCookieExpirationDate(config.customerCookieMaxAgeInDays)
const cookieExpirationDate = getCookieExpirationDate(
config.customerCookieMaxAgeInDays
)
const authCookie = prepareSetCookie(
config.customerCookie,
JSON.stringify(token),
token.accessTokenExpiration ? { expires: cookieExpirationDate }: {},
token.accessTokenExpiration ? { expires: cookieExpirationDate } : {}
)
setCookies(res, [authCookie])
return { data: null, headers: { 'Set-Cookie': authCookie } }
} catch (error) {
// Check if the email and password didn't match an existing account
if (
error instanceof FetcherError &&
invalidCredentials.test(error.message)
) {
return res.status(401).json({
data: null,
errors: [
{
message:
'Cannot find an account that matches the provided credentials',
code: 'invalid_credentials',
},
],
})
throw new CommerceAPIError(
'Cannot find an account that matches the provided credentials',
{
status: 401,
code: 'invalid_credentials',
}
)
} else {
throw error
}
throw error
}
res.status(200).json({ data: response })
}
export default login

View File

@@ -1,22 +1,22 @@
import type { LogoutEndpoint } from '.'
import {prepareSetCookie} from '../../../lib/prepare-set-cookie';
import {setCookies} from '../../../lib/set-cookie'
import { prepareSetCookie } from '../../../lib/prepare-set-cookie'
const logout: LogoutEndpoint['handlers']['logout'] = async ({
res,
body: { redirectTo },
config,
}) => {
// Remove the cookie
const authCookie = prepareSetCookie(config.customerCookie,'',{ maxAge: -1, path: '/' })
setCookies(res, [authCookie])
const authCookie = prepareSetCookie(config.customerCookie, '', {
maxAge: -1,
path: '/',
})
const headers = {
'Set-Cookie': authCookie,
}
// Only allow redirects to a relative URL
if (redirectTo?.startsWith('/')) {
res.redirect(redirectTo)
} else {
res.status(200).json({ data: null })
}
return redirectTo?.startsWith('/') ? { redirectTo, headers } : { headers }
}
export default logout

View File

@@ -1,91 +1,89 @@
import { FetcherError } from '@vercel/commerce/utils/errors'
import type { SignupEndpoint } from '.'
import { registerUserMutation, registerUserLoginMutation } from '../../mutations/signup-mutation'
import { prepareSetCookie } from '../../../lib/prepare-set-cookie';
import { setCookies } from '../../../lib/set-cookie'
import { FetcherError } from '@vercel/commerce/utils/errors'
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
import {
registerUserMutation,
registerUserLoginMutation,
} from '../../mutations/signup-mutation'
import { prepareSetCookie } from '../../../lib/prepare-set-cookie'
import { getCookieExpirationDate } from '../../../lib/get-cookie-expiration-date'
const invalidCredentials = /invalid credentials/i
const signup: SignupEndpoint['handlers']['signup'] = async ({
req,
res,
body: { email, password, firstName, lastName },
config,
commerce,
}) => {
if (!(email && password)) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
let response;
let response
try {
// Register user
const registerUserVariables = {
customerAccountInput: {
emailAddress: email,
firstName: firstName,
lastName: lastName,
acceptsMarketing: true,
id: 0
}
emailAddress: email,
firstName: firstName,
lastName: lastName,
acceptsMarketing: true,
id: 0,
},
}
const registerUserResponse = await config.fetch(registerUserMutation, { variables: registerUserVariables})
const accountId = registerUserResponse.data?.account?.id;
const registerUserResponse = await config.fetch(registerUserMutation, {
variables: registerUserVariables,
})
const accountId = registerUserResponse.data?.account?.id
// Login user
const registerUserLoginVairables = {
accountId: accountId,
customerLoginInfoInput: {
emailAddress: email,
username: email,
password: password,
isImport: false
}
emailAddress: email,
username: email,
password: password,
isImport: false,
},
}
response = await config.fetch(registerUserLoginMutation, { variables: registerUserLoginVairables})
const { account: token } = response.data;
response = await config.fetch(registerUserLoginMutation, {
variables: registerUserLoginVairables,
})
const { account: token } = response.data
// Set Cookie
const cookieExpirationDate = getCookieExpirationDate(config.customerCookieMaxAgeInDays)
const cookieExpirationDate = getCookieExpirationDate(
config.customerCookieMaxAgeInDays
)
const authCookie = prepareSetCookie(
config.customerCookie,
JSON.stringify(token),
token.accessTokenExpiration ? { expires: cookieExpirationDate }: {},
token.accessTokenExpiration ? { expires: cookieExpirationDate } : {}
)
setCookies(res, [authCookie])
return {
data: response,
headers: {
'Set-Cookie': authCookie,
},
}
} catch (error) {
// Check if the email and password didn't match an existing account
if (
error instanceof FetcherError &&
invalidCredentials.test(error.message)
) {
return res.status(401).json({
data: null,
errors: [
{
message:
'Cannot find an account that matches the provided credentials',
code: 'invalid_credentials',
},
],
})
throw new CommerceAPIError(
'Cannot find an account that matches the provided credentials',
{
status: 401,
code: 'invalid_credentials',
}
)
} else {
throw error
}
throw error
}
res.status(200).json({ data: response })
}
export default signup

View File

@@ -1,17 +1,20 @@
import getCustomerWishlist from '../../operations/get-customer-wishlist'
import getCustomerId from '../../utils/get-customer-id'
import type { WishlistEndpoint } from '.'
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
import { normalizeWishlistItem } from '../../../lib/normalize'
import { getProductQuery } from '../../../api/queries/get-product-query'
import addItemToWishlistMutation from '../../mutations/addItemToWishlist-mutation'
import getCustomerId from '../../utils/get-customer-id'
import createWishlist from '../../mutations/create-wishlist-mutation'
import addItemToWishlistMutation from '../../mutations/addItemToWishlist-mutation'
// Return wishlist info
const buildAddToWishlistVariables = ({
productId,
variantId,
productResponse,
wishlist
wishlist,
}: {
productId: string
variantId: string
@@ -23,7 +26,7 @@ const buildAddToWishlistVariables = ({
const selectedOptions = product.variations?.find(
(v: any) => v.productCode === variantId
).options
const quantity=1
const quantity = 1
let options: any[] = []
selectedOptions?.forEach((each: any) => {
product?.options
@@ -47,53 +50,50 @@ const buildAddToWishlistVariables = ({
productCode: productId,
variationProductCode: variantId ? variantId : null,
options,
}
},
},
}
}
const addItem: WishlistEndpoint['handlers']['addItem'] = async ({
res,
body: { customerToken, item },
config,
commerce,
}) => {
const token = customerToken ? Buffer.from(customerToken, 'base64').toString('ascii'): null;
const accessToken = token ? JSON.parse(token).accessToken : null;
const token = customerToken
? Buffer.from(customerToken, 'base64').toString('ascii')
: null
const accessToken = token ? JSON.parse(token).accessToken : null
let result: { data?: any } = {}
let wishlist: any
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
const customerId = customerToken && (await getCustomerId({ customerToken, config }))
const wishlistName= config.defaultWishlistName
const customerId =
customerToken && (await getCustomerId({ customerToken, config }))
const wishlistName = config.defaultWishlistName
if (!customerId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
throw new CommerceAPIError('Customer not found', { status: 404 })
}
const wishlistResponse = await commerce.getCustomerWishlist({
variables: { customerId, wishlistName },
config,
})
wishlist= wishlistResponse?.wishlist
if(Object.keys(wishlist).length === 0) {
const createWishlistResponse= await config.fetch(createWishlist, {variables: {
wishlistInput: {
customerAccountId: customerId,
name: wishlistName
}
}
}, {headers: { 'x-vol-user-claims': accessToken } })
wishlist= createWishlistResponse?.data?.createWishlist
wishlist = wishlistResponse?.wishlist
if (Object.keys(wishlist).length === 0) {
const createWishlistResponse = await config.fetch(
createWishlist,
{
variables: {
wishlistInput: {
customerAccountId: customerId,
name: wishlistName,
},
},
},
{ headers: { 'x-vol-user-claims': accessToken } }
)
wishlist = createWishlistResponse?.data?.createWishlist
}
const productResponse = await config.fetch(getProductQuery, {
@@ -103,22 +103,33 @@ const addItem: WishlistEndpoint['handlers']['addItem'] = async ({
const addItemToWishlistResponse = await config.fetch(
addItemToWishlistMutation,
{
variables: buildAddToWishlistVariables({ ...item, productResponse, wishlist }),
variables: buildAddToWishlistVariables({
...item,
productResponse,
wishlist,
}),
},
{ headers: { 'x-vol-user-claims': accessToken } }
)
if(addItemToWishlistResponse?.data?.createWishlistItem){
const wishlistResponse= await commerce.getCustomerWishlist({
if (addItemToWishlistResponse?.data?.createWishlistItem) {
const wishlistResponse = await commerce.getCustomerWishlist({
variables: { customerId, wishlistName },
config,
})
wishlist= wishlistResponse?.wishlist
wishlist = wishlistResponse?.wishlist
}
result = { data: {...wishlist, items: wishlist?.items?.map((item:any) => normalizeWishlistItem(item, config))} }
res.status(200).json({ data: result?.data })
result = {
data: {
...wishlist,
items: wishlist?.items?.map((item: any) =>
normalizeWishlistItem(item, config)
),
},
}
return { data: result?.data }
}
export default addItem

View File

@@ -1,35 +1,45 @@
import type { WishlistEndpoint } from '.'
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
import getCustomerId from '../../utils/get-customer-id'
import { normalizeWishlistItem } from '../../../lib/normalize'
// Return wishlist info
const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({
res,
body: { customerToken, includeProducts },
config,
commerce,
}) => {
let result: { data?: any } = {}
if (customerToken) {
const customerId = customerToken && (await getCustomerId({ customerToken, config }))
const wishlistName= config.defaultWishlistName
const customerId =
customerToken && (await getCustomerId({ customerToken, config }))
const wishlistName = config.defaultWishlistName
if (!customerId) {
// If the customerToken is invalid, then this request is too
return res.status(404).json({
data: null,
errors: [{ message: 'Wishlist not found' }],
throw new CommerceAPIError('Wishlist not found', {
status: 404,
code: 'not_found',
})
}
const { wishlist } = await commerce.getCustomerWishlist({
variables: { customerId, wishlistName },
includeProducts,
config,
})
result = { data: {...wishlist, items: wishlist?.items?.map((item:any) => normalizeWishlistItem(item, config, includeProducts))} }
result = {
data: {
...wishlist,
items: wishlist?.items?.map((item: any) =>
normalizeWishlistItem(item, config, includeProducts)
),
},
}
}
res.status(200).json({ data: result?.data ?? null })
return { data: result?.data ?? null }
}
export default getWishlist

View File

@@ -1,60 +1,69 @@
import getCustomerId from '../../utils/get-customer-id'
import type { WishlistEndpoint } from '.'
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
import { normalizeWishlistItem } from '../../../lib/normalize'
import getCustomerId from '../../utils/get-customer-id'
import removeItemFromWishlistMutation from '../../mutations/removeItemFromWishlist-mutation'
// Return wishlist info
const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({
res,
body: { customerToken, itemId },
config,
commerce,
}) => {
const token = customerToken ? Buffer.from(customerToken, 'base64').toString('ascii'): null;
const accessToken = token ? JSON.parse(token).accessToken : null;
const token = customerToken
? Buffer.from(customerToken, 'base64').toString('ascii')
: null
const accessToken = token ? JSON.parse(token).accessToken : null
let result: { data?: any } = {}
let wishlist: any
const customerId = customerToken && (await getCustomerId({ customerToken, config }))
const wishlistName= config.defaultWishlistName
const customerId =
customerToken && (await getCustomerId({ customerToken, config }))
const wishlistName = config.defaultWishlistName
const wishlistResponse = await commerce.getCustomerWishlist({
variables: { customerId, wishlistName },
config,
})
wishlist= wishlistResponse?.wishlist
if (!wishlist || !itemId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
wishlist = wishlistResponse?.wishlist
if (!wishlist) {
throw new CommerceAPIError('Wishlist not found', { status: 404 })
}
const removedItem = wishlist?.items?.find(
(item:any) => {
return item.product.productCode === itemId;
}
);
const removedItem = wishlist?.items?.find((item: any) => {
return item.product.productCode === itemId
})
const removeItemFromWishlistResponse = await config.fetch(
removeItemFromWishlistMutation,
{
variables: {
wishlistId: wishlist?.id,
wishlistItemId: removedItem?.id
wishlistItemId: removedItem?.id,
},
},
{ headers: { 'x-vol-user-claims': accessToken } }
)
if(removeItemFromWishlistResponse?.data?.deleteWishlistItem){
const wishlistResponse= await commerce.getCustomerWishlist({
if (removeItemFromWishlistResponse?.data?.deleteWishlistItem) {
const wishlistResponse = await commerce.getCustomerWishlist({
variables: { customerId, wishlistName },
config,
})
wishlist= wishlistResponse?.wishlist
wishlist = wishlistResponse?.wishlist
}
result = {
data: {
...wishlist,
items: wishlist?.items?.map((item: any) =>
normalizeWishlistItem(item, config)
),
},
}
return {
data: result?.data,
}
result = { data: {...wishlist, items: wishlist?.items?.map((item:any) => normalizeWishlistItem(item, config))} }
res.status(200).json({ data: result?.data })
}
export default removeItem

View File

@@ -9,16 +9,15 @@ import getCustomerWishlist from './operations/get-customer-wishlist'
import getAllProductPaths from './operations/get-all-product-paths'
import getAllProducts from './operations/get-all-products'
import getProduct from './operations/get-product'
import type { RequestInit } from '@vercel/fetch'
export interface KiboCommerceConfig extends CommerceAPIConfig {
apiHost?: string
clientId?: string
sharedSecret?: string
customerCookieMaxAgeInDays: number,
currencyCode: string,
documentListName: string,
defaultWishlistName: string,
customerCookieMaxAgeInDays: number
currencyCode: string
documentListName: string
defaultWishlistName: string
authUrl?: string
}
@@ -37,7 +36,7 @@ const config: KiboCommerceConfig = {
sharedSecret: process.env.KIBO_SHARED_SECRET || '',
customerCookieMaxAgeInDays: 30,
currencyCode: 'USD',
defaultWishlistName: 'My Wishlist'
defaultWishlistName: 'My Wishlist',
}
const operations = {
@@ -55,7 +54,7 @@ export const provider = { config, operations }
export type KiboCommerceProvider = typeof provider
export type KiboCommerceAPI<
P extends KiboCommerceProvider = KiboCommerceProvider
> = CommerceAPI<P | any>
> = CommerceAPI<P | any>
export function getCommerceApi<P extends KiboCommerceProvider>(
customProvider: P = provider as any

View File

@@ -1,6 +1,4 @@
import type { KiboCommerceConfig } from '../index'
import type { FetchOptions } from '@vercel/fetch'
import fetch from './fetch'
// This object is persisted during development
const authCache: { kiboAuthTicket?: AppAuthTicket } = {}
@@ -41,11 +39,11 @@ export class APIAuthenticationHelper {
this._clientId = clientId
this._sharedSecret = sharedSecret
this._authUrl = authUrl
if(!authTicketCache) {
this._authTicketCache = new RuntimeMemCache();
if (!authTicketCache) {
this._authTicketCache = new RuntimeMemCache()
}
}
private _buildFetchOptions(body: any = {}): FetchOptions {
private _buildFetchOptions(body: any = {}): any {
return {
method: 'POST',
headers: {

View File

@@ -1,25 +1,25 @@
import { KiboCommerceConfig } from './../index'
import { getCookieExpirationDate } from '../../lib/get-cookie-expiration-date'
import { prepareSetCookie } from '../../lib/prepare-set-cookie'
import { setCookies } from '../../lib/set-cookie'
import { NextApiRequest } from 'next'
import getAnonymousShopperToken from './get-anonymous-shopper-token'
import type { NextRequest } from 'next/server'
const parseCookie = (cookieValue?: any) => {
return cookieValue
? JSON.parse(Buffer.from(cookieValue, 'base64').toString('ascii'))
return cookieValue
? JSON.parse(Buffer.from(cookieValue, 'base64').toString('ascii'))
: null
}
export default class CookieHandler {
config: KiboCommerceConfig
request: NextApiRequest
response: any
request: NextRequest
headers: HeadersInit | undefined
accessToken: any
constructor(config: any, req: NextApiRequest, res: any) {
constructor(config: any, req: NextRequest) {
this.config = config
this.request = req
this.response = res
const encodedToken = req.cookies[config.customerCookie]
const encodedToken = req.cookies.get(config.customerCookie)
const token = parseCookie(encodedToken)
this.accessToken = token ? token.accessToken : null
}
@@ -36,9 +36,9 @@ export default class CookieHandler {
}
isShopperCookieAnonymous() {
const customerCookieKey = this.config.customerCookie
const shopperCookie = this.request.cookies[customerCookieKey]
const shopperSession = parseCookie(shopperCookie);
const isAnonymous = shopperSession?.customerAccount ? false : true
const shopperCookie = this.request.cookies.get(customerCookieKey)
const shopperSession = parseCookie(shopperCookie)
const isAnonymous = shopperSession?.customerAccount ? false : true
return isAnonymous
}
setAnonymousShopperCookie(anonymousShopperTokenResponse: any) {
@@ -53,7 +53,9 @@ export default class CookieHandler {
? { expires: cookieExpirationDate }
: {}
)
setCookies(this.response, [authCookie])
this.headers = {
'Set-Cookie': authCookie,
}
}
getAccessToken() {
return this.accessToken

View File

@@ -1,43 +1,46 @@
import { FetcherError } from '@vercel/commerce/utils/errors'
import type { GraphQLFetcher } from '@vercel/commerce/api'
import type { KiboCommerceConfig } from '../index'
import fetch from './fetch'
import { APIAuthenticationHelper } from './api-auth-helper';
import { APIAuthenticationHelper } from './api-auth-helper'
const fetchGraphqlApi: (
getConfig: () => KiboCommerceConfig
) => GraphQLFetcher = (getConfig) => async (
query: string,
{ variables, preview } = {},
fetchOptions
) => {
const config = getConfig()
const authHelper = new APIAuthenticationHelper(config);
const apiToken = await authHelper.getAccessToken();
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
...fetchOptions,
method: 'POST',
headers: {
...fetchOptions?.headers,
Authorization: `Bearer ${apiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
variables,
}),
})
const json = await res.json()
if (json.errors) {
console.warn(`Kibo API Request Correlation ID: ${res.headers.get('x-vol-correlation')}`);
throw new FetcherError({
errors: json.errors ?? [{ message: 'Failed to fetch KiboCommerce API' }],
status: res.status,
) => GraphQLFetcher =
(getConfig) =>
async (query: string, { variables, preview } = {}, headers?: HeadersInit) => {
const config = getConfig()
const authHelper = new APIAuthenticationHelper(config)
const apiToken = await authHelper.getAccessToken()
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
method: 'POST',
headers: {
...headers,
Authorization: `Bearer ${apiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
variables,
}),
})
const json = await res.json()
if (json.errors) {
console.warn(
`Kibo API Request Correlation ID: ${res.headers.get(
'x-vol-correlation'
)}`
)
throw new FetcherError({
errors: json.errors ?? [
{ message: 'Failed to fetch KiboCommerce API' },
],
status: res.status,
})
}
return { data: json.data, res }
}
return { data: json.data, res }
}
export default fetchGraphqlApi

View File

@@ -1,19 +1,19 @@
import { FetcherError } from '@vercel/commerce/utils/errors'
import type { GraphQLFetcher } from '@vercel/commerce/api'
import type { KiboCommerceConfig } from '../index'
import fetch from './fetch'
const fetchGraphqlApi: (getConfig: () => KiboCommerceConfig) => GraphQLFetcher =
const fetchGraphqlApi: (
getConfig: () => KiboCommerceConfig
) => GraphQLFetcher =
(getConfig) =>
async (query: string, { variables, preview } = {}, fetchOptions) => {
async (query: string, { variables, preview } = {}, headers?: HeadersInit) => {
const config = getConfig()
const res = await fetch(config.commerceUrl, {
//const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
...fetchOptions,
//const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
method: 'POST',
headers: {
Authorization: `Bearer ${config.apiToken}`,
...fetchOptions?.headers,
...headers,
'Content-Type': 'application/json',
},
body: JSON.stringify({
@@ -25,7 +25,9 @@ const fetchGraphqlApi: (getConfig: () => KiboCommerceConfig) => GraphQLFetcher =
const json = await res.json()
if (json.errors) {
throw new FetcherError({
errors: json.errors ?? [{ message: 'Failed to fetch KiboCommerce API' }],
errors: json.errors ?? [
{ message: 'Failed to fetch KiboCommerce API' },
],
status: res.status,
})
}

View File

@@ -8,17 +8,13 @@ async function getCustomerId({
customerToken: string
config: KiboCommerceConfig
}): Promise<string | undefined> {
const token = customerToken ? Buffer.from(customerToken, 'base64').toString('ascii'): null;
const accessToken = token ? JSON.parse(token).accessToken : null;
const { data } = await config.fetch(
getCustomerAccountQuery,
undefined,
{
headers: {
'x-vol-user-claims': accessToken,
},
}
)
const token = customerToken
? Buffer.from(customerToken, 'base64').toString('ascii')
: null
const accessToken = token ? JSON.parse(token).accessToken : null
const { data } = await config.fetch(getCustomerAccountQuery, undefined, {
'x-vol-user-claims': accessToken,
})
return data?.customerAccount?.id
}

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:

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

@@ -8,7 +8,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

@@ -1,3 +1,3 @@
export function setCookies(res: any, cookies: string[]): void {
res.setHeader('Set-Cookie', cookies);
}
res.setHeader('Set-Cookie', cookies)
}

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]
)
},
}