Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend Http base class with config interface and add optional chaining to the Client #344

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"progress-bar-webpack-plugin": "^2.1.0",
"source-map-loader": "^2.0.2",
"ts-loader": "^8.3.0",
"typescript": "^4.5.2",
"typescript": "4.7.4",
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1",
"webpack-merge": "^5.8.0",
Expand Down
110 changes: 44 additions & 66 deletions src/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@ import {
Vendors,
Wishlists
} from './endpoints'
import { AllowedClientBuilderOptions, DefaultBuilderOptions } from './interfaces/ClientBuilderOptions'
import type { CreateFetcherConfig, Fetcher, IClientConfig } from './interfaces/ClientConfig'
import { Currency } from './interfaces/Currency'
import { Locale } from './interfaces/Locale'
import { SetProperty } from './interfaces/SetProperty'
import { BearerToken, OrderToken } from './interfaces/Token'

class Client {
public account: Account
class Client<ClientOptions extends AllowedClientBuilderOptions = DefaultBuilderOptions> {
public account: Account<ClientOptions>
public authentication: Authentication
public cart: Cart
public checkout: Checkout
Expand All @@ -32,6 +37,7 @@ class Client {

protected host: string
protected fetcher: Fetcher
protected config: IClientConfig

constructor(customOptions: IClientConfig) {
const spreeHostEnvironmentValue: string | null = (globalThis.process && globalThis.process.env.SPREE_HOST) || null
Expand All @@ -40,84 +46,56 @@ class Client {
host: spreeHostEnvironmentValue || 'http://localhost:3000/'
}

const options: IClientConfig = {
...defaultOptions,
...customOptions
}

const fetcherOptions: CreateFetcherConfig = { host: options.host }

this.fetcher = options.createFetcher(fetcherOptions)

this.addEndpoints()
}
this.config = { ...defaultOptions, ...customOptions }
this.host = this.config.host

protected addEndpoints(): void {
this.account = this.makeAccount()
this.authentication = this.makeAuthentication()
this.cart = this.makeCart()
this.checkout = this.makeCheckout()
this.countries = this.makeCountries()
this.digitalAssets = this.makeDigitalAssets()
this.menus = this.makeMenus()
this.order = this.makeOrder()
this.pages = this.makePages()
this.products = this.makeProducts()
this.taxons = this.makeTaxons()
this.vendors = this.makeVendors()
this.wishlists = this.makeWishlists()
}
const fetcherOptions: CreateFetcherConfig = { host: this.config.host }

protected makeAccount(): Account {
return new Account({ fetcher: this.fetcher })
}
this.fetcher = this.config.createFetcher(fetcherOptions)

protected makeAuthentication(): Authentication {
return new Authentication({ fetcher: this.fetcher })
}

protected makeCart(): Cart {
return new Cart({ fetcher: this.fetcher })
}

protected makeCheckout(): Checkout {
return new Checkout({ fetcher: this.fetcher })
}

protected makeCountries(): Countries {
return new Countries({ fetcher: this.fetcher })
}

protected makeOrder(): Order {
return new Order({ fetcher: this.fetcher })
}

protected makePages(): Pages {
return new Pages({ fetcher: this.fetcher })
}
const endpointOptions = {
fetcher: this.fetcher,
bearer_token: this.config.bearer_token,
order_token: this.config.order_token,
locale: this.config.locale,
currency: this.config.currency
}

protected makeProducts(): Products {
return new Products({ fetcher: this.fetcher })
this.account = new Account(endpointOptions)
this.authentication = new Authentication(endpointOptions)
this.cart = new Cart(endpointOptions)
this.checkout = new Checkout(endpointOptions)
this.countries = new Countries(endpointOptions)
this.digitalAssets = new DigitalAssets(endpointOptions)
this.menus = new Menus(endpointOptions)
this.order = new Order(endpointOptions)
this.pages = new Pages(endpointOptions)
this.products = new Products(endpointOptions)
this.taxons = new Taxons(endpointOptions)
this.vendors = new Vendors(endpointOptions)
this.wishlists = new Wishlists(endpointOptions)
}

protected makeTaxons(): Taxons {
return new Taxons({ fetcher: this.fetcher })
public withOrderToken(order_token: OrderToken) {
return this.builderInstance<SetProperty<ClientOptions, 'order_token', true>>({ order_token })
}

protected makeDigitalAssets(): DigitalAssets {
return new DigitalAssets({ fetcher: this.fetcher })
public withBearerToken(bearer_token: BearerToken) {
return this.builderInstance<SetProperty<ClientOptions, 'bearer_token', true>>({ bearer_token })
}

protected makeMenus(): Menus {
return new Menus({ fetcher: this.fetcher })
public withLocale(locale: Locale) {
return this.builderInstance<SetProperty<ClientOptions, 'locale', true>>({ locale })
}

protected makeVendors(): Vendors {
return new Vendors({ fetcher: this.fetcher })
public withCurrency(currency: Currency) {
return this.builderInstance<SetProperty<ClientOptions, 'currency', true>>({ currency })
}

protected makeWishlists(): Wishlists {
return new Wishlists({ fetcher: this.fetcher })
protected builderInstance<T extends AllowedClientBuilderOptions = ClientOptions>(
config: Partial<IClientConfig> = {}
): Client<T> {
return new Client<T>({ ...this.config, ...config })
}
}

Expand Down
45 changes: 36 additions & 9 deletions src/Http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,46 @@ import {
SpreeSDKError
} from './errors'
import FetchError from './errors/FetchError'
import { isFetchError } from './helpers/typeguards/isFetchError'
import * as result from './helpers/result'
import { ClientBuilderOptions } from './interfaces/ClientBuilderOptions'
import type { Fetcher } from './interfaces/ClientConfig'
import type { ErrorType } from './interfaces/errors/ErrorType'
import type { FetchConfig, HttpMethod, ResponseParsing } from './interfaces/FetchConfig'
import type { JsonApiResponse } from './interfaces/JsonApi'
import type { ResultResponse } from './interfaces/ResultResponse'
import type { IToken } from './interfaces/Token'
import { SpreeOrderHeaders } from './interfaces/SpreeOrderHeaders'
import type { BearerToken, IToken } from './interfaces/Token'

export type EndpointOptions = {
export type EndpointOptions = ClientBuilderOptions & {
fetcher: Fetcher
}

export default class Http {
public fetcher: Fetcher
public bearerToken: BearerToken | undefined
public orderToken: BearerToken | undefined
public locale: EndpointOptions['locale'] | undefined
public currency: EndpointOptions['currency'] | undefined

constructor({ fetcher }: EndpointOptions) {
constructor({ fetcher, bearer_token, order_token, locale, currency }: EndpointOptions) {
this.fetcher = fetcher
this.bearerToken = bearer_token
this.orderToken = order_token
this.locale = locale
this.currency = currency
}

protected async spreeResponse<ResponseType = JsonApiResponse>(
method: HttpMethod,
url: string,
tokens: IToken = {},
params: any = {},
userTokens: IToken = {},
userParams: any = {},
responseParsing: ResponseParsing = 'automatic'
): Promise<ResultResponse<ResponseType>> {
try {
const headers = this.spreeOrderHeaders(tokens)
const headers = this.spreeOrderHeaders(userTokens)
const params = this.spreeParams(userParams)

const fetchOptions: FetchConfig = {
url,
Expand Down Expand Up @@ -69,7 +81,7 @@ export default class Http {
}

protected processError(error: Error): SpreeSDKError {
if (error instanceof FetchError) {
if (isFetchError(error)) {
if (error.response) {
// Error from Spree outside HTTP 2xx codes
return this.processSpreeError(error)
Expand Down Expand Up @@ -100,8 +112,13 @@ export default class Http {
}
}

protected spreeOrderHeaders(tokens: IToken): { [headerName: string]: string } {
const header = {}
protected spreeOrderHeaders(userTokens: IToken): SpreeOrderHeaders {
const header: SpreeOrderHeaders = {}
const tokens = {
orderToken: this.orderToken,
bearerToken: this.bearerToken,
...userTokens
}

if (tokens.orderToken) {
header['X-Spree-Order-Token'] = tokens.orderToken
Expand All @@ -113,4 +130,14 @@ export default class Http {

return header
}

protected spreeParams(userParams: any): Record<string, any> {
const params = {
locale: this.locale,
currency: this.currency,
...userParams
}

return params
}
}
12 changes: 9 additions & 3 deletions src/endpoints/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,23 @@ import type {
ShowAddressOptions,
CreateAddressOptions,
RemoveAddressOptions,
UpdateAddressOptions
UpdateAddressOptions,
AccountContractTestOptions
} from '../interfaces/Account'
import { AllowedClientBuilderOptions } from '../interfaces/ClientBuilderOptions'
import type { ICreditCard, ICreditCardResult, ICreditCards, ICreditCardsResult } from '../interfaces/CreditCard'
import type { NoContentResponse, NoContentResult } from '../interfaces/NoContent'
import type { IOrder, IOrderResult, IOrders, IOrdersResult } from '../interfaces/Order'
import type { IQuery } from '../interfaces/Query'
import type { IToken } from '../interfaces/Token'
import routes from '../routes'

export default class Account extends Http {
public async accountInfo(options: AccountInfoOptions): Promise<IAccountResult>
export default class Account<ClientOptions extends AllowedClientBuilderOptions> extends Http {
public async contractTest(options: AccountContractTestOptions<ClientOptions>): Promise<string> {
return options.locale ?? 'asd'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@letelete this seems like a leftover from local testing ;)

}

public async accountInfo(options: AccountInfoOptions<ClientOptions>): Promise<IAccountResult>
/**
* @deprecated Use the combined options signature instead.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/errors/FetchError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default class FetchError extends SpreeSDKError {
public data?: any

constructor(response?: RawFetchResponse, request?: unknown, data?: unknown, message?: string) {
super(message)
super(message || '')
Object.setPrototypeOf(this, FetchError.prototype)
this.name = 'FetchError'
this.response = response
Expand Down
2 changes: 1 addition & 1 deletion src/fetchers/createAxiosFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const createAxiosFetcher: CreateFetcher = (fetcherOptions) => {
payload = { params }
}

let responseType: string
let responseType: string | undefined

const isBrowser = RUNTIME_TYPE === 'browser'

Expand Down
5 changes: 3 additions & 2 deletions src/helpers/jsonApi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { JsonApiDocument, JsonApiResponse } from '../interfaces/JsonApi'
import type { RelationType } from '../interfaces/Relationships'
import { DocumentRelationshipError } from '../errors'
import { isNotNull } from './typeguards/isNotNull'

const findDocument = <DocumentType extends JsonApiDocument>(
spreeSuccessResponse: JsonApiResponse,
Expand Down Expand Up @@ -41,8 +42,8 @@ const findRelationshipDocuments = <DocumentType extends JsonApiDocument>(
}

return documentReferences
.map<DocumentType>((relationType: RelationType) => findDocument<DocumentType>(spreeSuccessResponse, relationType))
.filter(Boolean)
.map<DocumentType | null>((relationType) => findDocument<DocumentType>(spreeSuccessResponse, relationType))
.filter(isNotNull)
}

const findSingleRelationshipDocument = <DocumentType extends JsonApiDocument>(
Expand Down
8 changes: 6 additions & 2 deletions src/helpers/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ const isBuffer = (obj) => {
return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj))
}

const isNonNullishPrimitive = (v) =>
const isNonNullishPrimitive = (v: unknown) =>
typeof v === 'string' ||
typeof v === 'number' ||
typeof v === 'boolean' ||
typeof v === 'symbol' ||
typeof v === 'bigint'

const hexTable = (() => {
const array = []
const array: string[] = []

for (let i = 0; i < 256; ++i) {
array.push('%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase())
Expand Down Expand Up @@ -128,6 +128,10 @@ const stringify = (source: unknown, prefix: string) => {
return values
}

if (source === null || typeof source !== 'object') {
return values
}

const objKeys = Object.keys(source)

for (let i = 0; i < objKeys.length; ++i) {
Expand Down
3 changes: 3 additions & 0 deletions src/helpers/typeguards/isFetchError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { FetchError } from '../../errors'

export const isFetchError = (error: unknown): error is FetchError => error instanceof FetchError
1 change: 1 addition & 0 deletions src/helpers/typeguards/isNotNull.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const isNotNull = <T>(arg: T): arg is Exclude<T, null> => arg !== null
14 changes: 13 additions & 1 deletion src/interfaces/Account.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import type { IAddress } from './attributes/Address'
import { AllowedClientBuilderOptions, ClientBuilderOptions } from './ClientBuilderOptions'
import type { JsonApiDocument, JsonApiListResponse, JsonApiSingleResponse } from './JsonApi'
import { MakeOptional } from './MakeOptional'
import { MakeRequired } from './MakeRequired'
import type { IQuery } from './Query'
import type { IRelationships } from './Relationships'
import type { ResultResponse } from './ResultResponse'
import { WithClientBuilderOptions } from './WithClientBuilderOptions'
import type { WithCommonOptions } from './WithCommonOptions'

export interface AccountAttr extends JsonApiDocument {
Expand Down Expand Up @@ -75,7 +79,15 @@ export interface AccountAddressResult extends ResultResponse<AccountAddressRespo

export interface AccountAddressesResult extends ResultResponse<AccountAddressesResponse> {}

export type AccountInfoOptions = WithCommonOptions<{ suggestToken: true; suggestQuery: true }>
export type AccountContractTestOptions<ClientOptions extends AllowedClientBuilderOptions> = WithClientBuilderOptions<
ClientOptions,
MakeRequired<Partial<ClientBuilderOptions>, 'locale'>
>

export type AccountInfoOptions<ClientOptions extends AllowedClientBuilderOptions> = WithClientBuilderOptions<
ClientOptions,
MakeRequired<ClientBuilderOptions, 'bearer_token'>
>

export type CreditCardsListOptions = WithCommonOptions<{
suggestToken: true
Expand Down
12 changes: 12 additions & 0 deletions src/interfaces/ClientBuilderOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export type ClientBuilderOptions = Partial<{
order_token: string
bearer_token: string
locale: string
currency: string
}>

export type AllowedClientBuilderOptions = {
[K in keyof ClientBuilderOptions]-?: boolean
}

export type DefaultBuilderOptions = AllowedClientBuilderOptions & { [K in keyof AllowedClientBuilderOptions]: false }
Loading