forked from Shared/esg
initial commit
This commit is contained in:
152
src/providers/base/base-provider.abstract.ts
Normal file
152
src/providers/base/base-provider.abstract.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InquiryType } from '../../common/enums/inquiry-type.enum';
|
||||
import { ProviderName } from '../../common/enums/provider-name.enum';
|
||||
import { NormalizedErrorDto } from '../../common/dto/normalized-error.dto';
|
||||
import {
|
||||
InquiryProvider,
|
||||
ProviderExecutionContext,
|
||||
} from '../../common/interfaces/inquiry-provider.interface';
|
||||
import { withRetry } from '../../common/helpers/retry.helper';
|
||||
import { withTimeout } from '../../common/helpers/timeout.helper';
|
||||
import { RequestLogger } from '../../common/helpers/request-logger.helper';
|
||||
import { ProviderConfigSlice } from '../interfaces/provider-config.interface';
|
||||
|
||||
/**
|
||||
* Abstract base for all provider adapters.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Retry + timeout orchestration
|
||||
* - Standardized error normalization
|
||||
* - Response formatting hooks
|
||||
* - Structured logging
|
||||
*
|
||||
* Subclasses implement provider-specific HTTP/business logic only.
|
||||
*/
|
||||
export abstract class BaseProvider<TRequest = unknown, TResponse = unknown>
|
||||
implements InquiryProvider<TRequest, TResponse>
|
||||
{
|
||||
abstract readonly name: ProviderName;
|
||||
abstract readonly supportedInquiryTypes: InquiryType[];
|
||||
|
||||
protected readonly logger: RequestLogger;
|
||||
protected readonly nestLogger: Logger;
|
||||
|
||||
constructor(protected readonly config: ProviderConfigSlice) {
|
||||
this.logger = new RequestLogger(this.constructor.name);
|
||||
this.nestLogger = new Logger(this.constructor.name);
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
return this.config.enabled;
|
||||
}
|
||||
|
||||
async execute(
|
||||
inquiryType: InquiryType,
|
||||
payload: TRequest,
|
||||
context: ProviderExecutionContext,
|
||||
): Promise<TResponse> {
|
||||
if (!this.supportedInquiryTypes.includes(inquiryType)) {
|
||||
throw this.normalizeError({
|
||||
code: 'UNSUPPORTED_INQUIRY',
|
||||
message: `Provider ${this.name} does not support ${inquiryType}`,
|
||||
});
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
this.logger.logStart(
|
||||
{
|
||||
requestId: context.requestId,
|
||||
trackingCode: context.trackingCode,
|
||||
provider: this.name,
|
||||
inquiryType,
|
||||
},
|
||||
'Provider execution started',
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await this.executeWithResilience(
|
||||
() => this.callProvider(inquiryType, payload, context),
|
||||
context,
|
||||
);
|
||||
|
||||
this.logger.logSuccess(
|
||||
{
|
||||
requestId: context.requestId,
|
||||
trackingCode: context.trackingCode,
|
||||
provider: this.name,
|
||||
inquiryType,
|
||||
durationMs: Date.now() - start,
|
||||
},
|
||||
'Provider execution succeeded',
|
||||
);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.logFailure(
|
||||
{
|
||||
requestId: context.requestId,
|
||||
trackingCode: context.trackingCode,
|
||||
provider: this.name,
|
||||
inquiryType,
|
||||
durationMs: Date.now() - start,
|
||||
},
|
||||
'Provider execution failed',
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
protected async executeWithResilience<T>(
|
||||
fn: () => Promise<T>,
|
||||
context: ProviderExecutionContext,
|
||||
): Promise<T> {
|
||||
const operation = () =>
|
||||
withTimeout(fn(), this.config.timeout, `${this.name} request`);
|
||||
|
||||
return withRetry(operation, {
|
||||
maxAttempts: this.config.maxRetries,
|
||||
delayMs: 300,
|
||||
shouldRetry: (error) => this.isRetryable(error),
|
||||
});
|
||||
}
|
||||
|
||||
protected isRetryable(error: unknown): boolean {
|
||||
if (error && typeof error === 'object' && 'code' in error) {
|
||||
const code = (error as { code?: string }).code;
|
||||
return code === 'ETIMEDOUT' || code === 'ECONNABORTED' || code === 'ECONNRESET';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected normalizeError(partial: Partial<NormalizedErrorDto>): NormalizedErrorDto {
|
||||
return {
|
||||
code: partial.code ?? 'PROVIDER_ERROR',
|
||||
message: partial.message ?? 'Provider request failed',
|
||||
providerMessage: partial.providerMessage,
|
||||
providerCode: partial.providerCode,
|
||||
};
|
||||
}
|
||||
|
||||
protected formatProviderError(
|
||||
providerMessage?: string,
|
||||
providerCode?: string,
|
||||
fallbackMessage = 'Provider returned an error',
|
||||
): Error {
|
||||
const normalized = this.normalizeError({
|
||||
code: 'PROVIDER_ERROR',
|
||||
message: fallbackMessage,
|
||||
providerMessage,
|
||||
providerCode,
|
||||
});
|
||||
const err = new Error(normalized.message);
|
||||
(err as Error & { normalizedError: NormalizedErrorDto }).normalizedError = normalized;
|
||||
return err;
|
||||
}
|
||||
|
||||
protected abstract callProvider(
|
||||
inquiryType: InquiryType,
|
||||
payload: TRequest,
|
||||
context: ProviderExecutionContext,
|
||||
): Promise<TResponse>;
|
||||
}
|
||||
Reference in New Issue
Block a user