Skip to content

Commit

Permalink
Merge pull request #2 from leptoquark1/feat/application-instances
Browse files Browse the repository at this point in the history
Feature - Add support for multiple application instances
  • Loading branch information
ChristopherDosin authored Jun 7, 2022
2 parents 8192876 + 9e48f3f commit ab3a339
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 54 deletions.
3 changes: 1 addition & 2 deletions src/api/admin.api.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Context } from '../data';
import { ApiService } from '../service';

export class AdminApi extends ApiService {
protected getBasicHeaders(
additionalHeaders?: object,
): Record<string, string> {
let basicHeaders = super.getBasicHeaders(additionalHeaders);
const authToken = Context.getAuthToken();
const authToken = this.context.getAuthToken();
if (authToken) {
basicHeaders = {
...basicHeaders,
Expand Down
107 changes: 82 additions & 25 deletions src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
Context.setAuthToken(authToken);
if (authToken) {
await Application.loadEntitySchema();
}
}
export class ApplicationInstance {
readonly #context: ContextData;

public static async authenticate(grantType: GrantType): Promise<AuthToken> {
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<void> {
async loadEntitySchema(): Promise<void> {
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<string, EntitySchema> =
Expand All @@ -60,4 +58,63 @@ export class Application {
});
}
}

async authenticate(grantType: GrantType): Promise<AuthToken> {
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<void> {
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<void> {
return this.#instance.setAuthToken(authToken);
}

public static async authenticate(grantType: GrantType): Promise<AuthToken> {
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<void> {
return this.#instance.loadEntitySchema();
}
}
6 changes: 3 additions & 3 deletions src/auth/admin.auth.ts
Original file line number Diff line number Diff line change
@@ -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;
}

Expand Down
3 changes: 2 additions & 1 deletion src/data/repository.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 7 additions & 6 deletions src/factory/repository.factory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
ChangesetGenerator,
ChangesetGenerator, Context, ContextData,
EntityFactory,
Repository,
RepositoryOptions,
Expand All @@ -11,19 +11,20 @@ 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,
EntityDefinitionFactory.get(entityName),
new EntityHydrator(),
new ChangesetGenerator(),
new EntityFactory(),
options
options,
context
);
}
}
11 changes: 7 additions & 4 deletions src/helper/refresh-token.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand All @@ -17,13 +20,13 @@ export class RefreshTokenHelper {
*/
async fireRefreshTokenRequest(originError: any): Promise<AuthToken> {
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 {
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Everything you want to publish
import { Application } from './application';
import { Application, ApplicationInstance } from './application';
import {
AdminApi,
InfoApi,
Expand All @@ -26,6 +26,7 @@ import {

export {
Application,
ApplicationInstance,
Context,
AuthToken,
Criteria,
Expand Down
14 changes: 11 additions & 3 deletions src/service/api.service.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand Down
19 changes: 10 additions & 9 deletions src/service/http.service.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
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';

/**
* 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);
}

/**
* Creates the HTTP client with the provided context.
*
* @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');
}
Expand All @@ -30,8 +30,8 @@ const createClient = (): AxiosInstance => {
cancelToken: source.token,
});

if (Context.isAutoCalRefresh()) {
refreshTokenInterceptor(client);
if (context.isAutoCalRefresh()) {
refreshTokenInterceptor(client, context);
}

return client;
Expand All @@ -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,
Expand Down

0 comments on commit ab3a339

Please sign in to comment.