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