Sprint 9: add tenant-driven API resolution layer
This commit is contained in:
@@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, timer } from 'rxjs';
|
||||
import { retry } from 'rxjs/operators';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { ApiConfigService } from '../../config/api-config.service';
|
||||
import { CategoryDto } from '../dto/category.dto';
|
||||
import { CategoryRepository } from './category.repository';
|
||||
|
||||
@@ -13,10 +13,13 @@ export class ApiCategoryRepository implements CategoryRepository {
|
||||
delay: (_error: unknown, retryCount: number) => timer(Math.pow(2, retryCount) * 500)
|
||||
};
|
||||
|
||||
constructor(private readonly http: HttpClient) {}
|
||||
constructor(
|
||||
private readonly http: HttpClient,
|
||||
private readonly apiConfig: ApiConfigService
|
||||
) {}
|
||||
|
||||
getCategories(): Observable<CategoryDto[]> {
|
||||
return this.http.get<CategoryDto[]>(`${environment.apiUrl}/category`)
|
||||
return this.http.get<CategoryDto[]>(`${this.apiConfig.getBaseUrl()}/category`)
|
||||
.pipe(retry(this.retryConfig));
|
||||
}
|
||||
}
|
||||
86
src/app/core/config/api-config.service.ts
Normal file
86
src/app/core/config/api-config.service.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { ConfigService } from './config.service';
|
||||
import { TenantResolverService } from './tenant-resolver.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ApiConfigService {
|
||||
private readonly tenantResolver = inject(TenantResolverService);
|
||||
private readonly configService = inject(ConfigService);
|
||||
|
||||
getBaseUrl(): string {
|
||||
const tenantKey = this.tenantResolver.getTenantKey();
|
||||
const bootstrapUrl = this.resolveBootstrapApiBaseUrl();
|
||||
const tenantMap = (environment as any).tenantApiBaseUrls as Record<string, string> | undefined;
|
||||
const localhostUrl = (environment as any).localhostApiUrl as string | undefined;
|
||||
const apiTemplate = (environment as any).tenantApiTemplate as string | undefined;
|
||||
|
||||
let url = environment.apiUrl;
|
||||
|
||||
if (this.tenantResolver.isLocalhost() && localhostUrl) {
|
||||
url = localhostUrl;
|
||||
} else if (bootstrapUrl) {
|
||||
url = bootstrapUrl;
|
||||
} else if (tenantMap?.[tenantKey]) {
|
||||
url = tenantMap[tenantKey];
|
||||
} else if (apiTemplate) {
|
||||
url = apiTemplate.replace('{tenant}', tenantKey);
|
||||
}
|
||||
|
||||
return this.normalizeBaseUrl(url);
|
||||
}
|
||||
|
||||
isApiRequest(url: string): boolean {
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (url.startsWith('/api')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const baseUrl = this.getBaseUrl();
|
||||
return url.startsWith(baseUrl);
|
||||
}
|
||||
|
||||
toApiUrl(url: string): string {
|
||||
if (!url || /^https?:\/\//i.test(url)) {
|
||||
return url;
|
||||
}
|
||||
|
||||
if (!url.startsWith('/api')) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const baseUrl = this.getBaseUrl();
|
||||
const path = url.slice('/api'.length);
|
||||
return `${baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
}
|
||||
|
||||
private resolveBootstrapApiBaseUrl(): string | null {
|
||||
const bootstrap = this.configService.getBootstrapSnapshot() as any;
|
||||
if (!bootstrap) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const endpointBase = bootstrap?.apiEndpoints?.website?.baseUrl;
|
||||
if (typeof endpointBase === 'string' && endpointBase.trim().length > 0) {
|
||||
return endpointBase;
|
||||
}
|
||||
|
||||
const tenantBase = bootstrap?.tenant?.apiBaseUrl;
|
||||
if (typeof tenantBase === 'string' && tenantBase.trim().length > 0) {
|
||||
return tenantBase;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private normalizeBaseUrl(url: string): string {
|
||||
if (!url || url === '/') {
|
||||
return '/';
|
||||
}
|
||||
|
||||
return url.replace(/\/+$/, '');
|
||||
}
|
||||
}
|
||||
36
src/app/core/config/tenant-resolver.service.ts
Normal file
36
src/app/core/config/tenant-resolver.service.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TenantResolverService {
|
||||
private readonly document = inject(DOCUMENT);
|
||||
|
||||
getHostname(): string {
|
||||
const host = this.document?.location?.hostname ?? '';
|
||||
return host.toLowerCase();
|
||||
}
|
||||
|
||||
isLocalhost(): boolean {
|
||||
const hostname = this.getHostname();
|
||||
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
|
||||
}
|
||||
|
||||
getTenantKey(): string {
|
||||
if (this.isLocalhost()) {
|
||||
return (environment as any).fallbackTenantKey ?? 'default';
|
||||
}
|
||||
|
||||
const hostname = this.getHostname();
|
||||
const segments = hostname.split('.').filter(Boolean);
|
||||
if (segments.length === 0) {
|
||||
return (environment as any).fallbackTenantKey ?? 'default';
|
||||
}
|
||||
|
||||
if (segments[0] === 'www' && segments.length > 1) {
|
||||
return segments[1];
|
||||
}
|
||||
|
||||
return segments[0];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user