WIP OrderCloud provider

This commit is contained in:
goncy 2021-08-06 16:23:33 -03:00
parent 0e7e7b7d5f
commit f765590484
50 changed files with 691 additions and 4 deletions

4
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
}

View File

@ -1,7 +1,7 @@
{
"features": {
"cart": true,
"search": true,
"cart": false,
"search": false,
"wishlist": false,
"customerAuth": false,
"customCheckout": false

View File

@ -14,6 +14,7 @@ const PROVIDERS = [
'swell',
'vendure',
'local',
'ordercloud',
]
function getProviderName() {

View File

@ -0,0 +1 @@
COMMERCE_PROVIDER=ordercloud

View File

@ -0,0 +1 @@
# Next.js Ordercloud Provider

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

@ -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,50 @@
import type { CommerceAPI, CommerceAPIConfig } from '@commerce/api'
import { getCommerceApi as commerceApi } from '@commerce/api'
import createFetcher from './utils/fetch-rest'
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 OrdercloudConfig extends Omit<CommerceAPIConfig, 'fetch'> {
fetch: <T>(
method: string,
resource: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => Promise<T>
}
const config: OrdercloudConfig = {
commerceUrl: 'https://sandboxapi.ordercloud.io/v1',
apiToken: '',
cartCookie: '',
customerCookie: '',
cartCookieMaxAge: 2592000,
fetch: createFetcher(() => getCommerceApi().getConfig()),
}
const operations = {
getAllPages,
getPage,
getSiteInfo,
getCustomerWishlist,
getAllProductPaths,
getAllProducts,
getProduct,
}
export const provider = { config, operations }
export type Provider = typeof provider
export type OrderCloudAPI<P extends Provider = Provider> = CommerceAPI<P | any>
export function getCommerceApi<P extends Provider>(
customProvider: P = provider as any
): OrderCloudAPI<P> {
return commerceApi(customProvider as any)
}

View File

@ -0,0 +1,19 @@
export type Page = { url: string }
export type GetAllPagesResult = { pages: Page[] }
import type { OrdercloudConfig } from '../index'
export default function getAllPagesOperation() {
function getAllPages({
config,
preview,
}: {
url?: string
config?: Partial<OrdercloudConfig>
preview?: boolean
}): Promise<GetAllPagesResult> {
return Promise.resolve({
pages: [],
})
}
return getAllPages
}

View File

@ -0,0 +1,16 @@
import data from '../../data.json'
export type GetAllProductPathsResult = {
products: Array<{ path: string }>
}
export default function getAllProductPathsOperation() {
function getAllProductPaths(): Promise<GetAllProductPathsResult> {
return Promise.resolve({
products: []
// products: data.products.map(({ path }) => ({ path })),
})
}
return getAllProductPaths
}

View File

@ -0,0 +1,44 @@
import { Product } from '@commerce/types/product'
import { GetAllProductsOperation } from '@commerce/types/product'
import type { OperationContext } from '@commerce/api/operations'
import type { OrdercloudConfig, Provider } from '../index'
import {
PriceSchedule,
RawProduct,
RawProductWithPrice,
} from '@framework/types/product'
import { normalize as normalizeProduct } from '@framework/utils/product'
export default function getAllProductsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProducts<T extends GetAllProductsOperation>({
config,
}: {
query?: string
variables?: T['variables']
config?: Partial<OrdercloudConfig>
preview?: boolean
} = {}): Promise<{ products: Product[] }> {
const { fetch } = commerce.getConfig(config)
const rawProducts: RawProduct[] = await fetch<{ Items: RawProduct[] }>(
'GET',
'/products'
).then((response) => response.Items)
const rawProductsWithPrice: RawProductWithPrice[] = await Promise.all(
rawProducts.map(async (product) => ({
...product,
priceSchedule: await fetch<PriceSchedule>(
'GET',
`/priceschedules/${product.ID}`
),
}))
)
return {
products: rawProductsWithPrice.map(normalizeProduct),
}
}
return getAllProducts
}

View File

@ -0,0 +1,6 @@
export default function getCustomerWishlistOperation() {
function getCustomerWishlist(): any {
return { wishlist: {} }
}
return getCustomerWishlist
}

View File

@ -0,0 +1,13 @@
export type Page = any
export type GetPageResult = { page?: Page }
export type PageVariables = {
id: number
}
export default function getPageOperation() {
function getPage(): Promise<GetPageResult> {
return Promise.resolve({})
}
return getPage
}

View File

@ -0,0 +1,26 @@
import type { OrdercloudConfig } from '../index'
import { Product } from '@commerce/types/product'
import { GetProductOperation } from '@commerce/types/product'
import data from '../../data.json'
import type { OperationContext } from '@commerce/api/operations'
export default function getProductOperation({
commerce,
}: OperationContext<any>) {
async function getProduct<T extends GetProductOperation>({
query = '',
variables,
config,
}: {
query?: string
variables?: T['variables']
config?: Partial<OrdercloudConfig>
preview?: boolean
} = {}): Promise<Product | {} | any> {
return {
product: null // data.products.find(({ slug }) => slug === variables!.slug),
}
}
return getProduct
}

View File

@ -0,0 +1,43 @@
import { OperationContext } from '@commerce/api/operations'
import { Category } from '@commerce/types/site'
import { OrdercloudConfig } from '../index'
export type GetSiteInfoResult<
T extends { categories: any[]; brands: any[] } = {
categories: Category[]
brands: any[]
}
> = T
export default function getSiteInfoOperation({}: OperationContext<any>) {
function getSiteInfo({
query,
variables,
config: cfg,
}: {
query?: string
variables?: any
config?: Partial<OrdercloudConfig>
preview?: boolean
} = {}): Promise<GetSiteInfoResult> {
return Promise.resolve({
categories: [
{
id: 'new-arrivals',
name: 'New Arrivals',
slug: 'new-arrivals',
path: '/new-arrivals',
},
{
id: 'featured',
name: 'Featured',
slug: 'featured',
path: '/featured',
},
],
brands: [],
})
}
return getSiteInfo
}

View File

@ -0,0 +1,6 @@
export { default as getPage } from './get-page'
export { default as getSiteInfo } from './get-site-info'
export { default as getAllPages } from './get-all-pages'
export { default as getProduct } from './get-product'
export { default as getAllProducts } from './get-all-products'
export { default as getAllProductPaths } from './get-all-product-paths'

View File

@ -0,0 +1,43 @@
import { FetcherError } from '@commerce/utils/errors'
import type { OrdercloudConfig } from '../index'
import fetch from './fetch'
const fetchRestApi: (
getConfig: () => OrdercloudConfig
) => <T>(
method: string,
resource: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => Promise<T> =
(getConfig) =>
async <T>(
method: string,
resource: string,
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => {
const { commerceUrl } = getConfig()
const res = await fetch(`${commerceUrl}${resource}`, {
...fetchOptions,
method,
headers: {
...fetchOptions?.headers,
accept: 'application/json, text/plain, */*',
'accept-language': 'es,en;q=0.9,es-ES;q=0.8,fr;q=0.7',
authorization: 'Bearer <your token>',
},
body: body ? JSON.stringify(body) : undefined,
})
if (!res.ok) {
throw new FetcherError({
errors: [{ message: res.statusText }],
status: res.status,
})
}
return (await res.json()) as T
}
export default fetchRestApi

View File

@ -0,0 +1,3 @@
import zeitFetch from '@vercel/fetch'
export default zeitFetch()

View File

@ -0,0 +1,3 @@
export { default as useLogin } from './use-login'
export { default as useLogout } from './use-logout'
export { default as useSignup } from './use-signup'

View File

@ -0,0 +1,16 @@
import { MutationHook } from '@commerce/utils/types'
import useLogin, { UseLogin } from '@commerce/auth/use-login'
export default useLogin as UseLogin<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher() {
return null
},
useHook: () => () => {
return async function () {}
},
}

View File

@ -0,0 +1,17 @@
import { MutationHook } from '@commerce/utils/types'
import useLogout, { UseLogout } from '@commerce/auth/use-logout'
export default useLogout as UseLogout<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher() {
return null
},
useHook:
({ fetch }) =>
() =>
async () => {},
}

View File

@ -0,0 +1,19 @@
import { useCallback } from 'react'
import useCustomer from '../customer/use-customer'
import { MutationHook } from '@commerce/utils/types'
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
export default useSignup as UseSignup<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher() {
return null
},
useHook:
({ fetch }) =>
() =>
() => {},
}

View File

@ -0,0 +1,4 @@
export { default as useCart } from './use-cart'
export { default as useAddItem } from './use-add-item'
export { default as useRemoveItem } from './use-remove-item'
export { default as useUpdateItem } from './use-update-item'

View File

@ -0,0 +1,17 @@
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
import { MutationHook } from '@commerce/utils/types'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ fetch }) =>
() => {
return async function addItem() {
return {}
}
},
}

View File

@ -0,0 +1,42 @@
import { useMemo } from 'react'
import { SWRHook } from '@commerce/utils/types'
import useCart, { UseCart } from '@commerce/cart/use-cart'
export default useCart as UseCart<typeof handler>
export const handler: SWRHook<any> = {
fetchOptions: {
query: '',
},
async fetcher() {
return {
id: '',
createdAt: '',
currency: { code: '' },
taxesIncluded: '',
lineItems: [],
lineItemsSubtotalPrice: '',
subtotalPrice: 0,
totalPrice: 0,
}
},
useHook:
({ useData }) =>
(input) => {
return useMemo(
() =>
Object.create(
{},
{
isEmpty: {
get() {
return true
},
enumerable: true,
},
}
),
[]
)
},
}

View File

@ -0,0 +1,18 @@
import { MutationHook } from '@commerce/utils/types'
import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item'
export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ fetch }) =>
() => {
return async function removeItem(input) {
return {}
}
},
}

View File

@ -0,0 +1,18 @@
import { MutationHook } from '@commerce/utils/types'
import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item'
export default useUpdateItem as UseUpdateItem<any>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ fetch }) =>
() => {
return async function addItem() {
return {}
}
},
}

View File

@ -0,0 +1,9 @@
{
"provider": "ordercloud",
"features": {
"wishlist": false,
"cart": false,
"search": false,
"customerAuth": false
}
}

View File

@ -0,0 +1 @@
export { default as useCustomer } from './use-customer'

View File

@ -0,0 +1,15 @@
import { SWRHook } from '@commerce/utils/types'
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
export default useCustomer as UseCustomer<typeof handler>
export const handler: SWRHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook: () => () => {
return async function addItem() {
return {}
}
},
}

View File

@ -0,0 +1,7 @@
import { Fetcher } from '@commerce/utils/types'
export const fetcher: Fetcher = async () => {
throw new Error(
'Client side fetching has not been implemented yet, try to fetch from server side.'
)
}

View File

@ -0,0 +1,32 @@
import * as React from 'react'
import { ReactNode } from 'react'
import { ordercloudProvider } from './provider'
import {
CommerceConfig,
CommerceProvider as CoreCommerceProvider,
useCommerce as useCoreCommerce,
} from '@commerce'
export const ordercloudConfig: CommerceConfig = {
locale: 'en-us',
cartCookie: 'session',
}
export function CommerceProvider({
children,
...config
}: {
children?: ReactNode
locale: string
} & Partial<CommerceConfig>) {
return (
<CoreCommerceProvider
provider={ordercloudProvider}
config={{ ...ordercloudConfig, ...config }}
>
{children}
</CoreCommerceProvider>
)
}
export const useCommerce = () => useCoreCommerce()

View File

@ -0,0 +1,8 @@
const commerce = require('./commerce.config.json')
module.exports = {
commerce,
images: {
domains: ['localhost'],
},
}

View File

@ -0,0 +1,2 @@
export { default as usePrice } from './use-price'
export { default as useSearch } from './use-search'

View File

@ -0,0 +1,2 @@
export * from '@commerce/product/use-price'
export { default } from '@commerce/product/use-price'

View File

@ -0,0 +1,17 @@
import { SWRHook } from '@commerce/utils/types'
import useSearch, { UseSearch } from '@commerce/product/use-search'
export default useSearch as UseSearch<typeof handler>
export const handler: SWRHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook: () => () => {
return {
data: {
products: [],
},
}
},
}

View File

@ -0,0 +1,21 @@
import { fetcher } from './fetcher'
import { handler as useCart } from './cart/use-cart'
import { handler as useAddItem } from './cart/use-add-item'
import { handler as useUpdateItem } from './cart/use-update-item'
import { handler as useRemoveItem } from './cart/use-remove-item'
import { handler as useCustomer } from './customer/use-customer'
import { handler as useSearch } from './product/use-search'
import { handler as useLogin } from './auth/use-login'
import { handler as useLogout } from './auth/use-logout'
import { handler as useSignup } from './auth/use-signup'
export type Provider = typeof ordercloudProvider
export const ordercloudProvider = {
locale: 'en-us',
cartCookie: 'session',
fetcher: fetcher,
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
customer: { useCustomer },
products: { useSearch },
auth: { useLogin, useLogout, useSignup },
}

View File

@ -0,0 +1,44 @@
export interface RawProduct {
OwnerID: string
DefaultPriceScheduleID: string | null
AutoForward: boolean
ID: string
Name: string
Description: string
QuantityMultiplier: number
ShipWeight: null
ShipHeight: null
ShipWidth: null
ShipLength: null
Active: boolean
SpecCount: number
VariantCount: number
ShipFromAddressID: null
Inventory: null
DefaultSupplierID: null
AllSuppliersCanSell: boolean
xp: null
}
export interface RawProductWithPrice extends RawProduct {
priceSchedule: PriceSchedule
}
export interface PriceSchedule {
OwnerID: string
ID: string
Name: string
ApplyTax: boolean
ApplyShipping: boolean
MinQuantity: number
MaxQuantity: number
UseCumulativeQuantity: boolean
RestrictedQuantity: boolean
PriceBreaks: [
{
Quantity: number
Price: number
}
]
xp: null
}

View File

@ -0,0 +1,17 @@
import type { RawProductWithPrice } from '@framework/types/product'
import type { Product } from '@commerce/types/product'
export function normalize(product: RawProductWithPrice): Product {
return {
id: product.ID,
name: product.Name,
description: product.Description,
images: [],
variants: [],
price: {
value: product.priceSchedule.PriceBreaks[0].Price,
currencyCode: 'USD',
},
options: [],
}
}

View File

@ -0,0 +1,13 @@
import { useCallback } from 'react'
export function emptyHook() {
const useEmptyHook = async (options = {}) => {
return useCallback(async function () {
return Promise.resolve()
}, [])
}
return useEmptyHook
}
export default emptyHook

View File

@ -0,0 +1,17 @@
import { useCallback } from 'react'
type Options = {
includeProducts?: boolean
}
export function emptyHook(options?: Options) {
const useEmptyHook = async ({ id }: { id: string | number }) => {
return useCallback(async function () {
return Promise.resolve()
}, [])
}
return useEmptyHook
}
export default emptyHook

View File

@ -0,0 +1,43 @@
import { HookFetcher } from '@commerce/utils/types'
import type { Product } from '@commerce/types/product'
const defaultOpts = {}
export type Wishlist = {
items: [
{
product_id: number
variant_id: number
id: number
product: Product
}
]
}
export interface UseWishlistOptions {
includeProducts?: boolean
}
export interface UseWishlistInput extends UseWishlistOptions {
customerId?: number
}
export const fetcher: HookFetcher<Wishlist | null, UseWishlistInput> = () => {
return null
}
export function extendHook(
customFetcher: typeof fetcher,
// swrOptions?: SwrOptions<Wishlist | null, UseWishlistInput>
swrOptions?: any
) {
const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => {
return { data: null }
}
useWishlist.extend = extendHook
return useWishlist
}
export default extendHook(fetcher)

View File

@ -23,8 +23,8 @@
"@components/*": ["components/*"],
"@commerce": ["framework/commerce"],
"@commerce/*": ["framework/commerce/*"],
"@framework": ["framework/local"],
"@framework/*": ["framework/local/*"]
"@framework": ["framework/ordercloud"],
"@framework/*": ["framework/ordercloud/*"]
}
},
"include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],