chore: strengthen backend eslint profile
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user