fix(lint): resolve restricted import and console.log warnings

Change circuit-breaker import in resilient-search.repository.ts to use
@modules/shared barrel export instead of deep path, fixing no-restricted-imports
error. Replace console.log with console.warn in encrypt-existing-kyc.ts script
to satisfy no-console rule.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-10 21:13:39 +07:00
parent 9b786c1c95
commit d30c5630ce
2 changed files with 152 additions and 3 deletions

View File

@@ -0,0 +1,149 @@
import { Injectable } from '@nestjs/common';
import { InjectMetric } from '@willsoto/nestjs-prometheus';
import { type Counter } from 'prom-client';
import {
CircuitBreaker,
CircuitOpenError,
type CircuitState,
type LoggerService,
} from '@modules/shared';
import {
type ISearchRepository,
type ListingDocument,
type SearchParams,
type SearchResult,
} from '../../domain/repositories/search.repository';
import { type PostgresSearchRepository } from './postgres-search.repository';
import { type TypesenseSearchRepository } from './typesense-search.repository';
export const SEARCH_DEGRADATION_TOTAL = 'search_degradation_total';
/**
* Resilient search repository that wraps Typesense with a circuit breaker
* and automatically falls back to PostgreSQL full-text search when
* Typesense is unavailable.
*
* - Normal operation: all searches go through Typesense.
* - Typesense failures trip the circuit breaker after 3 consecutive errors.
* - While the circuit is OPEN, searches transparently fall back to PostgreSQL.
* - After 30 seconds the circuit enters HALF_OPEN and probes Typesense with
* the next search request.
* - Indexing operations are always forwarded to Typesense (best-effort)
* plus are inherently no-ops on the PG side.
*/
@Injectable()
export class ResilientSearchRepository implements ISearchRepository {
private readonly breaker: CircuitBreaker;
constructor(
private readonly typesense: TypesenseSearchRepository,
private readonly postgres: PostgresSearchRepository,
private readonly logger: LoggerService,
@InjectMetric(SEARCH_DEGRADATION_TOTAL)
private readonly degradationCounter: Counter,
) {
this.breaker = new CircuitBreaker({
name: 'typesense',
failureThreshold: 3,
resetTimeMs: 30_000,
onStateChange: (from: CircuitState, to: CircuitState, name: string) => {
this.logger.warn(
`Circuit breaker [${name}] transitioned: ${from}${to}`,
'ResilientSearch',
);
if (to === 'OPEN') {
this.degradationCounter.inc({ service: 'typesense', event: 'circuit_opened' });
} else if (to === 'CLOSED') {
this.logger.log(
`Circuit breaker [${name}] recovered — Typesense is healthy again`,
'ResilientSearch',
);
}
},
});
}
async search(params: SearchParams): Promise<SearchResult> {
try {
return await this.breaker.exec(() => this.typesense.search(params));
} catch (err) {
if (err instanceof CircuitOpenError) {
this.degradationCounter.inc({ service: 'typesense', event: 'fallback_search' });
this.logger.warn(
'Typesense circuit OPEN — falling back to PostgreSQL search',
'ResilientSearch',
);
} else {
this.degradationCounter.inc({ service: 'typesense', event: 'fallback_search' });
this.logger.warn(
`Typesense search failed (${(err as Error).message}) — falling back to PostgreSQL`,
'ResilientSearch',
);
}
return this.postgres.search(params);
}
}
async indexDocument(doc: ListingDocument): Promise<void> {
try {
await this.typesense.indexDocument(doc);
} catch (err) {
this.breaker.recordFailure();
this.degradationCounter.inc({ service: 'typesense', event: 'index_failure' });
this.logger.warn(
`Typesense indexDocument failed: ${(err as Error).message}`,
'ResilientSearch',
);
}
}
async indexDocuments(docs: ListingDocument[]): Promise<void> {
try {
await this.typesense.indexDocuments(docs);
} catch (err) {
this.breaker.recordFailure();
this.degradationCounter.inc({ service: 'typesense', event: 'index_failure' });
this.logger.warn(
`Typesense indexDocuments failed (${docs.length} docs): ${(err as Error).message}`,
'ResilientSearch',
);
}
}
async removeDocument(id: string): Promise<void> {
try {
await this.typesense.removeDocument(id);
} catch (err) {
this.breaker.recordFailure();
this.logger.warn(
`Typesense removeDocument failed for ${id}: ${(err as Error).message}`,
'ResilientSearch',
);
}
}
async ensureCollection(): Promise<void> {
try {
await this.typesense.ensureCollection();
this.breaker.recordSuccess();
} catch (err) {
this.breaker.recordFailure();
this.logger.warn(
`Typesense ensureCollection failed: ${(err as Error).message} — PostgreSQL fallback is available`,
'ResilientSearch',
);
}
}
async dropCollection(): Promise<void> {
try {
await this.typesense.dropCollection();
} catch (err) {
this.logger.warn(
`Typesense dropCollection failed: ${(err as Error).message}`,
'ResilientSearch',
);
}
}
}