diff --git a/package.json b/package.json index 1d17dab1..633b0100 100644 --- a/package.json +++ b/package.json @@ -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": [ diff --git a/src/client.ts b/src/client.ts index e1676e1c..868ebcad 100644 --- a/src/client.ts +++ b/src/client.ts @@ -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' @@ -16,6 +16,7 @@ const baseURL = (organization: string, domain?: string): string => { } + type RequestParams = Record type RequestHeaders = Record @@ -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 @@ -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, @@ -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 @@ -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 @@ -231,6 +247,10 @@ class ApiClient { return this.#accessToken } + get currentOrganization(): string { + return this.#organization + } + } diff --git a/src/commercelayer.ts b/src/commercelayer.ts index 9201c4c9..7759ecb1 100644 --- a/src/commercelayer.ts +++ b/src/commercelayer.ts @@ -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' @@ -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__## @@ -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) @@ -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 & { organization?: string }): void { - if (config.organization) this.#slug = config.organization + // if (config.organization) this.#slug = config.organization } @@ -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 diff --git a/src/config.ts b/src/config.ts index b51c7e68..09a5f83a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,3 +1,4 @@ +import { ApiClientInitConfig } from "./client" const config = { default: { @@ -7,7 +8,7 @@ const config = { }, client: { timeout: 15000, - requiredAttributes: ['organization', 'accessToken'], + requiredAttributes: ['organization', 'accessToken'] as Array, }, jsonapi: { maxResourceIncluded: 2 diff --git a/src/index.ts b/src/index.ts index 6749ea0d..109614cc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,8 @@ // SDK export { default, CommerceLayer } from './commercelayer' +export * from './instance' + // Commerce Layer static functions export { CommerceLayerStatic } from './static' diff --git a/src/instance.ts b/src/instance.ts new file mode 100644 index 00000000..b7ee4001 --- /dev/null +++ b/src/instance.ts @@ -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) +} \ No newline at end of file diff --git a/src/resource.ts b/src/resource.ts index 8da7e2b1..74cd6f65 100644 --- a/src/resource.ts +++ b/src/resource.ts @@ -101,26 +101,32 @@ type ResourcesInitConfig = ResourceAdapterConfig & ApiClientInitConfig type ResourcesConfig = Partial + 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 @@ -283,15 +289,23 @@ class ResourceAdapter { abstract class ApiResourceBase { 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(id: string | ResourceId | null): RR { return (((id === null) || (typeof id === 'string')) ? { id, type: this.type() } : { id: id.id, type: this.type() }) as RR } diff --git a/src/util.ts b/src/util.ts index 3a6a2d20..f5dd3703 100644 --- a/src/util.ts +++ b/src/util.ts @@ -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 => { +export const sleep = async (ms: number): Promise => { 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; @@ -43,23 +44,31 @@ const packageInfo = (fields?: string | string[], options?: any): Record { +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 } diff --git a/test/common.ts b/test/common.ts index 0cd0e986..dacdb4a5 100644 --- a/test/common.ts +++ b/test/common.ts @@ -90,7 +90,7 @@ const fakeClient = async (): Promise => { const getClient = (config?: CommerceLayerConfig): Promise => { - return config ? initClient(config) : fakeClient() + return config ? initClient(config) : fakeClient() } const printObject = (obj: unknown): string => { diff --git a/test/tree.ts b/test/tree.ts new file mode 100644 index 00000000..6995b4cf --- /dev/null +++ b/test/tree.ts @@ -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) + } + +})()