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:
@@ -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',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ async function main() {
|
|||||||
select: { id: true, kycData: true },
|
select: { id: true, kycData: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Found ${users.length} users with kycData.`);
|
console.warn(`Found ${users.length} users with kycData.`);
|
||||||
|
|
||||||
let encrypted = 0;
|
let encrypted = 0;
|
||||||
let skipped = 0;
|
let skipped = 0;
|
||||||
@@ -55,7 +55,7 @@ async function main() {
|
|||||||
const encryptedValue = encryptField(user.kycData, config);
|
const encryptedValue = encryptField(user.kycData, config);
|
||||||
|
|
||||||
if (dryRun) {
|
if (dryRun) {
|
||||||
console.log(`[DRY RUN] Would encrypt kycData for user ${user.id}`);
|
console.warn(`[DRY RUN] Would encrypt kycData for user ${user.id}`);
|
||||||
} else {
|
} else {
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
@@ -65,7 +65,7 @@ async function main() {
|
|||||||
encrypted++;
|
encrypted++;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.warn(
|
||||||
`${dryRun ? '[DRY RUN] ' : ''}Done. Encrypted: ${encrypted}, Already encrypted: ${skipped}`,
|
`${dryRun ? '[DRY RUN] ' : ''}Done. Encrypted: ${encrypted}, Already encrypted: ${skipped}`,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user