From 9e48f3f77b4f7f94f6b47c1530c3269915d26ed7 Mon Sep 17 00:00:00 2001 From: David Fecke Date: Sat, 21 May 2022 22:38:55 +0200 Subject: [PATCH] feat: add support for multiple application instances --- src/api/admin.api.ts | 3 +- src/application.ts | 107 ++++++++++++++++++++++------- src/auth/admin.auth.ts | 6 +- src/data/repository.data.ts | 3 +- src/factory/repository.factory.ts | 13 ++-- src/helper/refresh-token.helper.ts | 11 +-- src/index.ts | 3 +- src/service/api.service.ts | 14 +++- src/service/http.service.ts | 19 ++--- 9 files changed, 125 insertions(+), 54 deletions(-) diff --git a/src/api/admin.api.ts b/src/api/admin.api.ts index 0d1ece3..2d91750 100644 --- a/src/api/admin.api.ts +++ b/src/api/admin.api.ts @@ -1,4 +1,3 @@ -import { Context } from '../data'; import { ApiService } from '../service'; export class AdminApi extends ApiService { @@ -6,7 +5,7 @@ export class AdminApi extends ApiService { additionalHeaders?: object, ): Record { let basicHeaders = super.getBasicHeaders(additionalHeaders); - const authToken = Context.getAuthToken(); + const authToken = this.context.getAuthToken(); if (authToken) { basicHeaders = { ...basicHeaders, diff --git a/src/application.ts b/src/application.ts index 938c696..a2c3086 100644 --- a/src/application.ts +++ b/src/application.ts @@ -11,45 +11,43 @@ export type initOptions = { autoCallRefresh?: boolean; }; -export class Application { - public static init({ - shopUrl, - apiPath = '/api', - autoCallRefresh = true, - }: initOptions): void { - Context.setApiEndPoint(shopUrl, apiPath); - Context.setApiResourcePath(shopUrl, apiPath); - Context.setAutoCallRefresh(autoCallRefresh); - } +const defaultOptions = { + apiPath: '/api', + autoCallRefresh: true, +}; - public static async setAuthToken(authToken: AuthToken | null): Promise { - Context.setAuthToken(authToken); - if (authToken) { - await Application.loadEntitySchema(); - } - } +export class ApplicationInstance { + readonly #context: ContextData; - public static async authenticate(grantType: GrantType): Promise { - const adminAuth: AdminAuth = new AdminAuth(grantType); - const authToken: AuthToken = await adminAuth.fetchAccessToken(); + constructor(options: initOptions | ContextData) { + if (options instanceof ContextData) { + this.#context = options; + } else { + const { shopUrl } = options; + let { apiPath, autoCallRefresh} = options; + this.#context = new ContextData(); - await Application.setAuthToken(authToken); + apiPath ||= defaultOptions.apiPath; + autoCallRefresh ||= defaultOptions.autoCallRefresh; - return authToken; + this.#context.setApiEndPoint(options.shopUrl, apiPath); + this.#context.setApiResourcePath(shopUrl, apiPath); + this.#context.setAutoCallRefresh(autoCallRefresh ?? defaultOptions.autoCallRefresh); + } } - public static getConText(): ContextData { - return Context; + getContext(): ContextData { + return this.#context; } /** * Load new entity scheme from shopware application */ - public static async loadEntitySchema(): Promise { + async loadEntitySchema(): Promise { const definitionRegistry = EntityDefinitionFactory.getDefinitionRegistry(); if (definitionRegistry.size === 0) { - const infoApi: InfoApi = new InfoApi(); + const infoApi: InfoApi = new InfoApi(this.#context); // Load schema entity from server const schemas: Record = @@ -60,4 +58,63 @@ export class Application { }); } } + + async authenticate(grantType: GrantType): Promise { + const adminAuth: AdminAuth = new AdminAuth(grantType, this.#context); + const authToken: AuthToken = await adminAuth.fetchAccessToken(); + + await this.setAuthToken(authToken); + + return authToken; + } + + async setAuthToken(authToken: AuthToken | null): Promise { + this.#context.setAuthToken(authToken); + if (authToken) { + await this.loadEntitySchema(); + } + } +} + +export class Application { + static #instance = new ApplicationInstance(Context); + + /** + * @deprecated Use ApplicationInstance instead + */ + public static init({ + shopUrl, + apiPath = defaultOptions.apiPath, + autoCallRefresh = defaultOptions.autoCallRefresh, + }: initOptions): void { + Context.setApiEndPoint(shopUrl, apiPath); + Context.setApiResourcePath(shopUrl, apiPath); + Context.setAutoCallRefresh(autoCallRefresh); + } + + public static async setAuthToken(authToken: AuthToken | null): Promise { + return this.#instance.setAuthToken(authToken); + } + + public static async authenticate(grantType: GrantType): Promise { + return this.#instance.authenticate(grantType); + } + + /** + * @deprecated Use `getContext()` instead + */ + public static getConText(): ContextData { + return this.getContext(); + } + + public static getContext(): ContextData { + return this.#instance.getContext(); + } + + /** + * Load new entity scheme from shopware application + */ + public static async loadEntitySchema(): Promise { + return this.#instance.loadEntitySchema(); + } } diff --git a/src/auth/admin.auth.ts b/src/auth/admin.auth.ts index 1899be4..4adea51 100644 --- a/src/auth/admin.auth.ts +++ b/src/auth/admin.auth.ts @@ -1,15 +1,15 @@ import { ApiService } from '../service'; import { AuthorizationException } from '../exception'; import { GrantParamsType, GrantType } from '../grant'; -import { AuthToken } from '../data'; +import { AuthToken, ContextData } from '../data'; export class AdminAuth extends ApiService { public static OAUTH_TOKEN_ENDPOINT = '/oauth/token'; private grantType: GrantType; - constructor(grantType: GrantType) { - super(); + constructor(grantType: GrantType, context?: ContextData) { + super(context); this.grantType = grantType; } diff --git a/src/data/repository.data.ts b/src/data/repository.data.ts index e76f635..7508250 100644 --- a/src/data/repository.data.ts +++ b/src/data/repository.data.ts @@ -41,9 +41,10 @@ export class Repository { changesetGenerator: ChangesetGenerator, entityFactory: EntityFactory, options: RepositoryOptions, + context: ContextData, ) { this.route = route; - this.httpClient = createHTTPClient(); + this.httpClient = createHTTPClient(context); this.entityDefinition = entityDefinition; this.entityName = entityDefinition.entity; this.hydrator = hydrator; diff --git a/src/factory/repository.factory.ts b/src/factory/repository.factory.ts index 01373d6..6d5adf8 100644 --- a/src/factory/repository.factory.ts +++ b/src/factory/repository.factory.ts @@ -1,5 +1,5 @@ import { - ChangesetGenerator, + ChangesetGenerator, Context, ContextData, EntityFactory, Repository, RepositoryOptions, @@ -11,11 +11,11 @@ export class RepositoryFactory { public static create( entityName: string, route = '', - options: RepositoryOptions = {} + options: RepositoryOptions = {}, + context?: ContextData, ): Repository { - if (!route) { - route = `/${entityName.replace(/_/g, '-')}`; - } + route ||= `/${entityName.replace(/_/g, '-')}`; + context ||= Context; return new Repository( route, @@ -23,7 +23,8 @@ export class RepositoryFactory { new EntityHydrator(), new ChangesetGenerator(), new EntityFactory(), - options + options, + context ); } } diff --git a/src/helper/refresh-token.helper.ts b/src/helper/refresh-token.helper.ts index 59ae09f..3cfa0a9 100644 --- a/src/helper/refresh-token.helper.ts +++ b/src/helper/refresh-token.helper.ts @@ -2,13 +2,16 @@ * Refresh token helper which manages a cache of requests to retry them after the token got refreshed. * @class RefreshTokenHelper */ -import { AuthToken, Context } from '../data'; +import { AuthToken, Context, ContextData } from '../data'; import { RefreshTokenGrant } from '../grant'; import { AdminAuth } from '../auth'; export class RefreshTokenHelper { private _whitelists = ['/oauth/token']; + constructor(private readonly context: ContextData) { + } + /** * Fires the refresh token request and renews the bearer authentication * @@ -17,13 +20,13 @@ export class RefreshTokenHelper { */ async fireRefreshTokenRequest(originError: any): Promise { try { - let authToken = Context.getAuthToken(); + let authToken = this.context.getAuthToken(); if (authToken) { const grantType = new RefreshTokenGrant(authToken.refreshToken); - const adminClient = new AdminAuth(grantType); + const adminClient = new AdminAuth(grantType, this.context); authToken = await adminClient.fetchAccessToken(); - Context.setAuthToken(authToken); + this.context.setAuthToken(authToken); return authToken; } else { diff --git a/src/index.ts b/src/index.ts index 85e85d2..12a831c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ // Everything you want to publish -import { Application } from './application'; +import { Application, ApplicationInstance } from './application'; import { AdminApi, InfoApi, @@ -26,6 +26,7 @@ import { export { Application, + ApplicationInstance, Context, AuthToken, Criteria, diff --git a/src/service/api.service.ts b/src/service/api.service.ts index ed080df..8912671 100644 --- a/src/service/api.service.ts +++ b/src/service/api.service.ts @@ -1,13 +1,21 @@ import { AxiosInstance, AxiosRequestConfig } from 'axios'; import createHTTPClient from '../service/http.service'; +import { Context, ContextData } from '../data'; export abstract class ApiService { + protected readonly context: ContextData; protected httpClient: AxiosInstance; public contentType: string; - constructor(contentType = 'application/vnd.api+json') { - this.httpClient = createHTTPClient(); - this.contentType = contentType; + constructor(context?: ContextData | string, contentType?: string) { + if (typeof context === 'string') { + contentType = context; + context = Context; + } + + this.context = context ?? Context; + this.httpClient = createHTTPClient(this.context); + this.contentType = contentType ?? 'application/vnd.api+json'; } protected serializeUrl( diff --git a/src/service/http.service.ts b/src/service/http.service.ts index 1e9a607..737746d 100644 --- a/src/service/http.service.ts +++ b/src/service/http.service.ts @@ -1,5 +1,5 @@ import Axios, { AxiosError, AxiosInstance } from 'axios'; -import { AuthToken, Context } from '../data'; +import { AuthToken, ContextData } from '../data'; import { types } from './util.service'; import { Exception } from '../exception'; import { RefreshTokenHelper } from '../helper/refresh-token.helper'; @@ -7,8 +7,8 @@ import { RefreshTokenHelper } from '../helper/refresh-token.helper'; /** * Initializes the HTTP client with the provided context. */ -export default function createHTTPClient(): AxiosInstance { - return createClient(); +export default function createHTTPClient(context: ContextData): AxiosInstance { + return createClient(context); } /** @@ -16,8 +16,8 @@ export default function createHTTPClient(): AxiosInstance { * * @returns {AxiosInstance} */ -const createClient = (): AxiosInstance => { - const apiEndPoint = Context.getApiEndPoint(); +const createClient = (context: ContextData): AxiosInstance => { + const apiEndPoint = context.getApiEndPoint(); if (types.isEmpty(apiEndPoint)) { throw new Exception('Please provide shop-url to context'); } @@ -30,8 +30,8 @@ const createClient = (): AxiosInstance => { cancelToken: source.token, }); - if (Context.isAutoCalRefresh()) { - refreshTokenInterceptor(client); + if (context.isAutoCalRefresh()) { + refreshTokenInterceptor(client, context); } return client; @@ -41,10 +41,11 @@ const createClient = (): AxiosInstance => { * Sets up an interceptor to refresh the token, cache the requests and retry them after the token got refreshed. * * @param {AxiosInstance} client + * @param context * @returns {AxiosInstance} */ -function refreshTokenInterceptor(client: AxiosInstance): AxiosInstance { - const tokenHandler = new RefreshTokenHelper(); +function refreshTokenInterceptor(client: AxiosInstance, context: ContextData): AxiosInstance { + const tokenHandler = new RefreshTokenHelper(context); client.interceptors.response.use( (response) => response,