chore: strengthen backend eslint profile

This commit is contained in:
2026-09-28 16:37:01 +03:00
committed by ksv741
parent 53b2794963
commit a970eaaf7a
179 changed files with 4485 additions and 2716 deletions
+71 -62
View File
@@ -1,4 +1,3 @@
import { MILLISECONDS_PER_SECOND } from '../../common/time';
import {
ChannelCredentials,
Client,
@@ -17,8 +16,6 @@ import {
import { ConfigService } from '@nestjs/config';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { StructuredLogger } from '../../common/logging/structured-logger.service';
import { Environment } from '../../config/env.validation';
import type {
TbankBond,
@@ -29,6 +26,10 @@ import type {
TbankShare,
} from './tbank-market.types';
import { StructuredLogger } from '../../common/logging/structured-logger.service';
import { MILLISECONDS_PER_SECOND } from '../../common/time';
import { Environment } from '../../config/env.validation';
const RATE_WINDOW_MS = 1_000;
const METADATA_MAX_ATTEMPTS = 3;
const RPC_MAX_ATTEMPTS = 2;
@@ -42,78 +43,78 @@ type Unary<Request, Response> = (
request: Request,
metadata: Metadata,
options: { deadline: Date },
callback: (error: ServiceError | null, response: Response) => void,
callback: (error: null | ServiceError, response: Response) => void,
) => ClientUnaryCall;
type TbankClientService = Client & Record<string, Unary<never, never>>;
export type TbankDecimal = { units?: string | number; nano?: string | number };
export type TbankDecimal = { nano?: number | string; units?: number | string };
export type TbankMoney = TbankDecimal & { currency?: string };
export type TbankAccount = {
id: string;
type: string;
name: string;
openedDate?: { seconds?: string | number };
openedDate?: { seconds?: number | string };
type: string;
};
export type TbankPortfolio = {
positions?: {
figi?: string;
instrumentUid?: string;
instrumentType?: string;
ticker?: string;
quantity?: TbankDecimal;
currentPrice?: TbankMoney;
averagePositionPrice?: TbankMoney;
currentPrice?: TbankMoney;
figi?: string;
instrumentType?: string;
instrumentUid?: string;
quantity?: TbankDecimal;
ticker?: string;
}[];
};
export type TbankPositions = { money?: TbankMoney[] };
export type TbankInstrument = {
uid?: string;
brand?: { logoName?: null | string };
figi?: string;
instrumentType?: string;
ticker?: string;
name?: string;
isin?: string;
brand?: { logoName?: string | null };
name?: string;
ticker?: string;
uid?: string;
};
export type TbankDividend = {
dividendNet?: TbankMoney;
recordDate?: TbankTimestamp;
paymentDate?: TbankTimestamp;
lastBuyDate?: TbankTimestamp;
dividendType?: string;
lastBuyDate?: TbankTimestamp;
paymentDate?: TbankTimestamp;
recordDate?: TbankTimestamp;
};
export type TbankTimestamp = { seconds?: string | number; nanos?: number };
export type TbankTimestamp = { nanos?: number; seconds?: number | string };
export type TbankOperationItem = {
state?: string;
accruedInt?: TbankMoney;
commission?: TbankMoney;
cursor?: string;
id?: string;
name?: string;
date?: TbankTimestamp;
type?: string;
description?: string;
figi?: string;
instrumentUid?: string;
id?: string;
instrumentType?: string;
ticker?: string;
instrumentUid?: string;
name?: string;
payment?: TbankMoney;
price?: TbankMoney;
commission?: TbankMoney;
accruedInt?: TbankMoney;
quantity?: string | number;
quantityDone?: string | number;
quantity?: number | string;
quantityDone?: number | string;
state?: string;
ticker?: string;
type?: string;
};
export type TbankOperationsByCursorRequest = {
accountId: string;
instrumentId?: string;
from?: TbankTimestamp;
to?: TbankTimestamp;
cursor?: string;
from?: TbankTimestamp;
instrumentId?: string;
limit?: number;
state: 'OPERATION_STATE_EXECUTED';
to?: TbankTimestamp;
};
export type TbankOperationsByCursorResponse = {
hasNext?: boolean;
nextCursor?: string;
items?: TbankOperationItem[];
nextCursor?: string;
};
type TbankNamespace = {
tinkoff: {
@@ -197,12 +198,14 @@ export class TbankClient {
loadPackageDefinition(definition) as unknown as TbankNamespace
).tinkoff.public.invest.api.contract.v1;
const caPath = config.get<string | undefined>('TBANK_CA_CERT_PATH');
const credentials = caPath
? ChannelCredentials.createSsl(readFileSync(caPath))
: ChannelCredentials.createSsl();
const credentials = caPath ?
// eslint-disable-next-line security/detect-non-literal-fs-filename -- The CA path is trusted operator configuration, never supplied by an HTTP request.
ChannelCredentials.createSsl(readFileSync(caPath)) :
ChannelCredentials.createSsl();
const endpoint = config.getOrThrow<string>('TBANK_GRPC_ENDPOINT');
const create = (name: string) =>
new (
// eslint-disable-next-line security/detect-object-injection -- Service and method names are internal RPC selections, not request input.
services[name] as new (
address: string,
credentials: ChannelCredentials,
@@ -268,14 +271,14 @@ export class TbankClient {
getInstrumentByFigi(
token: string,
figi: string,
): Promise<TbankInstrument | null> {
): Promise<null | TbankInstrument> {
return this.getInstrument(token, 'INSTRUMENT_ID_TYPE_FIGI', figi);
}
getInstrumentByUid(
token: string,
instrumentUid: string,
): Promise<TbankInstrument | null> {
): Promise<null | TbankInstrument> {
return this.getInstrument(token, 'INSTRUMENT_ID_TYPE_UID', instrumentUid);
}
@@ -320,7 +323,8 @@ export class TbankClient {
},
).then((response) => response.instruments ?? []);
}
getBond(token: string, uid: string): Promise<TbankBond | null> {
getBond(token: string, uid: string): Promise<null | TbankBond> {
return this.call<{ instrument?: TbankBond }>(
token,
this.sparseInstruments,
@@ -331,6 +335,7 @@ export class TbankClient {
},
).then((response) => response.instrument ?? null);
}
getBondCoupons(
token: string,
uid: string,
@@ -349,7 +354,7 @@ export class TbankClient {
).then((response) => response.events ?? []);
}
getShare(token: string, uid: string): Promise<TbankShare | null> {
getShare(token: string, uid: string): Promise<null | TbankShare> {
return this.call<{ instrument?: TbankShare }>(
token,
this.instruments,
@@ -361,7 +366,7 @@ export class TbankClient {
).then((response) => response.instrument ?? null);
}
getAssetDescription(token: string, assetUid: string): Promise<string | null> {
getAssetDescription(token: string, assetUid: string): Promise<null | string> {
return this.call<{ asset?: { description?: string } }>(
token,
this.instruments,
@@ -375,7 +380,7 @@ export class TbankClient {
getFundamentals(
token: string,
assetUid: string,
): Promise<TbankFundamentals | null> {
): Promise<null | TbankFundamentals> {
return this.call<{ fundamentals?: TbankFundamentals[] }>(
token,
this.sparseInstruments,
@@ -399,13 +404,13 @@ export class TbankClient {
).then((response) => response.lastPrices ?? []);
}
getLastPrice(token: string, uid: string): Promise<TbankLastPrice | null> {
getLastPrice(token: string, uid: string): Promise<null | TbankLastPrice> {
return this.getLastPrices(token, [uid]).then(
(prices) => prices.find((item) => item.instrumentUid === uid) ?? null,
);
}
getTradingStatus(token: string, uid: string): Promise<string | null> {
getTradingStatus(token: string, uid: string): Promise<null | string> {
return this.call<{ tradingStatus?: string }>(
token,
this.marketData,
@@ -442,9 +447,9 @@ export class TbankClient {
private async getInstrument(
token: string,
idType: 'INSTRUMENT_ID_TYPE_UID' | 'INSTRUMENT_ID_TYPE_FIGI',
idType: 'INSTRUMENT_ID_TYPE_FIGI' | 'INSTRUMENT_ID_TYPE_UID',
id: string,
): Promise<TbankInstrument | null> {
): Promise<null | TbankInstrument> {
try {
const response = await this.call<{ instrument?: TbankInstrument }>(
token,
@@ -471,8 +476,9 @@ export class TbankClient {
const startedAt = performance.now();
const queueFull = this.queue.size >= MAX_QUEUE_SIZE;
try {
if (queueFull)
if (queueFull) {
throw new ServiceUnavailableException({ code: 'TBANK_UNAVAILABLE' });
}
const response = await this.queue.run(() =>
this.request<Response>(token, client, method, request),
);
@@ -484,15 +490,15 @@ export class TbankClient {
return response;
} catch (error) {
const grpcCode = (error as ServiceError).code;
const failureContext = queueFull
? { code: 'TBANK_UNAVAILABLE' }
: { grpcCode };
const retryable = queueFull
? metadataCall
: [
status.UNAVAILABLE,
const failureContext = queueFull ?
{ code: 'TBANK_UNAVAILABLE' } :
{ grpcCode };
const retryable = queueFull ?
metadataCall :
[
status.DEADLINE_EXCEEDED,
status.RESOURCE_EXHAUSTED,
status.UNAVAILABLE,
].includes(grpcCode);
if (retryable && attempts < maxAttempts) {
this.logger.event('warn', 'tbank.rpc.retry', {
@@ -504,9 +510,9 @@ export class TbankClient {
await new Promise((resolve) =>
setTimeout(
resolve,
metadataCall
? attempts * METADATA_RETRY_DELAY_MS
: RPC_RETRY_DELAY_MS,
metadataCall ?
attempts * METADATA_RETRY_DELAY_MS :
RPC_RETRY_DELAY_MS,
),
);
continue;
@@ -518,15 +524,17 @@ export class TbankClient {
durationMs: Math.round(performance.now() - startedAt),
});
if (
[status.UNAUTHENTICATED, status.PERMISSION_DENIED].includes(grpcCode)
)
[status.PERMISSION_DENIED, status.UNAUTHENTICATED].includes(grpcCode)
) {
// eslint-disable-next-line preserve-caught-error -- Discard raw upstream authentication errors at the integration boundary.
throw new Error('INVALID_TBANK_TOKEN');
}
if (
grpcCode === status.NOT_FOUND &&
(method === 'shareBy' || metadataCall)
)
) {
throw new NotFoundException();
}
throw new ServiceUnavailableException({ code: 'TBANK_UNAVAILABLE' });
}
}
@@ -542,6 +550,7 @@ export class TbankClient {
return new Promise((resolve, reject) => {
const metadata = new Metadata();
metadata.set('Authorization', `Bearer ${token}`);
// eslint-disable-next-line security/detect-object-injection -- Service and method names are internal RPC selections, not request input.
client[method](
request as never,
metadata,