Update Vendure provider to latest API changes (#352)

Relates to #349
This commit is contained in:
Michael Bromley
2021-06-02 16:46:38 +02:00
committed by GitHub
parent a98c95d447
commit 0e804d09f9
81 changed files with 1265 additions and 698 deletions

View File

@@ -1 +0,0 @@
export default function () {}

View File

@@ -1 +0,0 @@
export default function () {}

View File

@@ -1 +0,0 @@
export default function () {}

View File

@@ -1 +0,0 @@
export default function () {}

View File

@@ -1 +0,0 @@
export default function () {}

View File

@@ -1 +0,0 @@
export default function () {}

View File

@@ -1 +0,0 @@
export default function () {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -1,6 +1,13 @@
import { NextApiHandler } from 'next'
import { CommerceAPI, createEndpoint, GetAPISchema } from '@commerce/api'
import { CheckoutSchema } from '@commerce/types/checkout'
import checkoutEndpoint from '@commerce/api/endpoints/checkout'
const checkoutApi = async (req: any, res: any, config: any) => {
const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({
req,
res,
config,
}) => {
try {
const html = `
<!DOCTYPE html>
@@ -37,27 +44,15 @@ const checkoutApi = async (req: any, res: any, config: any) => {
}
}
export function createApiHandler<T = any, H = {}, Options extends {} = {}>(
handler: any,
handlers: H,
defaultOptions: Options
) {
return function getApiHandler({
config,
operations,
options,
}: {
config?: any
operations?: Partial<H>
options?: Options extends {} ? Partial<Options> : never
} = {}): NextApiHandler {
const ops = { ...operations, ...handlers }
const opts = { ...defaultOptions, ...options }
export type CheckoutAPI = GetAPISchema<CommerceAPI, CheckoutSchema>
return function apiHandler(req, res) {
return handler(req, res, config, ops, opts)
}
}
}
export type CheckoutEndpoint = CheckoutAPI['endpoint']
export default createApiHandler(checkoutApi, {}, {})
export const handlers: CheckoutEndpoint['handlers'] = { checkout }
const checkoutApi = createEndpoint<CheckoutAPI>({
handler: checkoutEndpoint,
handlers,
})
export default checkoutApi

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -1,6 +1,16 @@
import type { CommerceAPIConfig } from '@commerce/api'
import type { APIProvider, CommerceAPIConfig } from '@commerce/api'
import { CommerceAPI, getCommerceApi as commerceApi } from '@commerce/api'
import fetchGraphqlApi from './utils/fetch-graphql-api'
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 VendureConfig extends CommerceAPIConfig {}
const API_URL = process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL
@@ -11,41 +21,33 @@ if (!API_URL) {
)
}
export class Config {
private config: VendureConfig
constructor(config: VendureConfig) {
this.config = {
...config,
}
}
getConfig(userConfig: Partial<VendureConfig> = {}) {
return Object.entries(userConfig).reduce<VendureConfig>(
(cfg, [key, value]) => Object.assign(cfg, { [key]: value }),
{ ...this.config }
)
}
setConfig(newConfig: Partial<VendureConfig>) {
Object.assign(this.config, newConfig)
}
}
const ONE_DAY = 60 * 60 * 24
const config = new Config({
const config: VendureConfig = {
commerceUrl: API_URL,
apiToken: '',
cartCookie: '',
customerCookie: '',
cartCookieMaxAge: ONE_DAY * 30,
fetch: fetchGraphqlApi,
})
export function getConfig(userConfig?: Partial<VendureConfig>) {
return config.getConfig(userConfig)
}
export function setConfig(newConfig: Partial<VendureConfig>) {
return config.setConfig(newConfig)
const operations = {
login,
getAllPages,
getPage,
getSiteInfo,
getCustomerWishlist,
getAllProductPaths,
getAllProducts,
getProduct,
}
export const provider = { config, operations }
export type Provider = typeof provider
export function getCommerceApi<P extends Provider>(
customProvider: P = provider as any
): CommerceAPI<P> {
return commerceApi(customProvider)
}

View File

@@ -0,0 +1,41 @@
import { VendureConfig } from '../'
import { OperationContext } from '@commerce/api/operations'
import { Provider } from '../../../bigcommerce/api'
export type Page = any
export type GetAllPagesResult<
T extends { pages: any[] } = { pages: Page[] }
> = T
export default function getAllPagesOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllPages(opts?: {
config?: Partial<VendureConfig>
preview?: boolean
}): Promise<GetAllPagesResult>
async function getAllPages<T extends { pages: any[] }>(opts: {
url: string
config?: Partial<VendureConfig>
preview?: boolean
}): Promise<GetAllPagesResult<T>>
async function getAllPages({
config: cfg,
preview,
}: {
url?: string
config?: Partial<VendureConfig>
preview?: boolean
} = {}): Promise<GetAllPagesResult> {
const config = commerce.getConfig(cfg)
return {
pages: [],
}
}
return getAllPages
}

View File

@@ -0,0 +1,52 @@
import { OperationContext, OperationOptions } from '@commerce/api/operations'
import type { GetAllProductPathsQuery } from '../../schema'
import { Provider } from '../index'
import { getAllProductPathsQuery } from '../../utils/queries/get-all-product-paths-query'
import { GetAllProductPathsOperation } from '@commerce/types/product'
import { BigcommerceConfig } from '../../../bigcommerce/api'
export type GetAllProductPathsResult = {
products: Array<{ node: { path: string } }>
}
export default function getAllProductPathsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProductPaths<
T extends GetAllProductPathsOperation
>(opts?: {
variables?: T['variables']
config?: BigcommerceConfig
}): Promise<T['data']>
async function getAllProductPaths<T extends GetAllProductPathsOperation>(
opts: {
variables?: T['variables']
config?: BigcommerceConfig
} & OperationOptions
): Promise<T['data']>
async function getAllProductPaths<T extends GetAllProductPathsOperation>({
query = getAllProductPathsQuery,
variables,
config: cfg,
}: {
query?: string
variables?: T['variables']
config?: BigcommerceConfig
} = {}): Promise<T['data']> {
const config = commerce.getConfig(cfg)
// 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<GetAllProductPathsQuery>(query, {
variables,
})
const products = data.products.items
return {
products: products.map((p) => ({ path: `/${p.slug}` })),
}
}
return getAllProductPaths
}

View File

@@ -0,0 +1,46 @@
import { Product } from '@commerce/types/product'
import { Provider, VendureConfig } from '../'
import { GetAllProductsQuery } from '../../schema'
import { normalizeSearchResult } from '../../utils/normalize'
import { getAllProductsQuery } from '../../utils/queries/get-all-products-query'
import { OperationContext } from '@commerce/api/operations'
export type ProductVariables = { first?: number }
export default function getAllProductsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProducts(opts?: {
variables?: ProductVariables
config?: Partial<VendureConfig>
preview?: boolean
}): Promise<{ products: Product[] }>
async function getAllProducts({
query = getAllProductsQuery,
variables: { ...vars } = {},
config: cfg,
}: {
query?: string
variables?: ProductVariables
config?: Partial<VendureConfig>
preview?: boolean
} = {}): Promise<{ products: Product[] | any[] }> {
const config = commerce.getConfig(cfg)
const variables = {
input: {
take: vars.first,
groupByProduct: true,
},
}
const { data } = await config.fetch<GetAllProductsQuery>(query, {
variables,
})
return {
products: data.search.items.map((item) => normalizeSearchResult(item)),
}
}
return getAllProducts
}

View File

@@ -0,0 +1,23 @@
import { OperationContext } from '@commerce/api/operations'
import { Provider, VendureConfig } from '../'
export default function getCustomerWishlistOperation({
commerce,
}: OperationContext<Provider>) {
async function getCustomerWishlist({
config: cfg,
variables,
includeProducts,
}: {
url?: string
variables: any
config?: Partial<VendureConfig>
includeProducts?: boolean
}): Promise<any> {
// Not implemented as Vendure does not ship with wishlist functionality at present
const config = commerce.getConfig(cfg)
return { wishlist: {} }
}
return getCustomerWishlist
}

View File

@@ -0,0 +1,45 @@
import { VendureConfig, Provider } from '../'
import { OperationContext } from '@commerce/api/operations'
export type Page = any
export type GetPageResult<T extends { page?: any } = { page?: Page }> = T
export type PageVariables = {
id: number
}
export default function getPageOperation({
commerce,
}: OperationContext<Provider>) {
async function getPage(opts: {
url?: string
variables: PageVariables
config?: Partial<VendureConfig>
preview?: boolean
}): Promise<GetPageResult>
async function getPage<T extends { page?: any }, V = any>(opts: {
url: string
variables: V
config?: Partial<VendureConfig>
preview?: boolean
}): Promise<GetPageResult<T>>
async function getPage({
url,
variables,
config: cfg,
preview,
}: {
url?: string
variables: PageVariables
config?: Partial<VendureConfig>
preview?: boolean
}): Promise<GetPageResult> {
const config = commerce.getConfig(cfg)
return {}
}
return getPage
}

View File

@@ -0,0 +1,69 @@
import { Product } from '@commerce/types/product'
import { OperationContext } from '@commerce/api/operations'
import { Provider, VendureConfig } from '../'
import { GetProductQuery } from '../../schema'
import { getProductQuery } from '../../utils/queries/get-product-query'
export default function getProductOperation({
commerce,
}: OperationContext<Provider>) {
async function getProduct({
query = getProductQuery,
variables,
config: cfg,
}: {
query?: string
variables: { slug: string }
config?: Partial<VendureConfig>
preview?: boolean
}): Promise<Product | {} | any> {
const config = commerce.getConfig(cfg)
const locale = config.locale
const { data } = await config.fetch<GetProductQuery>(query, { variables })
const product = data.product
if (product) {
const getOptionGroupName = (id: string): string => {
return product.optionGroups.find((og) => og.id === id)!.name
}
return {
product: {
id: product.id,
name: product.name,
description: product.description,
slug: product.slug,
images: product.assets.map((a) => ({
url: a.preview,
alt: a.name,
})),
variants: product.variants.map((v) => ({
id: v.id,
options: v.options.map((o) => ({
// This __typename property is required in order for the correct
// variant selection to work, see `components/product/helpers.ts`
// `getVariant()` function.
__typename: 'MultipleChoiceOption',
id: o.id,
displayName: getOptionGroupName(o.groupId),
values: [{ label: o.name }],
})),
})),
price: {
value: product.variants[0].priceWithTax / 100,
currencyCode: product.variants[0].currencyCode,
},
options: product.optionGroups.map((og) => ({
id: og.id,
displayName: og.name,
values: og.options.map((o) => ({ label: o.name })),
})),
} as Product,
}
}
return {}
}
return getProduct
}

View File

@@ -0,0 +1,50 @@
import { Provider, VendureConfig } from '../'
import { GetCollectionsQuery } from '../../schema'
import { arrayToTree } from '../../utils/array-to-tree'
import { getCollectionsQuery } from '../../utils/queries/get-collections-query'
import { OperationContext } from '@commerce/api/operations'
import { Category } from '@commerce/types/site'
export type GetSiteInfoResult<
T extends { categories: any[]; brands: any[] } = {
categories: Category[]
brands: any[]
}
> = T
export default function getSiteInfoOperation({
commerce,
}: OperationContext<Provider>) {
async function getSiteInfo({
query = getCollectionsQuery,
variables,
config: cfg,
}: {
query?: string
variables?: any
config?: Partial<VendureConfig>
preview?: boolean
} = {}): Promise<GetSiteInfoResult> {
const config = commerce.getConfig(cfg)
// 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<GetCollectionsQuery>(query, {
variables,
})
const collections = data.collections?.items.map((i) => ({
...i,
entityId: i.id,
path: i.slug,
productCount: i.productVariants.totalItems,
}))
const categories = arrayToTree(collections).children
const brands = [] as any[]
return {
categories: categories ?? [],
brands,
}
}
return getSiteInfo
}

View File

@@ -0,0 +1,60 @@
import type { ServerResponse } from 'http'
import type {
OperationContext,
OperationOptions,
} from '@commerce/api/operations'
import { ValidationError } from '@commerce/utils/errors'
import type { LoginOperation } from '../../types/login'
import type { LoginMutation } from '../../schema'
import { Provider, VendureConfig } from '..'
import { loginMutation } from '../../utils/mutations/log-in-mutation'
export default function loginOperation({
commerce,
}: OperationContext<Provider>) {
async function login<T extends LoginOperation>(opts: {
variables: T['variables']
config?: Partial<VendureConfig>
res: ServerResponse
}): Promise<T['data']>
async function login<T extends LoginOperation>(
opts: {
variables: T['variables']
config?: Partial<VendureConfig>
res: ServerResponse
} & OperationOptions
): Promise<T['data']>
async function login<T extends LoginOperation>({
query = loginMutation,
variables,
res: response,
config: cfg,
}: {
query?: string
variables: T['variables']
res: ServerResponse
config?: Partial<VendureConfig>
}): Promise<T['data']> {
const config = commerce.getConfig(cfg)
const { data, res } = await config.fetch<LoginMutation>(query, {
variables,
})
switch (data.login.__typename) {
case 'NativeAuthStrategyError':
case 'InvalidCredentialsError':
case 'NotVerifiedError':
throw new ValidationError({
code: data.login.errorCode,
message: data.login.message,
})
}
return {
result: data.login.id,
}
}
return login
}

View File

@@ -1,6 +1,6 @@
import { FetcherError } from '@commerce/utils/errors'
import type { GraphQLFetcher } from '@commerce/api'
import { getConfig } from '..'
import { getCommerceApi } from '../'
import fetch from './fetch'
const fetchGraphqlApi: GraphQLFetcher = async (
@@ -8,12 +8,11 @@ const fetchGraphqlApi: GraphQLFetcher = async (
{ variables, preview } = {},
fetchOptions
) => {
const config = getConfig()
const config = getCommerceApi().getConfig()
const res = await fetch(config.commerceUrl, {
...fetchOptions,
method: 'POST',
headers: {
Authorization: `Bearer ${config.apiToken}`,
...fetchOptions?.headers,
'Content-Type': 'application/json',
},

View File

@@ -1,2 +0,0 @@
export type WishlistItem = { product: any; id: number }
export default function () {}