diff --git a/framework/bigcommerce/api/checkout.ts b/framework/bigcommerce/api/checkout.ts deleted file mode 100644 index 530f4c40a..000000000 --- a/framework/bigcommerce/api/checkout.ts +++ /dev/null @@ -1,77 +0,0 @@ -import isAllowedMethod from './utils/is-allowed-method' -import createApiHandler, { - BigcommerceApiHandler, -} from './utils/create-api-handler' -import { BigcommerceApiError } from './utils/errors' - -const METHODS = ['GET'] -const fullCheckout = true - -// TODO: a complete implementation should have schema validation for `req.body` -const checkoutApi: BigcommerceApiHandler = async (req, res, config) => { - if (!isAllowedMethod(req, res, METHODS)) return - - const { cookies } = req - const cartId = cookies[config.cartCookie] - - try { - if (!cartId) { - res.redirect('/cart') - return - } - - const { data } = await config.storeApiFetch( - `/v3/carts/${cartId}/redirect_urls`, - { - method: 'POST', - } - ) - - if (fullCheckout) { - res.redirect(data.checkout_url) - return - } - - // TODO: make the embedded checkout work too! - const html = ` - - - - - - Checkout - - - - -
- - - ` - - res.status(200) - res.setHeader('Content-Type', 'text/html') - res.write(html) - res.end() - } 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 default createApiHandler(checkoutApi, {}, {}) diff --git a/framework/bigcommerce/api/endpoints/catalog/products/get-products.ts b/framework/bigcommerce/api/endpoints/catalog/products/get-products.ts index e0cc27912..8471767aa 100644 --- a/framework/bigcommerce/api/endpoints/catalog/products/get-products.ts +++ b/framework/bigcommerce/api/endpoints/catalog/products/get-products.ts @@ -1,6 +1,5 @@ import { Product } from '@commerce/types/product' import { ProductsEndpoint } from '.' -import getAllProducts from '../../../../product/get-all-products' const SORT: { [key: string]: string | undefined } = { latest: 'id', @@ -15,6 +14,7 @@ const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({ res, body: { search, category, brand, sort }, config, + commerce, }) => { // Use a dummy base as we only care about the relative path const url = new URL('/v3/catalog/products', 'http://a') @@ -47,18 +47,18 @@ const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({ url.pathname + url.search ) - const entityIds = data.map((p) => p.id) - const found = entityIds.length > 0 + const ids = data.map((p) => String(p.id)) + const found = ids.length > 0 // We want the GraphQL version of each product - const graphqlData = await getAllProducts({ - variables: { first: LIMIT, entityIds }, + const graphqlData = await commerce.getAllProducts({ + variables: { first: LIMIT, ids }, config, }) // Put the products in an object that we can use to get them by id const productsById = graphqlData.products.reduce<{ - [k: number]: Product + [k: string]: Product }>((prods, p) => { prods[Number(p.id)] = p return prods @@ -68,7 +68,7 @@ const getProducts: ProductsEndpoint['handlers']['getProducts'] = async ({ // Populate the products array with the graphql products, in the order // assigned by the list of entity ids - entityIds.forEach((id) => { + ids.forEach((id) => { const product = productsById[id] if (product) products.push(product) }) diff --git a/framework/bigcommerce/api/endpoints/catalog/products/index.ts b/framework/bigcommerce/api/endpoints/catalog/products/index.ts index 3e13e68b8..555740f60 100644 --- a/framework/bigcommerce/api/endpoints/catalog/products/index.ts +++ b/framework/bigcommerce/api/endpoints/catalog/products/index.ts @@ -1,4 +1,5 @@ -import type { GetAPISchema } from '@commerce/api' +import { GetAPISchema, createEndpoint } from '@commerce/api' +import productsEndpoint from '@commerce/api/endpoints/catalog/products' import type { ProductsSchema } from '../../../../types/product' import type { BigcommerceAPI } from '../../..' import getProducts from './get-products' @@ -7,4 +8,11 @@ export type ProductsAPI = GetAPISchema export type ProductsEndpoint = ProductsAPI['endpoint'] -export const handlers = { getProducts } +export const handlers: ProductsEndpoint['handlers'] = { getProducts } + +const productsApi = createEndpoint({ + handler: productsEndpoint, + handlers, +}) + +export default productsApi diff --git a/framework/bigcommerce/api/endpoints/checkout/checkout.ts b/framework/bigcommerce/api/endpoints/checkout/checkout.ts new file mode 100644 index 000000000..517a57950 --- /dev/null +++ b/framework/bigcommerce/api/endpoints/checkout/checkout.ts @@ -0,0 +1,62 @@ +import type { CheckoutEndpoint } from '.' + +const fullCheckout = true + +const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({ + req, + res, + config, +}) => { + const { cookies } = req + const cartId = cookies[config.cartCookie] + + if (!cartId) { + res.redirect('/cart') + return + } + + const { data } = await config.storeApiFetch( + `/v3/carts/${cartId}/redirect_urls`, + { + method: 'POST', + } + ) + + if (fullCheckout) { + res.redirect(data.checkout_url) + return + } + + // TODO: make the embedded checkout work too! + const html = ` + + + + + + Checkout + + + + +
+ + + ` + + res.status(200) + res.setHeader('Content-Type', 'text/html') + res.write(html) + res.end() +} + +export default checkout diff --git a/framework/bigcommerce/api/endpoints/checkout/index.ts b/framework/bigcommerce/api/endpoints/checkout/index.ts new file mode 100644 index 000000000..eaba32e47 --- /dev/null +++ b/framework/bigcommerce/api/endpoints/checkout/index.ts @@ -0,0 +1,18 @@ +import { GetAPISchema, createEndpoint } from '@commerce/api' +import checkoutEndpoint from '@commerce/api/endpoints/checkout' +import type { CheckoutSchema } from '../../../types/checkout' +import type { BigcommerceAPI } from '../..' +import checkout from './checkout' + +export type CheckoutAPI = GetAPISchema + +export type CheckoutEndpoint = CheckoutAPI['endpoint'] + +export const handlers: CheckoutEndpoint['handlers'] = { checkout } + +const checkoutApi = createEndpoint({ + handler: checkoutEndpoint, + handlers, +}) + +export default checkoutApi diff --git a/framework/bigcommerce/api/endpoints/wishlist/add-item.ts b/framework/bigcommerce/api/endpoints/wishlist/add-item.ts index c2fdd9eff..4c5970a5d 100644 --- a/framework/bigcommerce/api/endpoints/wishlist/add-item.ts +++ b/framework/bigcommerce/api/endpoints/wishlist/add-item.ts @@ -1,4 +1,4 @@ -import getCustomerWishlist from '../../../customer/get-customer-wishlist' +import getCustomerWishlist from '../../operations/get-customer-wishlist' import { parseWishlistItem } from '../../utils/parse-item' import getCustomerId from './utils/get-customer-id' import type { WishlistEndpoint } from '.' @@ -8,6 +8,7 @@ const addItem: WishlistEndpoint['handlers']['addItem'] = async ({ res, body: { customerToken, item }, config, + commerce, }) => { if (!item) { return res.status(400).json({ @@ -26,7 +27,7 @@ const addItem: WishlistEndpoint['handlers']['addItem'] = async ({ }) } - const { wishlist } = await getCustomerWishlist({ + const { wishlist } = await commerce.getCustomerWishlist({ variables: { customerId }, config, }) diff --git a/framework/bigcommerce/api/endpoints/wishlist/get-wishlist.ts b/framework/bigcommerce/api/endpoints/wishlist/get-wishlist.ts index 43dcc0bfb..21443119c 100644 --- a/framework/bigcommerce/api/endpoints/wishlist/get-wishlist.ts +++ b/framework/bigcommerce/api/endpoints/wishlist/get-wishlist.ts @@ -1,13 +1,14 @@ import type { Wishlist } from '../../../types/wishlist' import type { WishlistEndpoint } from '.' import getCustomerId from './utils/get-customer-id' -import getCustomerWishlist from '../../../customer/get-customer-wishlist' +import getCustomerWishlist from '../../operations/get-customer-wishlist' // Return wishlist info const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({ res, body: { customerToken, includeProducts }, config, + commerce, }) => { let result: { data?: Wishlist } = {} @@ -23,7 +24,7 @@ const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({ }) } - const { wishlist } = await getCustomerWishlist({ + const { wishlist } = await commerce.getCustomerWishlist({ variables: { customerId }, includeProducts, config, diff --git a/framework/bigcommerce/api/endpoints/wishlist/remove-item.ts b/framework/bigcommerce/api/endpoints/wishlist/remove-item.ts index 7a243b322..22ac31cf9 100644 --- a/framework/bigcommerce/api/endpoints/wishlist/remove-item.ts +++ b/framework/bigcommerce/api/endpoints/wishlist/remove-item.ts @@ -1,5 +1,5 @@ import type { Wishlist } from '../../../types/wishlist' -import getCustomerWishlist from '../../../customer/get-customer-wishlist' +import getCustomerWishlist from '../../operations/get-customer-wishlist' import getCustomerId from './utils/get-customer-id' import type { WishlistEndpoint } from '.' @@ -8,12 +8,13 @@ const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({ res, body: { customerToken, itemId }, config, + commerce, }) => { const customerId = customerToken && (await getCustomerId({ customerToken, config })) const { wishlist } = (customerId && - (await getCustomerWishlist({ + (await commerce.getCustomerWishlist({ variables: { customerId }, config, }))) || diff --git a/framework/bigcommerce/api/endpoints/wishlist/utils/get-customer-id.ts b/framework/bigcommerce/api/endpoints/wishlist/utils/get-customer-id.ts index 5f9fc49df..603f8be2d 100644 --- a/framework/bigcommerce/api/endpoints/wishlist/utils/get-customer-id.ts +++ b/framework/bigcommerce/api/endpoints/wishlist/utils/get-customer-id.ts @@ -15,7 +15,7 @@ async function getCustomerId({ }: { customerToken: string config: BigcommerceConfig -}): Promise { +}): Promise { const { data } = await config.fetch( getCustomerIdQuery, undefined, @@ -26,7 +26,7 @@ async function getCustomerId({ } ) - return data?.customer?.entityId + return String(data?.customer?.entityId) } export default getCustomerId diff --git a/framework/bigcommerce/api/index.ts b/framework/bigcommerce/api/index.ts index 9eeea5b49..300f1087b 100644 --- a/framework/bigcommerce/api/index.ts +++ b/framework/bigcommerce/api/index.ts @@ -1,4 +1,3 @@ -import type { NextApiHandler } from 'next' import type { RequestInit } from '@vercel/fetch' import { CommerceAPI, @@ -20,6 +19,10 @@ import login from './operations/login' import getAllPages from './operations/get-all-pages' import getPage from './operations/get-page' import getSiteInfo from './operations/get-site-info' +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' export interface BigcommerceConfig extends CommerceAPIConfig { // Indicates if the returned metadata with translations should be applied to the @@ -57,47 +60,9 @@ if (!(STORE_API_URL && STORE_API_TOKEN && STORE_API_CLIENT_ID)) { ) } -export class Config { - private config: BigcommerceConfig - - constructor(config: Omit) { - this.config = { - ...config, - // The customerCookie is not customizable for now, BC sets the cookie and it's - // not important to rename it - customerCookie: 'SHOP_TOKEN', - } - } - - getConfig(userConfig: Partial = {}) { - return Object.entries(userConfig).reduce( - (cfg, [key, value]) => Object.assign(cfg, { [key]: value }), - { ...this.config } - ) - } - - setConfig(newConfig: Partial) { - Object.assign(this.config, newConfig) - } -} - const ONE_DAY = 60 * 60 * 24 -const config = new Config({ - commerceUrl: API_URL, - apiToken: API_TOKEN, - cartCookie: process.env.BIGCOMMERCE_CART_COOKIE ?? 'bc_cartId', - cartCookieMaxAge: ONE_DAY * 30, - fetch: fetchGraphqlApi, - applyLocale: true, - // REST API only - storeApiUrl: STORE_API_URL, - storeApiToken: STORE_API_TOKEN, - storeApiClientId: STORE_API_CLIENT_ID, - storeChannelId: STORE_CHANNEL_ID, - storeApiFetch: fetchStoreApi, -}) -const config2: BigcommerceConfig = { +const config: BigcommerceConfig = { commerceUrl: API_URL, apiToken: API_TOKEN, customerCookie: 'SHOP_TOKEN', @@ -113,11 +78,19 @@ const config2: BigcommerceConfig = { storeApiFetch: fetchStoreApi, } -export const provider = { - config: config2, - operations: { login, getAllPages, getPage, getSiteInfo }, +const operations = { + login, + getAllPages, + getPage, + getSiteInfo, + getCustomerWishlist, + getAllProductPaths, + getAllProducts, + getProduct, } +export const provider = { config, operations } + export type Provider = typeof provider export type APIs = @@ -136,11 +109,3 @@ export function getCommerceApi

( ): BigcommerceAPI

{ return commerceApi(customProvider) } - -export function getConfig(userConfig?: Partial) { - return config.getConfig(userConfig) -} - -export function setConfig(newConfig: Partial) { - return config.setConfig(newConfig) -} diff --git a/framework/bigcommerce/api/operations/get-all-pages.ts b/framework/bigcommerce/api/operations/get-all-pages.ts index 2f07670ff..3a9b64b1f 100644 --- a/framework/bigcommerce/api/operations/get-all-pages.ts +++ b/framework/bigcommerce/api/operations/get-all-pages.ts @@ -10,13 +10,13 @@ export default function getAllPagesOperation({ commerce, }: OperationContext) { async function getAllPages(opts?: { - config?: BigcommerceConfig + config?: Partial preview?: boolean }): Promise async function getAllPages( opts: { - config?: BigcommerceConfig + config?: Partial preview?: boolean } & OperationOptions ): Promise @@ -26,13 +26,13 @@ export default function getAllPagesOperation({ preview, }: { url?: string - config?: BigcommerceConfig + config?: Partial preview?: boolean } = {}): Promise { - config = commerce.getConfig(config) + const cfg = commerce.getConfig(config) // RecursivePartial forces the method to check for every prop in the data, which is // required in case there's a custom `url` - const { data } = await config.storeApiFetch< + const { data } = await cfg.storeApiFetch< RecursivePartial<{ data: Page[] }> >('/v3/content/pages') const pages = (data as RecursiveRequired) ?? [] diff --git a/framework/bigcommerce/api/operations/get-all-product-paths.ts b/framework/bigcommerce/api/operations/get-all-product-paths.ts new file mode 100644 index 000000000..da7b457eb --- /dev/null +++ b/framework/bigcommerce/api/operations/get-all-product-paths.ts @@ -0,0 +1,66 @@ +import type { + OperationContext, + OperationOptions, +} from '@commerce/api/operations' +import type { GetAllProductPathsQuery } from '../../schema' +import type { GetAllProductPathsOperation } from '../../types/product' +import type { RecursivePartial, RecursiveRequired } from '../utils/types' +import filterEdges from '../utils/filter-edges' +import { BigcommerceConfig, Provider } from '..' + +export const getAllProductPathsQuery = /* GraphQL */ ` + query getAllProductPaths($first: Int = 100) { + site { + products(first: $first) { + edges { + node { + path + } + } + } + } + } +` + +export default function getAllProductPathsOperation({ + commerce, +}: OperationContext) { + async function getAllProductPaths< + T extends GetAllProductPathsOperation + >(opts?: { + variables?: T['variables'] + config?: BigcommerceConfig + }): Promise + + async function getAllProductPaths( + opts: { + variables?: T['variables'] + config?: BigcommerceConfig + } & OperationOptions + ): Promise + + async function getAllProductPaths({ + query = getAllProductPathsQuery, + variables, + config, + }: { + query?: string + variables?: T['variables'] + config?: BigcommerceConfig + } = {}): Promise { + config = commerce.getConfig(config) + // RecursivePartial forces the method to check for every prop in the data, which is + // required in case there's a custom `query` + const { data } = await config.fetch< + RecursivePartial + >(query, { variables }) + const products = data.site?.products?.edges + + return { + products: filterEdges(products as RecursiveRequired).map( + ({ node }) => node + ), + } + } + return getAllProductPaths +} diff --git a/framework/bigcommerce/api/operations/get-all-products.ts b/framework/bigcommerce/api/operations/get-all-products.ts new file mode 100644 index 000000000..c2652f5bf --- /dev/null +++ b/framework/bigcommerce/api/operations/get-all-products.ts @@ -0,0 +1,135 @@ +import type { + OperationContext, + OperationOptions, +} from '@commerce/api/operations' +import type { + GetAllProductsQuery, + GetAllProductsQueryVariables, +} from '../../schema' +import type { GetAllProductsOperation } from '../../types/product' +import type { RecursivePartial, RecursiveRequired } from '../utils/types' +import filterEdges from '../utils/filter-edges' +import setProductLocaleMeta from '../utils/set-product-locale-meta' +import { productConnectionFragment } from '../fragments/product' +import { BigcommerceConfig, Provider } from '..' +import { normalizeProduct } from '../../lib/normalize' + +export const getAllProductsQuery = /* GraphQL */ ` + query getAllProducts( + $hasLocale: Boolean = false + $locale: String = "null" + $entityIds: [Int!] + $first: Int = 10 + $products: Boolean = false + $featuredProducts: Boolean = false + $bestSellingProducts: Boolean = false + $newestProducts: Boolean = false + ) { + site { + products(first: $first, entityIds: $entityIds) @include(if: $products) { + ...productConnnection + } + featuredProducts(first: $first) @include(if: $featuredProducts) { + ...productConnnection + } + bestSellingProducts(first: $first) @include(if: $bestSellingProducts) { + ...productConnnection + } + newestProducts(first: $first) @include(if: $newestProducts) { + ...productConnnection + } + } + } + + ${productConnectionFragment} +` + +export type ProductEdge = NonNullable< + NonNullable[0] +> + +export type ProductNode = ProductEdge['node'] + +export type GetAllProductsResult< + T extends Record = { + products: ProductEdge[] + } +> = T + +function getProductsType( + relevance?: GetAllProductsOperation['variables']['relevance'] +) { + switch (relevance) { + case 'featured': + return 'featuredProducts' + case 'best_selling': + return 'bestSellingProducts' + case 'newest': + return 'newestProducts' + default: + return 'products' + } +} + +export default function getAllProductsOperation({ + commerce, +}: OperationContext) { + async function getAllProducts(opts?: { + variables?: T['variables'] + config?: Partial + preview?: boolean + }): Promise + + async function getAllProducts( + opts: { + variables?: T['variables'] + config?: Partial + preview?: boolean + } & OperationOptions + ): Promise + + async function getAllProducts({ + query = getAllProductsQuery, + variables: vars = {}, + config: cfg, + }: { + query?: string + variables?: T['variables'] + config?: Partial + preview?: boolean + } = {}): Promise { + const config = commerce.getConfig(cfg) + const { locale } = config + const field = getProductsType(vars.relevance) + const variables: GetAllProductsQueryVariables = { + locale, + hasLocale: !!locale, + } + + variables[field] = true + + if (vars.first) variables.first = vars.first + if (vars.ids) variables.entityIds = vars.ids.map((id) => Number(id)) + + // RecursivePartial forces the method to check for every prop in the data, which is + // required in case there's a custom `query` + const { data } = await config.fetch>( + query, + { variables } + ) + const edges = data.site?.[field]?.edges + const products = filterEdges(edges as RecursiveRequired) + + if (locale && config.applyLocale) { + products.forEach((product: RecursivePartial) => { + if (product.node) setProductLocaleMeta(product.node) + }) + } + + return { + products: products.map(({ node }) => normalizeProduct(node as any)), + } + } + + return getAllProducts +} diff --git a/framework/bigcommerce/api/operations/get-customer-wishlist.ts b/framework/bigcommerce/api/operations/get-customer-wishlist.ts new file mode 100644 index 000000000..fc9487ffe --- /dev/null +++ b/framework/bigcommerce/api/operations/get-customer-wishlist.ts @@ -0,0 +1,81 @@ +import type { + OperationContext, + OperationOptions, +} from '@commerce/api/operations' +import type { + GetCustomerWishlistOperation, + Wishlist, +} from '../../types/wishlist' +import type { RecursivePartial, RecursiveRequired } from '../utils/types' +import { BigcommerceConfig, Provider } from '..' +import getAllProducts, { ProductEdge } from './get-all-products' + +export default function getCustomerWishlistOperation({ + commerce, +}: OperationContext) { + async function getCustomerWishlist< + T extends GetCustomerWishlistOperation + >(opts: { + variables: T['variables'] + config?: BigcommerceConfig + includeProducts?: boolean + }): Promise + + async function getCustomerWishlist( + opts: { + variables: T['variables'] + config?: BigcommerceConfig + includeProducts?: boolean + } & OperationOptions + ): Promise + + async function getCustomerWishlist({ + config, + variables, + includeProducts, + }: { + url?: string + variables: T['variables'] + config?: BigcommerceConfig + includeProducts?: boolean + }): Promise { + config = commerce.getConfig(config) + + const { data = [] } = await config.storeApiFetch< + RecursivePartial<{ data: Wishlist[] }> + >(`/v3/wishlists?customer_id=${variables.customerId}`) + const wishlist = data[0] + + if (includeProducts && wishlist?.items?.length) { + const ids = wishlist.items + ?.map((item) => (item?.product_id ? String(item?.product_id) : null)) + .filter((id): id is string => !!id) + + if (ids?.length) { + const graphqlData = await commerce.getAllProducts({ + variables: { first: 100, ids }, + config, + }) + // Put the products in an object that we can use to get them by id + const productsById = graphqlData.products.reduce<{ + [k: number]: ProductEdge + }>((prods, p) => { + prods[Number(p.id)] = p as any + return prods + }, {}) + // Populate the wishlist items with the graphql products + wishlist.items.forEach((item) => { + const product = item && productsById[item.product_id!] + if (item && product) { + // @ts-ignore Fix this type when the wishlist type is properly defined + item.product = product + } + }) + } + } + + return { wishlist: wishlist as RecursiveRequired } + } + + return getCustomerWishlist +} diff --git a/framework/bigcommerce/api/operations/get-page.ts b/framework/bigcommerce/api/operations/get-page.ts index a5bca327c..e8f852e92 100644 --- a/framework/bigcommerce/api/operations/get-page.ts +++ b/framework/bigcommerce/api/operations/get-page.ts @@ -11,14 +11,14 @@ export default function getPageOperation({ }: OperationContext) { async function getPage(opts: { variables: T['variables'] - config?: BigcommerceConfig + config?: Partial preview?: boolean }): Promise async function getPage( opts: { variables: T['variables'] - config?: BigcommerceConfig + config?: Partial preview?: boolean } & OperationOptions ): Promise @@ -31,13 +31,13 @@ export default function getPageOperation({ }: { url?: string variables: T['variables'] - config?: BigcommerceConfig + config?: Partial preview?: boolean }): Promise { - config = commerce.getConfig(config) + const cfg = commerce.getConfig(config) // RecursivePartial forces the method to check for every prop in the data, which is // required in case there's a custom `url` - const { data } = await config.storeApiFetch< + const { data } = await cfg.storeApiFetch< RecursivePartial<{ data: Page[] }> >(url || `/v3/content/pages?id=${variables.id}&include=body`) const firstPage = data?.[0] diff --git a/framework/bigcommerce/api/operations/get-product.ts b/framework/bigcommerce/api/operations/get-product.ts new file mode 100644 index 000000000..fb356e952 --- /dev/null +++ b/framework/bigcommerce/api/operations/get-product.ts @@ -0,0 +1,119 @@ +import type { + OperationContext, + OperationOptions, +} from '@commerce/api/operations' +import type { GetProductOperation } from '../../types/product' +import type { GetProductQuery, GetProductQueryVariables } from '../../schema' +import setProductLocaleMeta from '../utils/set-product-locale-meta' +import { productInfoFragment } from '../fragments/product' +import { BigcommerceConfig, Provider } from '..' +import { normalizeProduct } from '../../lib/normalize' + +export const getProductQuery = /* GraphQL */ ` + query getProduct( + $hasLocale: Boolean = false + $locale: String = "null" + $path: String! + ) { + site { + route(path: $path) { + node { + __typename + ... on Product { + ...productInfo + variants { + edges { + node { + entityId + defaultImage { + urlOriginal + altText + isDefault + } + prices { + ...productPrices + } + inventory { + aggregated { + availableToSell + warningLevel + } + isInStock + } + productOptions { + edges { + node { + __typename + entityId + displayName + ...multipleChoiceOption + } + } + } + } + } + } + } + } + } + } + } + + ${productInfoFragment} +` + +// TODO: See if this type is useful for defining the Product type +// export type ProductNode = Extract< +// GetProductQuery['site']['route']['node'], +// { __typename: 'Product' } +// > + +export default function getAllProductPathsOperation({ + commerce, +}: OperationContext) { + async function getProduct(opts: { + variables: T['variables'] + config?: Partial + preview?: boolean + }): Promise + + async function getProduct( + opts: { + variables: T['variables'] + config?: Partial + preview?: boolean + } & OperationOptions + ): Promise + + async function getProduct({ + query = getProductQuery, + variables: { slug, ...vars }, + config: cfg, + }: { + query?: string + variables: T['variables'] + config?: Partial + preview?: boolean + }): Promise { + const config = commerce.getConfig(cfg) + const { locale } = config + const variables: GetProductQueryVariables = { + locale, + hasLocale: !!locale, + path: slug ? `/${slug}/` : vars.path!, + } + const { data } = await config.fetch(query, { variables }) + const product = data.site?.route?.node + + if (product?.__typename === 'Product') { + if (locale && config.applyLocale) { + setProductLocaleMeta(product) + } + + return { product: normalizeProduct(product as any) } + } + + return {} + } + return getProduct +} diff --git a/framework/bigcommerce/api/operations/get-site-info.ts b/framework/bigcommerce/api/operations/get-site-info.ts index cca11ed74..afe5f3626 100644 --- a/framework/bigcommerce/api/operations/get-site-info.ts +++ b/framework/bigcommerce/api/operations/get-site-info.ts @@ -53,13 +53,13 @@ export default function getSiteInfoOperation({ commerce, }: OperationContext) { async function getSiteInfo(opts?: { - config?: BigcommerceConfig + config?: Partial preview?: boolean }): Promise async function getSiteInfo( opts: { - config?: BigcommerceConfig + config?: Partial preview?: boolean } & OperationOptions ): Promise @@ -69,15 +69,13 @@ export default function getSiteInfoOperation({ config, }: { query?: string - config?: BigcommerceConfig + config?: Partial preview?: boolean } = {}): Promise { - config = commerce.getConfig(config) + const cfg = commerce.getConfig(config) // RecursivePartial forces the method to check for every prop in the data, which is // required in case there's a custom `query` - const { data } = await config.fetch>( - query - ) + const { data } = await cfg.fetch>(query) const categories = data.site?.categoryTree const brands = data.site?.brands?.edges diff --git a/framework/bigcommerce/api/utils/create-api-handler.ts b/framework/bigcommerce/api/utils/create-api-handler.ts deleted file mode 100644 index c1d651d9b..000000000 --- a/framework/bigcommerce/api/utils/create-api-handler.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next' -import { BigcommerceConfig, getConfig } from '..' - -export type BigcommerceApiHandler< - T = any, - H extends BigcommerceHandlers = {}, - Options extends {} = {} -> = ( - req: NextApiRequest, - res: NextApiResponse>, - config: BigcommerceConfig, - handlers: H, - // Custom configs that may be used by a particular handler - options: Options -) => void | Promise - -export type BigcommerceHandler = (options: { - req: NextApiRequest - res: NextApiResponse> - config: BigcommerceConfig - body: Body -}) => void | Promise - -export type BigcommerceHandlers = { - [k: string]: BigcommerceHandler -} - -export type BigcommerceApiResponse = { - data: T | null - errors?: { message: string; code?: string }[] -} - -export default function createApiHandler< - T = any, - H extends BigcommerceHandlers = {}, - Options extends {} = {} ->( - handler: BigcommerceApiHandler, - handlers: H, - defaultOptions: Options -) { - return function getApiHandler({ - config, - operations, - options, - }: { - config?: BigcommerceConfig - operations?: Partial - options?: Options extends {} ? Partial : never - } = {}): NextApiHandler { - const ops = { ...handlers, ...operations } - const opts = { ...defaultOptions, ...options } - - return function apiHandler(req, res) { - return handler(req, res, getConfig(config), ops, opts) - } - } -} diff --git a/framework/bigcommerce/api/utils/fetch-graphql-api.ts b/framework/bigcommerce/api/utils/fetch-graphql-api.ts index a449b81e0..9c2eaa2ac 100644 --- a/framework/bigcommerce/api/utils/fetch-graphql-api.ts +++ b/framework/bigcommerce/api/utils/fetch-graphql-api.ts @@ -1,15 +1,15 @@ import { FetcherError } from '@commerce/utils/errors' import type { GraphQLFetcher } from '@commerce/api' -import { getConfig } from '..' +import { provider } from '..' import fetch from './fetch' +const { config } = provider const fetchGraphqlApi: GraphQLFetcher = async ( query: string, { variables, preview } = {}, fetchOptions ) => { // log.warn(query) - const config = getConfig() const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), { ...fetchOptions, method: 'POST', diff --git a/framework/bigcommerce/api/utils/fetch-store-api.ts b/framework/bigcommerce/api/utils/fetch-store-api.ts index 7e59b9f06..68817417e 100644 --- a/framework/bigcommerce/api/utils/fetch-store-api.ts +++ b/framework/bigcommerce/api/utils/fetch-store-api.ts @@ -1,13 +1,14 @@ import type { RequestInit, Response } from '@vercel/fetch' -import { getConfig } from '..' +import { provider } from '..' import { BigcommerceApiError, BigcommerceNetworkError } from './errors' import fetch from './fetch' +const { config } = provider + export default async function fetchStoreApi( endpoint: string, options?: RequestInit ): Promise { - const config = getConfig() let res: Response try { diff --git a/framework/bigcommerce/api/utils/is-allowed-method.ts b/framework/bigcommerce/api/utils/is-allowed-method.ts deleted file mode 100644 index 78bbba568..000000000 --- a/framework/bigcommerce/api/utils/is-allowed-method.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from 'next' - -export default function isAllowedMethod( - req: NextApiRequest, - res: NextApiResponse, - allowedMethods: string[] -) { - const methods = allowedMethods.includes('OPTIONS') - ? allowedMethods - : [...allowedMethods, 'OPTIONS'] - - if (!req.method || !methods.includes(req.method)) { - res.status(405) - res.setHeader('Allow', methods.join(', ')) - res.end() - return false - } - - if (req.method === 'OPTIONS') { - res.status(200) - res.setHeader('Allow', methods.join(', ')) - res.setHeader('Content-Length', '0') - res.end() - return false - } - - return true -} diff --git a/framework/bigcommerce/api/utils/set-product-locale-meta.ts b/framework/bigcommerce/api/utils/set-product-locale-meta.ts index 974a197bd..767286477 100644 --- a/framework/bigcommerce/api/utils/set-product-locale-meta.ts +++ b/framework/bigcommerce/api/utils/set-product-locale-meta.ts @@ -1,4 +1,4 @@ -import type { ProductNode } from '../../product/get-all-products' +import type { ProductNode } from '../operations/get-all-products' import type { RecursivePartial } from './types' export default function setProductLocaleMeta( diff --git a/framework/bigcommerce/api/wishlist/handlers/add-item.ts b/framework/bigcommerce/api/wishlist/handlers/add-item.ts deleted file mode 100644 index cbf0ec9d6..000000000 --- a/framework/bigcommerce/api/wishlist/handlers/add-item.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { WishlistHandlers } from '..' -import getCustomerId from '../../endpoints/wishlist/utils/get-customer-id' -import getCustomerWishlist from '../../../customer/get-customer-wishlist' -import { parseWishlistItem } from '../../utils/parse-item' - -// Returns the wishlist of the signed customer -const addItem: WishlistHandlers['addItem'] = async ({ - res, - body: { customerToken, item }, - config, -}) => { - if (!item) { - return res.status(400).json({ - data: null, - errors: [{ message: 'Missing item' }], - }) - } - - const customerId = - customerToken && (await getCustomerId({ customerToken, config })) - - if (!customerId) { - return res.status(400).json({ - data: null, - errors: [{ message: 'Invalid request' }], - }) - } - - const { wishlist } = await getCustomerWishlist({ - variables: { customerId }, - config, - }) - const options = { - method: 'POST', - body: JSON.stringify( - wishlist - ? { - items: [parseWishlistItem(item)], - } - : { - name: 'Wishlist', - customer_id: customerId, - items: [parseWishlistItem(item)], - is_public: false, - } - ), - } - - const { data } = wishlist - ? await config.storeApiFetch(`/v3/wishlists/${wishlist.id}/items`, options) - : await config.storeApiFetch('/v3/wishlists', options) - - res.status(200).json({ data }) -} - -export default addItem diff --git a/framework/bigcommerce/api/wishlist/handlers/get-wishlist.ts b/framework/bigcommerce/api/wishlist/handlers/get-wishlist.ts deleted file mode 100644 index a1c74fb29..000000000 --- a/framework/bigcommerce/api/wishlist/handlers/get-wishlist.ts +++ /dev/null @@ -1,37 +0,0 @@ -import getCustomerId from '../../endpoints/wishlist/utils/get-customer-id' -import getCustomerWishlist from '../../../customer/get-customer-wishlist' -import type { Wishlist, WishlistHandlers } from '..' - -// Return wishlist info -const getWishlist: WishlistHandlers['getWishlist'] = async ({ - res, - body: { customerToken, includeProducts }, - config, -}) => { - let result: { data?: Wishlist } = {} - - if (customerToken) { - const customerId = - customerToken && (await getCustomerId({ customerToken, config })) - - if (!customerId) { - // If the customerToken is invalid, then this request is too - return res.status(404).json({ - data: null, - errors: [{ message: 'Wishlist not found' }], - }) - } - - const { wishlist } = await getCustomerWishlist({ - variables: { customerId }, - includeProducts, - config, - }) - - result = { data: wishlist } - } - - res.status(200).json({ data: result.data ?? null }) -} - -export default getWishlist diff --git a/framework/bigcommerce/api/wishlist/handlers/remove-item.ts b/framework/bigcommerce/api/wishlist/handlers/remove-item.ts deleted file mode 100644 index ea5764a28..000000000 --- a/framework/bigcommerce/api/wishlist/handlers/remove-item.ts +++ /dev/null @@ -1,39 +0,0 @@ -import getCustomerId from '../../endpoints/wishlist/utils/get-customer-id' -import getCustomerWishlist, { - Wishlist, -} from '../../../customer/get-customer-wishlist' -import type { WishlistHandlers } from '..' - -// Return current wishlist info -const removeItem: WishlistHandlers['removeItem'] = async ({ - res, - body: { customerToken, itemId }, - config, -}) => { - const customerId = - customerToken && (await getCustomerId({ customerToken, config })) - const { wishlist } = - (customerId && - (await getCustomerWishlist({ - variables: { customerId }, - config, - }))) || - {} - - if (!wishlist || !itemId) { - return res.status(400).json({ - data: null, - errors: [{ message: 'Invalid request' }], - }) - } - - const result = await config.storeApiFetch<{ data: Wishlist } | null>( - `/v3/wishlists/${wishlist.id}/items/${itemId}`, - { method: 'DELETE' } - ) - const data = result?.data ?? null - - res.status(200).json({ data }) -} - -export default removeItem diff --git a/framework/bigcommerce/api/wishlist/index.ts b/framework/bigcommerce/api/wishlist/index.ts deleted file mode 100644 index 7c700689c..000000000 --- a/framework/bigcommerce/api/wishlist/index.ts +++ /dev/null @@ -1,104 +0,0 @@ -import isAllowedMethod from '../utils/is-allowed-method' -import createApiHandler, { - BigcommerceApiHandler, - BigcommerceHandler, -} from '../utils/create-api-handler' -import { BigcommerceApiError } from '../utils/errors' -import type { - Wishlist, - WishlistItem, -} from '../../customer/get-customer-wishlist' -import getWishlist from './handlers/get-wishlist' -import addItem from './handlers/add-item' -import removeItem from './handlers/remove-item' -import type { Product, ProductVariant, Customer } from '@commerce/types' - -export type { Wishlist, WishlistItem } - -export type ItemBody = { - productId: Product['id'] - variantId: ProductVariant['id'] -} - -export type AddItemBody = { item: ItemBody } - -export type RemoveItemBody = { itemId: Product['id'] } - -export type WishlistBody = { - customer_id: Customer['entityId'] - is_public: number - name: string - items: any[] -} - -export type AddWishlistBody = { wishlist: WishlistBody } - -export type WishlistHandlers = { - getWishlist: BigcommerceHandler< - Wishlist, - { customerToken?: string; includeProducts?: boolean } - > - addItem: BigcommerceHandler< - Wishlist, - { customerToken?: string } & Partial - > - removeItem: BigcommerceHandler< - Wishlist, - { customerToken?: string } & Partial - > -} - -const METHODS = ['GET', 'POST', 'DELETE'] - -// TODO: a complete implementation should have schema validation for `req.body` -const wishlistApi: BigcommerceApiHandler = async ( - req, - res, - config, - handlers -) => { - if (!isAllowedMethod(req, res, METHODS)) return - - 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']({ req, res, config, body }) - } - - // Add an item to the wishlist - if (req.method === 'POST') { - const body = { ...req.body, customerToken } - return await handlers['addItem']({ req, res, config, body }) - } - - // Remove an item from the wishlist - if (req.method === 'DELETE') { - const body = { ...req.body, customerToken } - 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 = { - getWishlist, - addItem, - removeItem, -} - -export default createApiHandler(wishlistApi, handlers, {}) diff --git a/framework/bigcommerce/customer/get-customer-wishlist.ts b/framework/bigcommerce/customer/get-customer-wishlist.ts deleted file mode 100644 index 97e5654a9..000000000 --- a/framework/bigcommerce/customer/get-customer-wishlist.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { RecursivePartial, RecursiveRequired } from '../api/utils/types' -import { definitions } from '../api/definitions/wishlist' -import { BigcommerceConfig, getConfig } from '../api' -import getAllProducts, { ProductEdge } from '../product/get-all-products' - -export type Wishlist = Omit & { - items?: WishlistItem[] -} - -export type WishlistItem = NonNullable< - definitions['wishlist_Full']['items'] ->[0] & { - product?: ProductEdge['node'] -} - -export type GetCustomerWishlistResult< - T extends { wishlist?: any } = { wishlist?: Wishlist } -> = T - -export type GetCustomerWishlistVariables = { - customerId: number -} - -async function getCustomerWishlist(opts: { - variables: GetCustomerWishlistVariables - config?: BigcommerceConfig - includeProducts?: boolean -}): Promise - -async function getCustomerWishlist< - T extends { wishlist?: any }, - V = any ->(opts: { - url: string - variables: V - config?: BigcommerceConfig - includeProducts?: boolean -}): Promise> - -async function getCustomerWishlist({ - config, - variables, - includeProducts, -}: { - url?: string - variables: GetCustomerWishlistVariables - config?: BigcommerceConfig - includeProducts?: boolean -}): Promise { - config = getConfig(config) - - const { data = [] } = await config.storeApiFetch< - RecursivePartial<{ data: Wishlist[] }> - >(`/v3/wishlists?customer_id=${variables.customerId}`) - const wishlist = data[0] - - if (includeProducts && wishlist?.items?.length) { - const entityIds = wishlist.items - ?.map((item) => item?.product_id) - .filter((id): id is number => !!id) - - if (entityIds?.length) { - const graphqlData = await getAllProducts({ - variables: { first: 100, entityIds }, - config, - }) - // Put the products in an object that we can use to get them by id - const productsById = graphqlData.products.reduce<{ - [k: number]: ProductEdge - }>((prods, p) => { - prods[Number(p.id)] = p as any - return prods - }, {}) - // Populate the wishlist items with the graphql products - wishlist.items.forEach((item) => { - const product = item && productsById[item.product_id!] - if (item && product) { - // @ts-ignore Fix this type when the wishlist type is properly defined - item.product = product - } - }) - } - } - - return { wishlist: wishlist as RecursiveRequired } -} - -export default getCustomerWishlist diff --git a/framework/bigcommerce/product/get-all-product-paths.ts b/framework/bigcommerce/product/get-all-product-paths.ts deleted file mode 100644 index c1b23b38d..000000000 --- a/framework/bigcommerce/product/get-all-product-paths.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { - GetAllProductPathsQuery, - GetAllProductPathsQueryVariables, -} from '../schema' -import type { RecursivePartial, RecursiveRequired } from '../api/utils/types' -import filterEdges from '../api/utils/filter-edges' -import { BigcommerceConfig, getConfig } from '../api' - -export const getAllProductPathsQuery = /* GraphQL */ ` - query getAllProductPaths($first: Int = 100) { - site { - products(first: $first) { - edges { - node { - path - } - } - } - } - } -` - -export type ProductPath = NonNullable< - NonNullable[0] -> - -export type ProductPaths = ProductPath[] - -export type { GetAllProductPathsQueryVariables } - -export type GetAllProductPathsResult< - T extends { products: any[] } = { products: ProductPaths } -> = T - -async function getAllProductPaths(opts?: { - variables?: GetAllProductPathsQueryVariables - config?: BigcommerceConfig -}): Promise - -async function getAllProductPaths< - T extends { products: any[] }, - V = any ->(opts: { - query: string - variables?: V - config?: BigcommerceConfig -}): Promise> - -async function getAllProductPaths({ - query = getAllProductPathsQuery, - variables, - config, -}: { - query?: string - variables?: GetAllProductPathsQueryVariables - config?: BigcommerceConfig -} = {}): Promise { - config = getConfig(config) - // RecursivePartial forces the method to check for every prop in the data, which is - // required in case there's a custom `query` - const { data } = await config.fetch< - RecursivePartial - >(query, { variables }) - const products = data.site?.products?.edges - - return { - products: filterEdges(products as RecursiveRequired), - } -} - -export default getAllProductPaths diff --git a/framework/bigcommerce/product/get-all-products.ts b/framework/bigcommerce/product/get-all-products.ts deleted file mode 100644 index 4c563bc62..000000000 --- a/framework/bigcommerce/product/get-all-products.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { - GetAllProductsQuery, - GetAllProductsQueryVariables, -} from '../schema' -import type { Product } from '@commerce/types' -import type { RecursivePartial, RecursiveRequired } from '../api/utils/types' -import filterEdges from '../api/utils/filter-edges' -import setProductLocaleMeta from '../api/utils/set-product-locale-meta' -import { productConnectionFragment } from '../api/fragments/product' -import { BigcommerceConfig, getConfig } from '../api' -import { normalizeProduct } from '../lib/normalize' - -export const getAllProductsQuery = /* GraphQL */ ` - query getAllProducts( - $hasLocale: Boolean = false - $locale: String = "null" - $entityIds: [Int!] - $first: Int = 10 - $products: Boolean = false - $featuredProducts: Boolean = false - $bestSellingProducts: Boolean = false - $newestProducts: Boolean = false - ) { - site { - products(first: $first, entityIds: $entityIds) @include(if: $products) { - ...productConnnection - } - featuredProducts(first: $first) @include(if: $featuredProducts) { - ...productConnnection - } - bestSellingProducts(first: $first) @include(if: $bestSellingProducts) { - ...productConnnection - } - newestProducts(first: $first) @include(if: $newestProducts) { - ...productConnnection - } - } - } - - ${productConnectionFragment} -` - -export type ProductEdge = NonNullable< - NonNullable[0] -> - -export type ProductNode = ProductEdge['node'] - -export type GetAllProductsResult< - T extends Record = { - products: ProductEdge[] - } -> = T - -const FIELDS = [ - 'products', - 'featuredProducts', - 'bestSellingProducts', - 'newestProducts', -] - -export type ProductTypes = - | 'products' - | 'featuredProducts' - | 'bestSellingProducts' - | 'newestProducts' - -export type ProductVariables = { field?: ProductTypes } & Omit< - GetAllProductsQueryVariables, - ProductTypes | 'hasLocale' -> - -async function getAllProducts(opts?: { - variables?: ProductVariables - config?: BigcommerceConfig - preview?: boolean -}): Promise<{ products: Product[] }> - -async function getAllProducts< - T extends Record, - V = any ->(opts: { - query: string - variables?: V - config?: BigcommerceConfig - preview?: boolean -}): Promise> - -async function getAllProducts({ - query = getAllProductsQuery, - variables: { field = 'products', ...vars } = {}, - config, -}: { - query?: string - variables?: ProductVariables - config?: BigcommerceConfig - preview?: boolean - // TODO: fix the product type here -} = {}): Promise<{ products: Product[] | any[] }> { - config = getConfig(config) - - const locale = vars.locale || config.locale - const variables: GetAllProductsQueryVariables = { - ...vars, - locale, - hasLocale: !!locale, - } - - if (!FIELDS.includes(field)) { - throw new Error( - `The field variable has to match one of ${FIELDS.join(', ')}` - ) - } - - variables[field] = true - - // RecursivePartial forces the method to check for every prop in the data, which is - // required in case there's a custom `query` - const { data } = await config.fetch>( - query, - { variables } - ) - const edges = data.site?.[field]?.edges - const products = filterEdges(edges as RecursiveRequired) - - if (locale && config.applyLocale) { - products.forEach((product: RecursivePartial) => { - if (product.node) setProductLocaleMeta(product.node) - }) - } - - return { products: products.map(({ node }) => normalizeProduct(node as any)) } -} - -export default getAllProducts diff --git a/framework/bigcommerce/product/get-product.ts b/framework/bigcommerce/product/get-product.ts deleted file mode 100644 index b52568b62..000000000 --- a/framework/bigcommerce/product/get-product.ts +++ /dev/null @@ -1,121 +0,0 @@ -import type { GetProductQuery, GetProductQueryVariables } from '../schema' -import setProductLocaleMeta from '../api/utils/set-product-locale-meta' -import { productInfoFragment } from '../api/fragments/product' -import { BigcommerceConfig, getConfig } from '../api' -import { normalizeProduct } from '../lib/normalize' -import type { Product } from '@commerce/types' - -export const getProductQuery = /* GraphQL */ ` - query getProduct( - $hasLocale: Boolean = false - $locale: String = "null" - $path: String! - ) { - site { - route(path: $path) { - node { - __typename - ... on Product { - ...productInfo - variants { - edges { - node { - entityId - defaultImage { - urlOriginal - altText - isDefault - } - prices { - ...productPrices - } - inventory { - aggregated { - availableToSell - warningLevel - } - isInStock - } - productOptions { - edges { - node { - __typename - entityId - displayName - ...multipleChoiceOption - } - } - } - } - } - } - } - } - } - } - } - - ${productInfoFragment} -` - -export type ProductNode = Extract< - GetProductQuery['site']['route']['node'], - { __typename: 'Product' } -> - -export type GetProductResult< - T extends { product?: any } = { product?: ProductNode } -> = T - -export type ProductVariables = { locale?: string } & ( - | { path: string; slug?: never } - | { path?: never; slug: string } -) - -async function getProduct(opts: { - variables: ProductVariables - config?: BigcommerceConfig - preview?: boolean -}): Promise - -async function getProduct(opts: { - query: string - variables: V - config?: BigcommerceConfig - preview?: boolean -}): Promise> - -async function getProduct({ - query = getProductQuery, - variables: { slug, ...vars }, - config, -}: { - query?: string - variables: ProductVariables - config?: BigcommerceConfig - preview?: boolean -}): Promise { - config = getConfig(config) - - const locale = vars.locale || config.locale - const variables: GetProductQueryVariables = { - ...vars, - locale, - hasLocale: !!locale, - path: slug ? `/${slug}/` : vars.path!, - } - const { data } = await config.fetch(query, { variables }) - const product = data.site?.route?.node - - if (product?.__typename === 'Product') { - if (locale && config.applyLocale) { - setProductLocaleMeta(product) - } - - return { product: normalizeProduct(product as any) } - } - - return {} -} - -export default getProduct diff --git a/framework/bigcommerce/product/index.ts b/framework/bigcommerce/product/index.ts index b290c189f..426a3edcd 100644 --- a/framework/bigcommerce/product/index.ts +++ b/framework/bigcommerce/product/index.ts @@ -1,4 +1,2 @@ export { default as usePrice } from './use-price' export { default as useSearch } from './use-search' -export { default as getProduct } from './get-product' -export { default as getAllProducts } from './get-all-products' diff --git a/framework/bigcommerce/types/checkout.ts b/framework/bigcommerce/types/checkout.ts new file mode 100644 index 000000000..4e2412ef6 --- /dev/null +++ b/framework/bigcommerce/types/checkout.ts @@ -0,0 +1 @@ +export * from '@commerce/types/checkout' diff --git a/framework/bigcommerce/types/wishlist.ts b/framework/bigcommerce/types/wishlist.ts index 9c98a8df4..1e148b88c 100644 --- a/framework/bigcommerce/types/wishlist.ts +++ b/framework/bigcommerce/types/wishlist.ts @@ -1,6 +1,6 @@ import * as Core from '@commerce/types/wishlist' import { definitions } from '../api/definitions/wishlist' -import type { ProductEdge } from '../product/get-all-products' +import type { ProductEdge } from '../api/operations/get-all-products' export * from '@commerce/types/wishlist' @@ -20,3 +20,4 @@ export type WishlistTypes = { } export type WishlistSchema = Core.WishlistSchema +export type GetCustomerWishlistOperation = Core.GetCustomerWishlistOperation diff --git a/framework/commerce/api/endpoints/checkout.ts b/framework/commerce/api/endpoints/checkout.ts new file mode 100644 index 000000000..b39239a6a --- /dev/null +++ b/framework/commerce/api/endpoints/checkout.ts @@ -0,0 +1,35 @@ +import type { CheckoutSchema } from '../../types/checkout' +import { CommerceAPIError } from '../utils/errors' +import isAllowedOperation from '../utils/is-allowed-operation' +import type { GetAPISchema } from '..' + +const checkoutEndpoint: GetAPISchema< + any, + CheckoutSchema +>['endpoint']['handler'] = async (ctx) => { + const { req, res, handlers } = ctx + + if ( + !isAllowedOperation(req, res, { + GET: handlers['checkout'], + }) + ) { + return + } + + try { + const body = null + return await handlers['checkout']({ ...ctx, body }) + } catch (error) { + console.error(error) + + const message = + error instanceof CommerceAPIError + ? 'An unexpected error ocurred with the Commerce API' + : 'An unexpected error ocurred' + + res.status(500).json({ data: null, errors: [{ message }] }) + } +} + +export default checkoutEndpoint diff --git a/framework/commerce/api/index.ts b/framework/commerce/api/index.ts index 6a28a9597..8a08b92ee 100644 --- a/framework/commerce/api/index.ts +++ b/framework/commerce/api/index.ts @@ -8,6 +8,7 @@ import type { LogoutSchema } from '../types/logout' import type { SignupSchema } from '../types/signup' import type { ProductsSchema } from '../types/product' import type { WishlistSchema } from '../types/wishlist' +import type { CheckoutSchema } from '../types/checkout' import { defaultOperations, OPERATIONS, @@ -23,6 +24,7 @@ export type APISchemas = | SignupSchema | ProductsSchema | WishlistSchema + | CheckoutSchema export type GetAPISchema< C extends CommerceAPI, diff --git a/framework/commerce/api/operations.ts b/framework/commerce/api/operations.ts index b6529f059..aa30f426e 100644 --- a/framework/commerce/api/operations.ts +++ b/framework/commerce/api/operations.ts @@ -2,6 +2,12 @@ import type { ServerResponse } from 'http' import type { LoginOperation } from '../types/login' import type { GetAllPagesOperation, GetPageOperation } from '../types/page' import type { GetSiteInfoOperation } from '../types/site' +import type { GetCustomerWishlistOperation } from '../types/wishlist' +import type { + GetAllProductPathsOperation, + GetAllProductsOperation, + GetProductOperation, +} from '../types/product' import type { APIProvider, CommerceAPI } from '.' const noop = () => { @@ -77,6 +83,68 @@ export type Operations

= { } & OperationOptions ): Promise } + + getCustomerWishlist: { + (opts: { + variables: T['variables'] + config?: P['config'] + includeProducts?: boolean + }): Promise + + ( + opts: { + variables: T['variables'] + config?: P['config'] + includeProducts?: boolean + } & OperationOptions + ): Promise + } + + getAllProductPaths: { + (opts: { + variables?: T['variables'] + config?: P['config'] + }): Promise + + ( + opts: { + variables?: T['variables'] + config?: P['config'] + } & OperationOptions + ): Promise + } + + getAllProducts: { + (opts: { + variables?: T['variables'] + config?: P['config'] + preview?: boolean + }): Promise + + ( + opts: { + variables?: T['variables'] + config?: P['config'] + preview?: boolean + } & OperationOptions + ): Promise + } + + getProduct: { + (opts: { + variables: T['variables'] + config?: P['config'] + preview?: boolean + }): Promise + + ( + opts: { + variables: T['variables'] + config?: P['config'] + preview?: boolean + } & OperationOptions + ): Promise + } } export type APIOperations

= { diff --git a/framework/commerce/types/checkout.ts b/framework/commerce/types/checkout.ts new file mode 100644 index 000000000..9e3c7ecfa --- /dev/null +++ b/framework/commerce/types/checkout.ts @@ -0,0 +1,10 @@ +export type CheckoutSchema = { + endpoint: { + options: {} + handlers: { + checkout: { + data: null + } + } + } +} diff --git a/framework/commerce/types/product.ts b/framework/commerce/types/product.ts index d33cf3743..f69827f89 100644 --- a/framework/commerce/types/product.ts +++ b/framework/commerce/types/product.ts @@ -65,3 +65,24 @@ export type ProductsSchema = { } } } + +export type GetAllProductPathsOperation< + T extends ProductTypes = ProductTypes +> = { + data: { products: Pick[] } + variables: { first?: number } +} + +export type GetAllProductsOperation = { + data: { products: T['product'][] } + variables: { + relevance?: 'featured' | 'best_selling' | 'newest' + ids?: string[] + first?: number + } +} + +export type GetProductOperation = { + data: { product?: T['product'] } + variables: { path: string; slug?: never } | { path?: never; slug: string } +} diff --git a/framework/commerce/types/wishlist.ts b/framework/commerce/types/wishlist.ts index 70f900362..24c0a7c28 100644 --- a/framework/commerce/types/wishlist.ts +++ b/framework/commerce/types/wishlist.ts @@ -30,3 +30,10 @@ export type WishlistSchema = { } } } + +export type GetCustomerWishlistOperation< + T extends WishlistTypes = WishlistTypes +> = { + data: { wishlist?: T['wishlist'] } + variables: { customerId: string } +} diff --git a/framework/shopify/product/get-all-product-paths.ts b/framework/shopify/product/get-all-product-paths.ts index f95b533d4..e8ee04065 100644 --- a/framework/shopify/product/get-all-product-paths.ts +++ b/framework/shopify/product/get-all-product-paths.ts @@ -1,5 +1,5 @@ import { getConfig, ShopifyConfig } from '../api' -import fetchAllProducts from '../api/utils/fetch-all-products' +import { ProductEdge } from '../schema' import getAllProductsPathsQuery from '../utils/queries/get-all-products-paths-query' type ProductPath = { @@ -19,21 +19,22 @@ const getAllProductPaths = async (options?: { config?: ShopifyConfig preview?: boolean }): Promise => { - let { config, variables = { first: 250 } } = options ?? {} + let { config, variables = { first: 100, sortKey: 'BEST_SELLING' } } = + options ?? {} config = getConfig(config) - const products = await fetchAllProducts({ - config, - query: getAllProductsPathsQuery, + const { data } = await config.fetch(getAllProductsPathsQuery, { variables, }) return { - products: products?.map(({ node: { handle } }) => ({ - node: { - path: `/${handle}`, - }, - })), + products: data.products?.edges?.map( + ({ node: { handle } }: ProductEdge) => ({ + node: { + path: `/${handle}`, + }, + }) + ), } } diff --git a/framework/shopify/product/get-all-products.ts b/framework/shopify/product/get-all-products.ts index 3d74448dc..d7e811dbc 100644 --- a/framework/shopify/product/get-all-products.ts +++ b/framework/shopify/product/get-all-products.ts @@ -2,7 +2,7 @@ import { getConfig, ShopifyConfig } from '../api' import { GetAllProductsQuery, Product as ShopifyProduct } from '../schema' import { getAllProductsQuery } from '../utils/queries' import { normalizeProduct } from '../utils/normalize' -import { Product } from '@commerce/types' +import { Product } from '../types/product' type Variables = { first?: number diff --git a/pages/[...pages].tsx b/pages/[...pages].tsx index 2fd13d674..3e6ef65c9 100644 --- a/pages/[...pages].tsx +++ b/pages/[...pages].tsx @@ -8,7 +8,6 @@ import { Text } from '@components/ui' import { Layout } from '@components/common' import getSlug from '@lib/get-slug' import { missingLocaleInPages } from '@lib/usage-warns' -import { getConfig } from '@framework/api' import { defaultPageProps } from '@lib/defaults' export async function getStaticProps({ @@ -16,7 +15,7 @@ export async function getStaticProps({ params, locale, }: GetStaticPropsContext<{ pages: string[] }>) { - const config = getConfig({ locale }) + const config = { locale } const { pages } = await commerce.getAllPages({ preview, config }) const path = params?.pages.join('/') const slug = locale ? `${locale}/${path}` : path diff --git a/pages/api/bigcommerce/checkout.ts b/pages/api/bigcommerce/checkout.ts deleted file mode 100644 index bd754deab..000000000 --- a/pages/api/bigcommerce/checkout.ts +++ /dev/null @@ -1,3 +0,0 @@ -import checkoutApi from '@framework/api/checkout' - -export default checkoutApi() diff --git a/pages/api/catalog/products.ts b/pages/api/catalog/products.ts index 5e5a4707d..631bfd516 100644 --- a/pages/api/catalog/products.ts +++ b/pages/api/catalog/products.ts @@ -1,11 +1,4 @@ -import products from '@commerce/api/endpoints/catalog/products' -import { - ProductsAPI, - handlers, -} from '@framework/api/endpoints/catalog/products' +import productsApi from '@framework/api/endpoints/catalog/products' import commerce from '@lib/api/commerce' -export default commerce.endpoint({ - handler: products as ProductsAPI['endpoint']['handler'], - handlers, -}) +export default productsApi(commerce) diff --git a/pages/api/checkout.ts b/pages/api/checkout.ts new file mode 100644 index 000000000..7bf0fd9aa --- /dev/null +++ b/pages/api/checkout.ts @@ -0,0 +1,4 @@ +import checkoutApi from '@framework/api/endpoints/checkout' +import commerce from '@lib/api/commerce' + +export default checkoutApi(commerce) diff --git a/pages/blog.tsx b/pages/blog.tsx index 10dba8c56..c1bdc4624 100644 --- a/pages/blog.tsx +++ b/pages/blog.tsx @@ -1,5 +1,4 @@ import type { GetStaticPropsContext } from 'next' -import { getConfig } from '@framework/api' import commerce from '@lib/api/commerce' import { Layout } from '@components/common' import { Container } from '@components/ui' @@ -8,7 +7,7 @@ export async function getStaticProps({ preview, locale, }: GetStaticPropsContext) { - const config = getConfig({ locale }) + const config = { locale } const { pages } = await commerce.getAllPages({ config, preview }) return { props: { pages }, diff --git a/pages/cart.tsx b/pages/cart.tsx index e318ee6e9..d14a7bfd2 100644 --- a/pages/cart.tsx +++ b/pages/cart.tsx @@ -1,5 +1,4 @@ import type { GetStaticPropsContext } from 'next' -import { getConfig } from '@framework/api' import useCart from '@framework/cart/use-cart' import usePrice from '@framework/product/use-price' import commerce from '@lib/api/commerce' @@ -12,7 +11,7 @@ export async function getStaticProps({ preview, locale, }: GetStaticPropsContext) { - const config = getConfig({ locale }) + const config = { locale } const { pages } = await commerce.getAllPages({ config, preview }) return { props: { pages }, diff --git a/pages/index.tsx b/pages/index.tsx index b1a8b2105..e8c342449 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -5,16 +5,12 @@ import { ProductCard } from '@components/product' // import HomeAllProductsGrid from '@components/common/HomeAllProductsGrid' import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next' -import { getConfig } from '@framework/api' -import getAllProducts from '@framework/product/get-all-products' - export async function getStaticProps({ preview, locale, }: GetStaticPropsContext) { - const config = getConfig({ locale }) - - const { products } = await getAllProducts({ + const config = { locale } + const { products } = await commerce.getAllProducts({ variables: { first: 12 }, config, preview, diff --git a/pages/orders.tsx b/pages/orders.tsx index f1dd630ac..c43ff9e5a 100644 --- a/pages/orders.tsx +++ b/pages/orders.tsx @@ -3,13 +3,12 @@ import commerce from '@lib/api/commerce' import { Bag } from '@components/icons' import { Layout } from '@components/common' import { Container, Text } from '@components/ui' -import { getConfig } from '@framework/api' export async function getStaticProps({ preview, locale, }: GetStaticPropsContext) { - const config = getConfig({ locale }) + const config = { locale } const { pages } = await commerce.getAllPages({ config, preview }) return { props: { pages }, diff --git a/pages/product/[slug].tsx b/pages/product/[slug].tsx index f1a94032a..bf71ac21e 100644 --- a/pages/product/[slug].tsx +++ b/pages/product/[slug].tsx @@ -8,18 +8,14 @@ import commerce from '@lib/api/commerce' import { Layout } from '@components/common' import { ProductView } from '@components/product' -import { getConfig } from '@framework/api' -import getProduct from '@framework/product/get-product' -import getAllProductPaths from '@framework/product/get-all-product-paths' - export async function getStaticProps({ params, locale, preview, }: GetStaticPropsContext<{ slug: string }>) { - const config = getConfig({ locale }) + const config = { locale } const { pages } = await commerce.getAllPages({ config, preview }) - const { product } = await getProduct({ + const { product } = await commerce.getProduct({ variables: { slug: params!.slug }, config, preview, @@ -39,18 +35,18 @@ export async function getStaticProps({ } export async function getStaticPaths({ locales }: GetStaticPathsContext) { - const { products } = await getAllProductPaths() + const { products } = await commerce.getAllProductPaths() return { paths: locales ? locales.reduce((arr, locale) => { // Add a product path for every locale products.forEach((product) => { - arr.push(`/${locale}/product${product.node.path}`) + arr.push(`/${locale}/product${product.path}`) }) return arr }, []) - : products.map((product) => `/product${product.node.path}`), + : products.map((product) => `/product${product.path}`), fallback: 'blocking', } } diff --git a/pages/profile.tsx b/pages/profile.tsx index 0c9d7b26d..b73469fa5 100644 --- a/pages/profile.tsx +++ b/pages/profile.tsx @@ -1,5 +1,4 @@ import type { GetStaticPropsContext } from 'next' -import { getConfig } from '@framework/api' import useCustomer from '@framework/customer/use-customer' import commerce from '@lib/api/commerce' import { Layout } from '@components/common' @@ -9,7 +8,7 @@ export async function getStaticProps({ preview, locale, }: GetStaticPropsContext) { - const config = getConfig({ locale }) + const config = { locale } const { pages } = await commerce.getAllPages({ config, preview }) return { props: { pages }, diff --git a/pages/search.tsx b/pages/search.tsx index 46311a5dc..53452b74a 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -8,7 +8,6 @@ import { Layout } from '@components/common' import { ProductCard } from '@components/product' import { Container, Grid, Skeleton } from '@components/ui' -import { getConfig } from '@framework/api' import useSearch from '@framework/product/use-search' import commerce from '@lib/api/commerce' import rangeMap from '@lib/range-map' @@ -36,7 +35,7 @@ export async function getStaticProps({ preview, locale, }: GetStaticPropsContext) { - const config = getConfig({ locale }) + const config = { locale } const { pages } = await commerce.getAllPages({ config, preview }) const { categories, brands } = await commerce.getSiteInfo({ config, preview }) return { diff --git a/pages/wishlist.tsx b/pages/wishlist.tsx index ef49dfff3..9927c536a 100644 --- a/pages/wishlist.tsx +++ b/pages/wishlist.tsx @@ -4,7 +4,6 @@ import { Heart } from '@components/icons' import { Layout } from '@components/common' import { Text, Container } from '@components/ui' import { defaultPageProps } from '@lib/defaults' -import { getConfig } from '@framework/api' import { useCustomer } from '@framework/customer' import { WishlistCard } from '@components/wishlist' import useWishlist from '@framework/wishlist/use-wishlist' @@ -20,7 +19,7 @@ export async function getStaticProps({ } } - const config = getConfig({ locale }) + const config = { locale } const { pages } = await commerce.getAllPages({ config, preview }) return { props: {