Adding types, fixes and other updates

This commit is contained in:
Luis Alvarez 2021-03-26 12:50:25 -06:00
parent e6df8dfb40
commit b2fbaab5d5
11 changed files with 400 additions and 38 deletions

View File

@ -1,8 +1,9 @@
import { GetAPISchema } from '@commerce/api' import type { GetAPISchema } from '@commerce/api'
import type { AddItemOperation } from '@commerce/types'
import getCart from './get-cart' import getCart from './get-cart'
import addItem from './add-item' import addItem from './add-item'
import updateItem from './handlers/update-item' import updateItem from './update-item'
import removeItem from './handlers/remove-item' import removeItem from './remove-item'
import type { import type {
GetCartHandlerBody, GetCartHandlerBody,
AddCartItemHandlerBody, AddCartItemHandlerBody,
@ -18,14 +19,10 @@ export type CartAPI = GetAPISchema<
endpoint: { endpoint: {
options: {} options: {}
operations: { operations: {
getCart: { getCart: { data: Cart | null; body: GetCartHandlerBody }
data: Cart | null addItem: { data: Cart; body: AddItemOperation['body'] }
body: GetCartHandlerBody updateItem: { data: Cart; body: UpdateCartItemHandlerBody }
options: { yay: string } removeItem: { data: Cart; body: RemoveCartItemHandlerBody }
}
addItem: { data: Cart; body: AddCartItemHandlerBody; options: {} }
updateItem: { data: Cart; body: UpdateCartItemHandlerBody; options: {} }
removeItem: { data: Cart; body: RemoveCartItemHandlerBody; options: {} }
} }
} }
} }
@ -33,4 +30,4 @@ export type CartAPI = GetAPISchema<
export type CartEndpoint = CartAPI['endpoint'] export type CartEndpoint = CartAPI['endpoint']
export const operations = { getCart, addItem } export const operations = { getCart, addItem, updateItem, removeItem }

View File

@ -0,0 +1,34 @@
import { normalizeCart } from '@framework/lib/normalize'
import getCartCookie from '../utils/get-cart-cookie'
import type { CartEndpoint } from '.'
const removeItem: CartEndpoint['operations']['removeItem'] = async ({
res,
body: { cartId, itemId },
config,
}) => {
if (!cartId || !itemId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const result = await config.storeApiFetch<{ data: any } | null>(
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
{ method: 'DELETE' }
)
const data = result?.data ?? null
res.setHeader(
'Set-Cookie',
data
? // Update the cart cookie
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
: // Remove the cart cookie if the cart was removed (empty items)
getCartCookie(config.cartCookie)
)
res.status(200).json({ data: normalizeCart(data) })
}
export default removeItem

View File

@ -0,0 +1,36 @@
import { normalizeCart } from '@framework/lib/normalize'
import { parseCartItem } from '../utils/parse-item'
import getCartCookie from '../utils/get-cart-cookie'
import type { CartEndpoint } from '.'
const updateItem: CartEndpoint['operations']['updateItem'] = async ({
res,
body: { cartId, itemId, item },
config,
}) => {
if (!cartId || !itemId || !item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
const { data } = await config.storeApiFetch(
`/v3/carts/${cartId}/items/${itemId}?include=line_items.physical_items.options`,
{
method: 'PUT',
body: JSON.stringify({
line_item: parseCartItem(item),
}),
}
)
// Update the cart cookie
res.setHeader(
'Set-Cookie',
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
)
res.status(200).json({ data: normalizeCart(data) })
}
export default updateItem

View File

@ -1,3 +1,4 @@
import type { NextApiHandler } from 'next'
import type { RequestInit } from '@vercel/fetch' import type { RequestInit } from '@vercel/fetch'
import { import {
CommerceAPI as CoreCommerceAPI, CommerceAPI as CoreCommerceAPI,
@ -6,6 +7,8 @@ import {
import fetchGraphqlApi from './utils/fetch-graphql-api' import fetchGraphqlApi from './utils/fetch-graphql-api'
import fetchStoreApi from './utils/fetch-store-api' import fetchStoreApi from './utils/fetch-store-api'
import type { CartAPI } from './cart'
export interface BigcommerceConfig extends CommerceAPIConfig { export interface BigcommerceConfig extends CommerceAPIConfig {
// Indicates if the returned metadata with translations should be applied to the // Indicates if the returned metadata with translations should be applied to the
// data or returned as it is // data or returned as it is
@ -104,11 +107,20 @@ export const provider = {
export type Provider = typeof provider export type Provider = typeof provider
export class CommerceAPI< export type APIs = CartAPI
P extends Provider = Provider
> extends CoreCommerceAPI<P> { export class CommerceAPI extends CoreCommerceAPI<Provider> {
constructor(readonly provider: P = provider) { constructor(customProvider: Provider = provider) {
super(provider) super(customProvider)
}
endpoint<E extends APIs>(
context: E['endpoint'] & {
config?: Provider['config']
options?: E['schema']['endpoint']['options']
}
): NextApiHandler {
return this.endpoint(context)
} }
} }

View File

@ -25,7 +25,6 @@ export type BigcommerceCart = {
export type Cart = Core.Cart & { export type Cart = Core.Cart & {
lineItems: LineItem[] lineItems: LineItem[]
core: string
} }
export type LineItem = Core.LineItem export type LineItem = Core.LineItem

View File

@ -1,19 +1,7 @@
import type { NextApiHandler } from 'next' import type { NextApiHandler } from 'next'
import type { RequestInit, Response } from '@vercel/fetch' import type { RequestInit, Response } from '@vercel/fetch'
import type { APIEndpoint, APIHandler } from './utils/types' import type { APIEndpoint, APIHandler } from './utils/types'
import type { Cart } from '../types' import type { CartSchema } from '../types'
export type CartSchema = {
endpoint: {
options: {}
operations: {
getCart: { data?: Cart | null; body?: any }
addItem: { data?: Cart; body?: any }
updateItem: { data?: Cart; body?: any }
removeItem: { data?: Cart; body?: any }
}
}
}
export type APISchemas = CartSchema export type APISchemas = CartSchema
@ -78,10 +66,10 @@ export class CommerceAPI<P extends APIProvider = APIProvider> {
Object.assign(this.provider.config, newConfig) Object.assign(this.provider.config, newConfig)
} }
endpoint<E extends GetAPISchema<this>>( endpoint<T extends GetAPISchema<any, any>>(
context: E['endpoint'] & { context: T['endpoint'] & {
config?: P['config'] config?: P['config']
options?: E['schema']['endpoint']['options'] options?: T['schema']['endpoint']['options']
} }
): NextApiHandler { ): NextApiHandler {
const commerce = this const commerce = this
@ -93,7 +81,7 @@ export class CommerceAPI<P extends APIProvider = APIProvider> {
res, res,
commerce, commerce,
config: cfg, config: cfg,
handlers: context.operations, operations: context.operations,
options: context.options ?? {}, options: context.options ?? {},
}) })
} }

View File

@ -20,7 +20,7 @@ export type APIHandlerContext<
res: NextApiResponse<APIResponse<Data>> res: NextApiResponse<APIResponse<Data>>
commerce: C commerce: C
config: C['provider']['config'] config: C['provider']['config']
handlers: H operations: H
/** /**
* Custom configs that may be used by a particular handler * Custom configs that may be used by a particular handler
*/ */

View File

@ -0,0 +1,147 @@
import type { Discount, Measurement, Image } from './common'
export type LineItem = {
id: string
variantId: string
productId: string
name: string
quantity: number
discounts: Discount[]
// A human-friendly unique string automatically generated from the products name
path: string
variant: ProductVariant
}
export type ProductVariant = {
id: string
// The SKU (stock keeping unit) associated with the product variant.
sku: string
// The product variants title, or the product's name.
name: string
// Whether a customer needs to provide a shipping address when placing
// an order for the product variant.
requiresShipping: boolean
// The product variants price after all discounts are applied.
price: number
// Product variants price, as quoted by the manufacturer/distributor.
listPrice: number
// Image associated with the product variant. Falls back to the product image
// if no image is available.
image?: Image
// Indicates whether this product variant is in stock.
isInStock?: boolean
// Indicates if the product variant is available for sale.
availableForSale?: boolean
// The variant's weight. If a weight was not explicitly specified on the
// variant this will be the product's weight.
weight?: Measurement
// The variant's height. If a height was not explicitly specified on the
// variant, this will be the product's height.
height?: Measurement
// The variant's width. If a width was not explicitly specified on the
// variant, this will be the product's width.
width?: Measurement
// The variant's depth. If a depth was not explicitly specified on the
// variant, this will be the product's depth.
depth?: Measurement
}
// Shopping cart, a.k.a Checkout
export type Cart = {
id: string
// ID of the customer to which the cart belongs.
customerId?: string
// The email assigned to this cart
email?: string
// The date and time when the cart was created.
createdAt: string
// The currency used for this cart
currency: { code: string }
// Specifies if taxes are included in the line items.
taxesIncluded: boolean
lineItems: LineItem[]
// The sum of all the prices of all the items in the cart.
// Duties, taxes, shipping and discounts excluded.
lineItemsSubtotalPrice: number
// Price of the cart before duties, shipping and taxes.
subtotalPrice: number
// The sum of all the prices of all the items in the cart.
// Duties, taxes and discounts included.
totalPrice: number
// Discounts that have been applied on the cart.
discounts?: Discount[]
}
/**
* Base cart item body used for cart mutations
*/
export type CartItemBody = {
variantId: string
productId?: string
quantity?: number
}
/**
* Hooks schema
*/
export type CartHooks = {
getCart: GetCartHook
addItem: AddItemHook
updateItem: UpdateItemHook
remoteItem: RemoveItemHook
}
export type GetCartHook = {
data: Cart | null
}
export type AddItemHook = {
data: Cart
body: { item: CartItemBody }
}
export type UpdateItemHook = {
data: Cart
body: { itemId: string; item: CartItemBody }
}
export type RemoveItemHook = {
data: Cart | null
body: { itemId: string }
}
/**
* API Schema
*/
export type CartSchema = {
endpoint: {
options: {}
operations: CartOperations
}
}
export type CartOperations = {
getCart: GetCartOperation
addItem: AddItemOperation
updateItem: UpdateItemOperation
removeItem: RemoveItemOperation
}
export type GetCartOperation = {
data: Cart | null
body: { cartId?: string }
}
export type AddItemOperation = AddItemHook & {
body: { cartId: string }
}
export type UpdateItemOperation = UpdateItemHook & {
body: { cartId: string }
}
export type RemoveItemOperation = RemoveItemHook & {
body: { cartId: string }
}

View File

@ -0,0 +1,16 @@
export type Discount = {
// The value of the discount, can be an amount or percentage
value: number
}
export type Measurement = {
value: number
unit: 'KILOGRAMS' | 'GRAMS' | 'POUNDS' | 'OUNCES'
}
export type Image = {
url: string
altText?: string
width?: number
height?: number
}

View File

@ -0,0 +1,130 @@
import type { Wishlist as BCWishlist } from '../../bigcommerce/api/wishlist'
import type { Customer as BCCustomer } from '../../bigcommerce/api/customers'
import type { SearchProductsData as BCSearchProductsData } from '../../bigcommerce/api/catalog/products'
export * from './cart'
export * from './common'
// TODO: Properly define this type
export interface Wishlist extends BCWishlist {}
// TODO: Properly define this type
export interface Customer extends BCCustomer {}
// TODO: Properly define this type
export interface SearchProductsData extends BCSearchProductsData {}
/**
* Cart mutations
*/
// Base cart item body used for cart mutations
export type CartItemBody = {
variantId: string
productId?: string
quantity?: number
}
// Body used by the `getCart` operation handler
export type GetCartHandlerBody = {
cartId?: string
}
// Body used by the add item to cart operation
export type AddCartItemBody<T extends CartItemBody> = {
item: T
}
// Body expected by the add item to cart operation handler
export type AddCartItemHandlerBody<T extends CartItemBody> = Partial<
AddCartItemBody<T>
> & {
cartId?: string
}
// Body used by the update cart item operation
export type UpdateCartItemBody<T extends CartItemBody> = {
itemId: string
item: T
}
// Body expected by the update cart item operation handler
export type UpdateCartItemHandlerBody<T extends CartItemBody> = Partial<
UpdateCartItemBody<T>
> & {
cartId?: string
}
// Body used by the remove cart item operation
export type RemoveCartItemBody = {
itemId: string
}
// Body expected by the remove cart item operation handler
export type RemoveCartItemHandlerBody = Partial<RemoveCartItemBody> & {
cartId?: string
}
/**
* Temporal types
*/
interface Entity {
id: string | number
[prop: string]: any
}
export interface Product2 {
id: string
name: string
description: string
sku?: string
slug?: string
path?: string
images: ProductImage[]
variants: ProductVariant2[]
price: ProductPrice
options: ProductOption[]
}
export interface Product extends Entity {
name: string
description: string
slug?: string
path?: string
images: ProductImage[]
variants: ProductVariant2[]
price: ProductPrice
options: ProductOption[]
sku?: string
}
interface ProductOption extends Entity {
displayName: string
values: ProductOptionValues[]
}
interface ProductOptionValues {
label: string
hexColors?: string[]
}
interface ProductImage {
url: string
alt?: string
}
interface ProductVariant2 {
id: string | number
options: ProductOption[]
}
interface ProductPrice {
value: number
currencyCode: 'USD' | 'ARS' | string | undefined
retailPrice?: number
salePrice?: number
listPrice?: number
extendedSalePrice?: number
extendedListPrice?: number
}

View File

@ -1,5 +1,8 @@
import cart from '@commerce/api/endpoints/cart' import cart from '@commerce/api/endpoints/cart'
import { operations } from '@framework/api/cart' import { CartAPI, operations } from '@framework/api/cart'
import commerce from '@lib/api/commerce' import commerce from '@lib/api/commerce'
export default commerce.endpoint({ handler: cart, operations }) export default commerce.endpoint({
handler: cart as CartAPI['endpoint']['handler'],
operations,
})