Taking multiple steps into better API types

This commit is contained in:
Luis Alvarez 2021-03-19 18:53:28 -06:00
parent df07a55740
commit 149df82f45
8 changed files with 173 additions and 88 deletions

View File

@ -1,7 +1,7 @@
import { normalizeCart } from '@framework/lib/normalize'
import { parseCartItem } from '../utils/parse-item' import { parseCartItem } from '../utils/parse-item'
import getCartCookie from '../utils/get-cart-cookie' import getCartCookie from '../utils/get-cart-cookie'
import type { CartHandlers } from '.' import type { CartHandlers } from '.'
import { normalizeCart } from '@framework/lib/normalize'
const addItem: CartHandlers['addItem'] = async ({ const addItem: CartHandlers['addItem'] = async ({
res, res,

View File

@ -0,0 +1,35 @@
import { normalizeCart } from '@framework/lib/normalize'
import { BigcommerceApiError } from '../utils/errors'
import getCartCookie from '../utils/get-cart-cookie'
import type { BigcommerceCart } from '../../types'
import type { CartHandlers } from '.'
// Return current cart info
const getCart: CartHandlers['getCart'] = async ({
res,
body: { cartId },
config,
}) => {
let result: { data?: BigcommerceCart } = {}
if (cartId) {
try {
result = await config.storeApiFetch(
`/v3/carts/${cartId}?include=line_items.physical_items.options`
)
} catch (error) {
if (error instanceof BigcommerceApiError && error.status === 404) {
// Remove the cookie if it exists but the cart wasn't found
res.setHeader('Set-Cookie', getCartCookie(config.cartCookie))
} else {
throw error
}
}
}
res.status(200).json({
data: result.data ? normalizeCart(result.data) : null,
})
}
export default getCart

View File

@ -1,86 +1,34 @@
import { CartHandlers as CartHandlersCore } from '@commerce/api' import { EndpointSchema } from '@commerce/api'
import isAllowedMethod from '../utils/is-allowed-method' import getCart from './get-cart'
import createApiHandler, { import addItem from './add-item'
BigcommerceApiHandler,
} from '../utils/create-api-handler'
import { BigcommerceApiError } from '../utils/errors'
import getCart from './handlers/get-cart'
import addItem from './handlers/add-item'
import updateItem from './handlers/update-item' import updateItem from './handlers/update-item'
import removeItem from './handlers/remove-item' import removeItem from './handlers/remove-item'
import type { import type {
BigcommerceCart,
GetCartHandlerBody, GetCartHandlerBody,
AddCartItemHandlerBody, AddCartItemHandlerBody,
UpdateCartItemHandlerBody, UpdateCartItemHandlerBody,
RemoveCartItemHandlerBody, RemoveCartItemHandlerBody,
Cart, Cart,
} from '../../types' } from '../../types'
import { APIHandler } from '@commerce/api/utils/types' import type { CommerceAPIEndpoints } from '..'
import { CommerceAPI } from '..'
export type CartHandlers< export type CartEndpointSchema = EndpointSchema<
C extends CommerceAPI = CommerceAPI 'cart',
> = CartHandlersCore<
C,
{ {
getCart: APIHandler<C, CartHandlers<C>, Cart | null, GetCartHandlerBody> options: {}
addItem: APIHandler<C, CartHandlers<C>, Cart, AddCartItemHandlerBody> operations: {
updateItem: APIHandler<C, CartHandlers<C>, Cart, UpdateCartItemHandlerBody> getCart: {
removeItem: APIHandler<C, CartHandlers<C>, Cart, RemoveCartItemHandlerBody> data: Cart | null
body: GetCartHandlerBody
options: { yay: string }
}
addItem: { data: Cart; body: AddCartItemHandlerBody; options: {} }
updateItem: { data: Cart; body: UpdateCartItemHandlerBody; options: {} }
removeItem: { data: Cart; body: RemoveCartItemHandlerBody; options: {} }
}
} }
> >
const METHODS = ['GET', 'POST', 'PUT', 'DELETE'] export type CartAPI = CommerceAPIEndpoints['cart']
// TODO: a complete implementation should have schema validation for `req.body` export const operations = { getCart, addItem }
const cartApi: BigcommerceApiHandler<BigcommerceCart, CartHandlers> = async (
req,
res,
config,
handlers
) => {
if (!isAllowedMethod(req, res, METHODS)) return
const { cookies } = req
const cartId = cookies[config.cartCookie]
try {
// Return current cart info
if (req.method === 'GET') {
const body = { cartId }
return await handlers['getCart']({ req, res, config, body })
}
// Create or add an item to the cart
if (req.method === 'POST') {
const body = { ...req.body, cartId }
return await handlers['addItem']({ req, res, config, body })
}
// Update item in cart
if (req.method === 'PUT') {
const body = { ...req.body, cartId }
return await handlers['updateItem']({ req, res, config, body })
}
// Remove an item from the cart
if (req.method === 'DELETE') {
const body = { ...req.body, cartId }
return await handlers['removeItem']({ req, res, config, body })
}
} catch (error) {
console.error(error)
const message =
error instanceof BigcommerceApiError
? 'An unexpected error ocurred with the Bigcommerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export const handlers = { getCart, addItem, updateItem, removeItem }
export default createApiHandler(cartApi, handlers, {})

View File

@ -2,9 +2,11 @@ import type { RequestInit } from '@vercel/fetch'
import { import {
CommerceAPI as CoreCommerceAPI, CommerceAPI as CoreCommerceAPI,
CommerceAPIConfig, CommerceAPIConfig,
GetEndpointsSchema,
} from '@commerce/api' } from '@commerce/api'
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 { CartEndpointSchema } 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
@ -104,14 +106,19 @@ export const provider = {
export type Provider = typeof provider export type Provider = typeof provider
export type EndpointsSchema = { cart: CartEndpointSchema }
export class CommerceAPI< export class CommerceAPI<
P extends Provider = Provider P extends Provider = Provider,
> extends CoreCommerceAPI<P> { E extends EndpointsSchema = EndpointsSchema
> extends CoreCommerceAPI<P, E> {
constructor(readonly provider: P = provider) { constructor(readonly provider: P = provider) {
super(provider) super(provider)
} }
} }
export type CommerceAPIEndpoints = GetEndpointsSchema<CommerceAPI>
export function getConfig(userConfig?: Partial<BigcommerceConfig>) { export function getConfig(userConfig?: Partial<BigcommerceConfig>) {
return config.getConfig(userConfig) return config.getConfig(userConfig)
} }

View File

@ -25,6 +25,7 @@ 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

@ -5,8 +5,14 @@ import type { Cart } from '../types'
export type CartEndpoint = APIEndpoint<any, any, CartHandlers<CommerceAPI>, any> export type CartEndpoint = APIEndpoint<any, any, CartHandlers<CommerceAPI>, any>
export type CartHandlersBase<C> = { export type CartHandlersBase<C extends CommerceAPI> = {
getCart: APIHandler<any, CartHandlersBase<C>, Cart | null, any> getCart: APIHandler<
any,
CartHandlersBase<C>,
Cart | null,
any,
{ yay: string }
>
addItem: APIHandler<any, CartHandlersBase<C>, Cart, any> addItem: APIHandler<any, CartHandlersBase<C>, Cart, any>
updateItem: APIHandler<any, CartHandlersBase<C>, Cart, any> updateItem: APIHandler<any, CartHandlersBase<C>, Cart, any>
removeItem: APIHandler<any, CartHandlersBase<C>, Cart, any> removeItem: APIHandler<any, CartHandlersBase<C>, Cart, any>
@ -17,13 +23,103 @@ export type CartHandlers<
T extends CartHandlersBase<C> = CartHandlersBase<C> T extends CartHandlersBase<C> = CartHandlersBase<C>
> = T > = T
export type Endpoints = CartEndpoint export type CartHandlersType = {
getCart: { data: Cart | null; body: any; options: {} }
addItem: { data: Cart; body: any; options: {} }
updateItem: { data: Cart; body: any; options: {} }
removeItem: { data: Cart; body: any; options: {} }
}
export type EndpointHandlers<E> = E extends APIEndpoint<any, any, infer T> export type CartHandlers2<
C extends CommerceAPI,
T extends CartHandlersType = CartHandlersType
> = {
getCart: APIHandler<
any,
CartHandlersBase<C>,
Cart | null,
any,
{ yay: string }
>
addItem: APIHandler<any, CartHandlersBase<C>, Cart, any>
updateItem: APIHandler<any, CartHandlersBase<C>, Cart, any>
removeItem: APIHandler<any, CartHandlersBase<C>, Cart, any>
}
export type EndpointsSchema = {
cart?: {
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 GetEndpointsSchema<
C extends CommerceAPI,
Schema extends EndpointsSchema = C extends CommerceAPI<any, infer E>
? E
: never
> = {
[E in keyof EndpointsSchema]-?: Schema[E] & {
endpoint: Endpoint<C, NonNullable<Schema[E]>>
handlers: EndpointHandlers<C, NonNullable<Schema[E]>>
}
}
type X = Endpoint<CommerceAPI, NonNullable<EndpointsSchema['cart']>>
export type EndpointSchemaBase = {
options: {}
operations: {
[k: string]: { data?: any; body?: any }
}
}
export type OperationData<T> = T extends { data?: infer D; body?: any }
? D
: never
export type EndpointSchema<
E extends keyof EndpointsSchema,
Handlers extends EndpointsSchema[E]
> = Handlers
export type Endpoint<
C extends CommerceAPI,
E extends EndpointSchemaBase
> = APIEndpoint<
C,
EndpointHandlers<C, E>,
OperationData<E['operations'][keyof E['operations']]>,
E['options']
>
export type EndpointHandlers<
C extends CommerceAPI,
E extends EndpointSchemaBase
> = {
[H in keyof E['operations']]: APIHandler<
C,
EndpointHandlers<C, E>,
E['operations'][H]['data'],
E['operations'][H]['body'],
E['options']
>
}
export type CommerceEndpointsSchema<C> = C extends CommerceAPI<any, infer E>
? E
: never
export type HandlerOperations<E> = E extends APIEndpoint<any, any, infer T>
? T ? T
: never : never
export type EndpointOptions<E> = E extends APIEndpoint<any, any, any, infer T> export type HandlerOptions<E> = E extends APIEndpoint<any, any, any, infer T>
? T ? T
: never : never
@ -33,7 +129,7 @@ export type APIProvider = {
export class CommerceAPI< export class CommerceAPI<
P extends APIProvider = APIProvider, P extends APIProvider = APIProvider,
E extends Endpoints = Endpoints E extends EndpointsSchema = EndpointsSchema
> { > {
constructor(readonly provider: P) { constructor(readonly provider: P) {
this.provider = provider this.provider = provider
@ -53,8 +149,8 @@ export class CommerceAPI<
endpoint(context: { endpoint(context: {
handler: E handler: E
config?: P['config'] config?: P['config']
operations: EndpointHandlers<typeof context.handler> operations: HandlerOperations<typeof context.handler>
options?: EndpointOptions<typeof context.handler> options?: HandlerOptions<typeof context.handler>
}): NextApiHandler { }): NextApiHandler {
const commerce = this const commerce = this
const cfg = this.getConfig(context.config) const cfg = this.getConfig(context.config)

View File

@ -37,8 +37,8 @@ export type APIHandler<
context: APIHandlerContext<C, H, Data, Options> & { body: Body } context: APIHandlerContext<C, H, Data, Options> & { body: Body }
) => void | Promise<void> ) => void | Promise<void>
export type APIHandlers<C extends CommerceAPI, Data = any> = { export type APIHandlers<C extends CommerceAPI> = {
[k: string]: APIHandler<C, any, Data, any, any> [k: string]: APIHandler<C, any, any, any, any>
} }
export type APIEndpoint< export type APIEndpoint<

View File

@ -1,7 +1,5 @@
import cartApi from '@framework/api/cart'
import cart from '@commerce/api/endpoints/cart' import cart from '@commerce/api/endpoints/cart'
import { operations } from '@framework/api/cart'
import commerce from '@lib/api/commerce' import commerce from '@lib/api/commerce'
const x = commerce.endpoint({ handler: cart, operations: {} as any }) export default commerce.endpoint({ handler: cart, operations })
export default cartApi()