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
+32 -20
View File
@@ -1,5 +1,5 @@
import { constants as httpStatus } from 'node:http2';
import { Injectable, ServiceUnavailableException } from '@nestjs/common';
import { constants as httpStatus } from 'node:http2';
const FALLBACK_PAGE_SIZE = 100;
@@ -7,18 +7,18 @@ const baseUrl = 'https://iss.moex.com/iss';
const timeoutMs = 10_000;
export type MoexBondEvent =
| { type: 'coupon'; date: string; value: string; currency: string }
| { type: 'maturity'; date: string; value: string; currency: string }
| { type: 'offer'; date: string };
| { currency: string; date: string; type: 'coupon'; value: string } |
{ currency: string; date: string; type: 'maturity'; value: string } |
{ date: string; type: 'offer' };
type IssBlock = { columns?: unknown; data?: unknown };
type IssResponse = Record<string, IssBlock | undefined>;
export type MoexBondYield = {
value: string;
pricePercent: string;
date: string;
dateType: string | null;
dateType: null | string;
pricePercent: string;
tradeAt: string;
value: string;
};
@Injectable()
@@ -58,8 +58,9 @@ export class MoexClient {
!Number.isFinite(Date.parse(date)) ||
!tradeAt ||
!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(tradeAt)
)
) {
return null;
}
const tradeIso = `${tradeAt.replace(' ', 'T')}+03:00`;
if (!Number.isFinite(Date.parse(tradeIso))) return null;
return {
@@ -70,6 +71,7 @@ export class MoexClient {
tradeAt: new Date(tradeIso).toISOString(),
};
}
async bondEvents(secid: string): Promise<MoexBondEvent[]> {
const path = `securities/${encodeURIComponent(secid)}/bondization.json`;
const [coupons, offers, description] = await Promise.all([
@@ -86,8 +88,9 @@ export class MoexClient {
const date = text(row.coupondate);
const value = numberText(row.value);
const currency = currencyText(row.faceunit);
if (date && value && currency)
if (date && value && currency) {
events.push({ type: 'coupon', date, value, currency });
}
}
for (const row of offers) {
const date = text(row.offerdate);
@@ -99,8 +102,9 @@ export class MoexClient {
const date = values.get('MATDATE');
const value = numberText(values.get('FACEVALUE'));
const currency = currencyText(values.get('FACEUNIT'));
if (date && value && currency)
if (date && value && currency) {
events.push({ type: 'maturity', date, value, currency });
}
return events;
}
@@ -115,10 +119,11 @@ export class MoexClient {
const query = new URLSearchParams({
'iss.meta': 'off',
'iss.only': `${block},${block}.cursor`,
start: String(start),
'start': String(start),
});
const response = await this.request(`${baseUrl}/${path}?${query}`);
if (!response) return rows;
// eslint-disable-next-line security/detect-object-injection -- Keys are fixed MOEX block names; row indexes come from the validated columns array.
const current = parseRows(response[block]);
rows.push(...current);
if (!paginated) return rows;
@@ -140,8 +145,9 @@ export class MoexClient {
pageSize === null ||
pageSize === 0 ||
current.length !== Math.min(pageSize, total - start)
)
) {
throw new ServiceUnavailableException({ code: 'MOEX_UNAVAILABLE' });
}
if (index + pageSize >= total) return rows;
start = index + pageSize;
}
@@ -155,8 +161,9 @@ export class MoexClient {
if (response.status === httpStatus.HTTP_STATUS_NOT_FOUND) return null;
if (!response.ok) throw new Error(`MOEX HTTP ${response.status}`);
const body: unknown = await response.json();
if (!body || typeof body !== 'object')
if (!body || typeof body !== 'object') {
throw new Error('MOEX malformed response');
}
return body as IssResponse;
} catch {
throw new ServiceUnavailableException({ code: 'MOEX_UNAVAILABLE' });
@@ -165,35 +172,40 @@ export class MoexClient {
}
function parseRows(block: IssBlock | undefined): Record<string, unknown>[] {
if (!Array.isArray(block?.columns) || !Array.isArray(block?.data))
if (!Array.isArray(block?.columns) || !Array.isArray(block?.data)) {
throw new ServiceUnavailableException({ code: 'MOEX_UNAVAILABLE' });
}
const columns = block.columns;
if (!columns.every((column) => typeof column === 'string'))
if (!columns.every((column) => typeof column === 'string')) {
throw new ServiceUnavailableException({ code: 'MOEX_UNAVAILABLE' });
}
return block.data.map((values) => {
if (!Array.isArray(values) || values.length !== columns.length)
if (!Array.isArray(values) || values.length !== columns.length) {
throw new ServiceUnavailableException({ code: 'MOEX_UNAVAILABLE' });
}
return Object.fromEntries(
// eslint-disable-next-line security/detect-object-injection -- Keys are fixed MOEX block names; row indexes come from the validated columns array.
columns.map((column, index) => [column.toLowerCase(), values[index]]),
);
});
}
function text(value: unknown): string | null {
function text(value: unknown): null | string {
return typeof value === 'string' && value.trim() ? value.trim() : null;
}
function numberText(value: unknown): string | null {
function numberText(value: unknown): null | string {
const result = typeof value === 'number' ? String(value) : text(value);
// eslint-disable-next-line security/detect-unsafe-regex -- Anchored decimal grammar: the literal decimal point separates disjoint digit runs; backtracking is linear.
return result && /^-?\d+(?:\.\d+)?$/.test(result) ? result : null;
}
function currencyText(value: unknown): string | null {
function currencyText(value: unknown): null | string {
const result = text(value)?.toUpperCase();
return result === 'SUR' ? 'RUB' : (result ?? null);
}
function integer(value: unknown): number | null {
function integer(value: unknown): null | number {
const result = Number(numberText(value) ?? NaN);
return Number.isSafeInteger(result) && result >= 0 ? result : null;
}