Skip to content

Commit

Permalink
feat: add adapter instances
Browse files Browse the repository at this point in the history
  • Loading branch information
pviti committed Feb 25, 2025
1 parent facc79f commit fac2899
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 30 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"semantic-release": "semantic-release",
"test": "vitest",
"test-local": "tsx test/spot.ts",
"test-tree": "tsx test/tree.ts",
"coverage": "vitest run --coverage"
},
"keywords": [
Expand Down
28 changes: 24 additions & 4 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { InterceptorManager } from './interceptor'
import config from './config'
import type { FetchResponse, FetchRequestOptions, FetchClientOptions, Fetch } from './fetch'
import { fetchURL } from './fetch'
import { isTokenExpired } from './util'
import { extractTokenData, isTokenExpired } from './util'


import Debug from './debug'
Expand All @@ -16,6 +16,7 @@ const baseURL = (organization: string, domain?: string): string => {
}



type RequestParams = Record<string, string | number | boolean>
type RequestHeaders = Record<string, string>

Expand Down Expand Up @@ -49,14 +50,25 @@ export type Method = 'GET' | 'DELETE' | 'POST' | 'PUT' | 'PATCH'
class ApiClient {

static create(options: ApiClientInitConfig): ApiClient {

// Take organization and domain from access token if not defined by user
if ((!options.organization || !options.domain) && options.accessToken) {
const tokenData = extractTokenData(options.accessToken)
if (!options.organization && tokenData?.organization) options.organization = tokenData.organization
if (!options.domain && tokenData?.domain) options.domain = tokenData.domain
}

for (const attr of config.client.requiredAttributes)
if (!options || !options[attr as keyof ApiClientInitConfig]) throw new SdkError({ message: `Undefined '${attr}' parameter` })
if (!options[attr]) throw new SdkError({ message: `Undefined '${attr}' parameter` })
return new ApiClient(options)

}


#baseUrl: string
#accessToken: string
#organization: string
#domain?: string
readonly #clientConfig: RequestConfig
readonly #interceptors: InterceptorManager

Expand All @@ -67,6 +79,8 @@ class ApiClient {

this.#baseUrl = baseURL(options.organization ?? '', options.domain)
this.#accessToken = options.accessToken
this.#organization = options.organization ?? '' // organization is always defined
this.#domain = options.domain

const fetchConfig: RequestConfig = {
timeout: options.timeout || config.client.timeout,
Expand Down Expand Up @@ -130,7 +144,9 @@ class ApiClient {
if (config.refreshToken) this.#clientConfig.refreshToken = config.refreshToken

// API Client config
if (config.organization) this.#baseUrl = baseURL(config.organization, config.domain)
if (config.organization || config.domain) this.#baseUrl = baseURL(config.organization || this.#organization, config.domain || this.#domain)
if (config.organization) this.#organization = config.organization
if (config.domain) this.#domain = config.domain
if (config.accessToken) {
this.#accessToken = config.accessToken
def.headers.Authorization = 'Bearer ' + this.#accessToken
Expand Down Expand Up @@ -191,7 +207,7 @@ class ApiClient {
} catch (err: any) { // Error executing api call

if (isExpiredTokenError(err) && this.#clientConfig.refreshToken && isTokenExpired(this.#accessToken)) { // If token has expired and must be refreshed

debug('Access token has expired')
const newAccessToken = await this.#clientConfig.refreshToken(this.#accessToken) // Refresh access token ...
.catch((e: any) => { // Error refreshing access token
Expand Down Expand Up @@ -231,6 +247,10 @@ class ApiClient {
return this.#accessToken
}

get currentOrganization(): string {
return this.#organization
}

}


Expand Down
16 changes: 8 additions & 8 deletions src/commercelayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as api from './api'
import type { ApiError } from './error'
import type { ErrorInterceptor, InterceptorType, RawResponseReader, RequestInterceptor, ResponseInterceptor, ResponseObj, HeadersObj, InterceptorManager } from './interceptor'
import { CommerceLayerStatic } from './static'
import ResourceAdapter, { type ResourcesInitConfig } from './resource'
import ResourceAdapter, { ApiResourceAdapter, type ResourcesInitConfig } from './resource'
import { extractTokenData } from './util'


Expand Down Expand Up @@ -32,7 +32,7 @@ class CommerceLayerClient {
readonly openApiSchemaVersion = OPEN_API_SCHEMA_VERSION

readonly #adapter: ResourceAdapter
#slug: string
// #slug: string

// ##__CL_RESOURCES_DEF_START__##
// ##__CL_RESOURCES_DEF_TEMPLATE:: ##__TAB__#####__RESOURCE_TYPE__##?: api.##__RESOURCE_CLASS__##
Expand Down Expand Up @@ -175,12 +175,12 @@ class CommerceLayerClient {
// Take organization and domain from access token if not defined by user
if ((!config.organization || !config.domain) && config.accessToken) {
const tokenData = extractTokenData(config.accessToken)
if (!config.organization && tokenData?.organization?.slug) config.organization = tokenData.organization.slug
if (!config.domain && tokenData?.iss) config.domain = String(tokenData.iss).replace('https://auth.', '')
if (!config.organization && tokenData?.organization) config.organization = tokenData.organization
if (!config.domain && tokenData?.domain) config.domain = tokenData.domain
}

this.#adapter = new ResourceAdapter(config)
this.#slug = config.organization ?? ''
// this.#slug = config.organization ?? ''

// ##__CL_RESOURCES_INIT_START__##
// ##__CL_RESOURCES_INIT_TEMPLATE:: ##__TAB__####__TAB__##this.##__RESOURCE_TYPE__## = new api.##__RESOURCE_CLASS__##(this.#adapter)
Expand Down Expand Up @@ -322,13 +322,13 @@ class CommerceLayerClient {
// ##__CL_RESOURCES_LEAZY_LOADING_STOP__##


get currentOrganization(): string { return this.#slug }
get currentOrganization(): string { return this.#adapter?.client?.currentOrganization }
get currentAccessToken(): string { return this.#adapter?.client?.currentAccessToken }
private get interceptors(): InterceptorManager { return this.#adapter.client.interceptors }


private localConfig(config: Partial<SdkConfig> & { organization?: string }): void {
if (config.organization) this.#slug = config.organization
// if (config.organization) this.#slug = config.organization
}


Expand All @@ -340,7 +340,7 @@ class CommerceLayerClient {
this.localConfig(config)
// ResourceAdapter config
// To rebuild baseUrl in client in case only the domain is defined
if (!config.organization) config.organization = this.currentOrganization
// if (!config.organization) config.organization = this.currentOrganization
this.#adapter.config(config)

return this
Expand Down
3 changes: 2 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ApiClientInitConfig } from "./client"

const config = {
default: {
Expand All @@ -7,7 +8,7 @@ const config = {
},
client: {
timeout: 15000,
requiredAttributes: ['organization', 'accessToken'],
requiredAttributes: ['organization', 'accessToken'] as Array<keyof ApiClientInitConfig>,
},
jsonapi: {
maxResourceIncluded: 2
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// SDK
export { default, CommerceLayer } from './commercelayer'

export * from './instance'

// Commerce Layer static functions
export { CommerceLayerStatic } from './static'

Expand Down
16 changes: 16 additions & 0 deletions src/instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {
Addresses,
Customers
} from './api'
import { CommerceLayerInitConfig } from './commercelayer'
import { ApiResourceAdapter } from './resource'


export const addresses = new Addresses()
export const customers = new Customers()



export const initCommerceLayer = (config: CommerceLayerInitConfig): void => {
ApiResourceAdapter.init(config)
}
28 changes: 21 additions & 7 deletions src/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,26 +101,32 @@ type ResourcesInitConfig = ResourceAdapterConfig & ApiClientInitConfig
type ResourcesConfig = Partial<ResourcesInitConfig>



export class ApiResourceAdapter {

private static adapter: ResourceAdapter


static init(config: ResourcesInitConfig): ResourceAdapter {
return (ApiResourceAdapter.adapter = new ResourceAdapter(config))
}

static get(config?: ResourcesInitConfig): ResourceAdapter {
if (config) return ApiResourceAdapter.adapter = new ResourceAdapter(config)
if (config) return ApiResourceAdapter.init(config)
else {
if (ApiResourceAdapter.adapter) return ApiResourceAdapter.adapter
else throw new SdkError({ message: 'Invalid adapter config' })
else throw new SdkError({ message: 'RersourceAdapter not initialized' })
}
}

static config(config: ResourcesConfig): void {
if (ApiResourceAdapter.adapter) ApiResourceAdapter.adapter.config(config)
else throw new SdkError({ message: 'Adapter not inizialized' })
ApiResourceAdapter.get().config(config)
}

}



class ResourceAdapter {

readonly #client: ApiClient
Expand Down Expand Up @@ -283,15 +289,23 @@ class ResourceAdapter {
abstract class ApiResourceBase<R extends Resource> {

static readonly TYPE: ResourceTypeLock
protected readonly resources: ResourceAdapter
#resources?: ResourceAdapter


constructor(adapter: ResourceAdapter) {
constructor(adapter?: ResourceAdapter) {
debug('new resource instance: %s', this.type())
this.resources = adapter
this.#resources = adapter
}


protected get resources(): ResourceAdapter {
return this.#resources || (this.#resources = ApiResourceAdapter.get())
}


abstract relationship(id: string | ResourceId | null): ResourceRel


protected relationshipOneToOne<RR extends ResourceRel>(id: string | ResourceId | null): RR {
return (((id === null) || (typeof id === 'string')) ? { id, type: this.type() } : { id: id.id, type: this.type() }) as RR
}
Expand Down
27 changes: 18 additions & 9 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type { ObjectType } from "../src/types"
import { SdkError } from "./error"
// import path from 'node:path'



const sleep = async (ms: number): Promise<NodeJS.Timeout> => {
export const sleep = async (ms: number): Promise<NodeJS.Timeout> => {
return new Promise(resolve => setTimeout(resolve, ms))
}


const sortObjectFields = (obj: ObjectType): ObjectType => {
export const sortObjectFields = (obj: ObjectType): ObjectType => {
const sorted = Object.keys(obj).sort().reduce((accumulator: ObjectType, key: string) => {
accumulator[key] = obj[key];
return accumulator;
Expand Down Expand Up @@ -43,23 +44,31 @@ const packageInfo = (fields?: string | string[], options?: any): Record<string,
}
*/

const extractTokenData = (token: string): any => {
export type TokenData = {
organization: string,
domain?: string,
expiration: number
}

export const extractTokenData = (token: string): TokenData | undefined => {
try {
return JSON.parse(atob(token.split('.')[1]))
const data = JSON.parse(atob(token.split('.')[1]))
return {
organization: data.organization.slug,
domain: data.iss? String(data.iss).replace('https://auth.', '') : undefined,
expiration: data.exp
}
} catch (err: any) {
return undefined
}
}


const isTokenExpired = (token: string): boolean => {
export const isTokenExpired = (token: string): boolean => {
try {
const tokenData = extractTokenData(token)
return (((tokenData.exp * 1000) - Date.now()) < 0)
return tokenData?.expiration? (((tokenData.expiration * 1000) - Date.now()) < 0) : false
} catch (err: any) {
return false
}
}


export { sleep, sortObjectFields, /* packageInfo */ isTokenExpired, extractTokenData }
2 changes: 1 addition & 1 deletion test/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const fakeClient = async (): Promise<CommerceLayerClient> => {


const getClient = (config?: CommerceLayerConfig): Promise<CommerceLayerClient> => {
return config ? initClient(config) : fakeClient()
return config ? initClient(config) : fakeClient()
}

const printObject = (obj: unknown): string => {
Expand Down
22 changes: 22 additions & 0 deletions test/tree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { handleError, initConfig } from './util'
import commercelayer, { customers, initCommerceLayer } from '../src'


; (async () => {

const config = await initConfig()
// const cl = commercelayer(config)

try {

initCommerceLayer(config)

const res = await customers.list()
console.log(res)


} catch (error: any) {
handleError(error, true)
}

})()

0 comments on commit fac2899

Please sign in to comment.