MVP реализации MoexVibe — NestJS бэкенд + React фронтенд + CI/CD #1
@ -11,32 +11,34 @@
|
|||||||
"test:watch": "vitest"
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^10.0.0",
|
|
||||||
"@nestjs/core": "^10.0.0",
|
|
||||||
"@nestjs/platform-express": "^10.0.0",
|
|
||||||
"@nestjs/config": "^3.0.0",
|
|
||||||
"@nestjs/swagger": "^7.0.0",
|
|
||||||
"@nestjs/axios": "^3.0.0",
|
"@nestjs/axios": "^3.0.0",
|
||||||
"@nestjs/cache-manager": "^2.0.0",
|
"@nestjs/cache-manager": "^2.0.0",
|
||||||
"cache-manager": "^5.0.0",
|
"@nestjs/common": "^10.0.0",
|
||||||
|
"@nestjs/config": "^3.0.0",
|
||||||
|
"@nestjs/core": "^10.0.0",
|
||||||
|
"@nestjs/platform-express": "^10.0.0",
|
||||||
|
"@nestjs/swagger": "^7.0.0",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
|
"cache-manager": "^5.0.0",
|
||||||
|
"class-transformer": "^0.5.0",
|
||||||
|
"class-validator": "^0.14.0",
|
||||||
|
"p-queue": "^7.3.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rxjs": "^7.8.0",
|
"rxjs": "^7.8.0",
|
||||||
"class-validator": "^0.14.0",
|
|
||||||
"class-transformer": "^0.5.0",
|
|
||||||
"p-queue": "^7.3.0",
|
|
||||||
"swagger-ui-express": "^5.0.0"
|
"swagger-ui-express": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.0.0",
|
||||||
"@nestjs/testing": "^10.0.0",
|
"@nestjs/testing": "^10.0.0",
|
||||||
|
"@swc/core": "^1.15.41",
|
||||||
"@types/express": "^4.17.0",
|
"@types/express": "^4.17.0",
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"typescript": "^5.3.0",
|
|
||||||
"vitest": "^1.0.0",
|
|
||||||
"eslint": "^8.0.0",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
"@typescript-eslint/parser": "^7.0.0"
|
"@typescript-eslint/parser": "^7.0.0",
|
||||||
|
"eslint": "^8.0.0",
|
||||||
|
"typescript": "^5.3.0",
|
||||||
|
"unplugin-swc": "^1.5.9",
|
||||||
|
"vitest": "^1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { MoexClientService } from './moex-client.service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [MoexClientService],
|
||||||
|
exports: [MoexClientService],
|
||||||
|
})
|
||||||
|
export class MoexClientModule {}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
import 'reflect-metadata';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { MoexClientService } from './moex-client.service';
|
||||||
|
import configuration from '../../config/configuration';
|
||||||
|
|
||||||
|
describe('MoexClientService', () => {
|
||||||
|
let service: MoexClientService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({ load: [configuration] }),
|
||||||
|
],
|
||||||
|
providers: [MoexClientService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<MoexClientService>(MoexClientService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('searchSecurities', () => {
|
||||||
|
it('should return results for SBER query', async () => {
|
||||||
|
const results = await service.searchSecurities('SBER');
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
if (results.length > 0) {
|
||||||
|
expect(results[0].secid).toBeDefined();
|
||||||
|
}
|
||||||
|
}, 15000);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getShareMarketData', () => {
|
||||||
|
it('should return market data for SBER', async () => {
|
||||||
|
const data = await service.getShareMarketData('SBER');
|
||||||
|
if (data) {
|
||||||
|
expect(data.secid).toBe('SBER');
|
||||||
|
}
|
||||||
|
}, 15000);
|
||||||
|
});
|
||||||
|
});
|
||||||
330
apps/backend/src/modules/moex-client/moex-client.service.ts
Normal file
330
apps/backend/src/modules/moex-client/moex-client.service.ts
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
import PQueue from 'p-queue';
|
||||||
|
import {
|
||||||
|
MoexSecurityDescription,
|
||||||
|
MoexShareMarketData,
|
||||||
|
MoexBondData,
|
||||||
|
MoexBondMarketData,
|
||||||
|
MoexDividend,
|
||||||
|
MoexCandle,
|
||||||
|
MoexHistoryEntry,
|
||||||
|
MoexBondHistoryEntry,
|
||||||
|
} from './moex-client.types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MoexClientService {
|
||||||
|
private readonly logger = new Logger(MoexClientService.name);
|
||||||
|
private readonly client: AxiosInstance;
|
||||||
|
private readonly queue: PQueue;
|
||||||
|
private circuitOpen = false;
|
||||||
|
private circuitErrorCount = 0;
|
||||||
|
private readonly threshold: number;
|
||||||
|
private readonly resetMs: number;
|
||||||
|
|
||||||
|
constructor(private configService: ConfigService) {
|
||||||
|
const baseUrl = this.configService.get<string>('app.moex.baseUrl')!;
|
||||||
|
this.threshold = this.configService.get<number>(
|
||||||
|
'app.moex.circuitBreakerThreshold',
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
this.resetMs =
|
||||||
|
this.configService.get<number>(
|
||||||
|
'app.moex.circuitBreakerResetSeconds',
|
||||||
|
30,
|
||||||
|
) * 1000;
|
||||||
|
const rateLimit = this.configService.get<number>(
|
||||||
|
'app.moex.rateLimit',
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.client = axios.create({
|
||||||
|
baseURL: baseUrl,
|
||||||
|
timeout: 10000,
|
||||||
|
paramsSerializer: { indexes: null },
|
||||||
|
});
|
||||||
|
|
||||||
|
this.queue = new PQueue({
|
||||||
|
interval: 1000,
|
||||||
|
intervalCap: rateLimit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request<T>(path: string, params?: Record<string, string>): Promise<T> {
|
||||||
|
if (this.circuitOpen) {
|
||||||
|
throw new Error('Circuit breaker is open — MOEX requests paused');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.queue.add(async () => {
|
||||||
|
try {
|
||||||
|
const response = await this.client.get(path, {
|
||||||
|
params: { ...params, 'iss.meta': 'off' },
|
||||||
|
});
|
||||||
|
this.circuitErrorCount = 0;
|
||||||
|
return response.data as T;
|
||||||
|
} catch (error) {
|
||||||
|
this.circuitErrorCount++;
|
||||||
|
if (this.circuitErrorCount >= this.threshold) {
|
||||||
|
this.circuitOpen = true;
|
||||||
|
this.logger.warn(`Circuit breaker opened after ${this.threshold} errors`);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.circuitOpen = false;
|
||||||
|
this.circuitErrorCount = 0;
|
||||||
|
this.logger.log('Circuit breaker reset');
|
||||||
|
}, this.resetMs);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}) as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractTable(data: Record<string, unknown>, name: string): Record<string, unknown>[] {
|
||||||
|
const table = data[name] as Record<string, unknown> | undefined;
|
||||||
|
if (!table || !table.columns || !table.data) return [];
|
||||||
|
const columns = table.columns as string[];
|
||||||
|
const rows = table.data as unknown[][];
|
||||||
|
return rows.map((row) => {
|
||||||
|
const obj: Record<string, unknown> = {};
|
||||||
|
columns.forEach((col, i) => {
|
||||||
|
obj[col] = row[i];
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchSecurities(query: string): Promise<MoexSecurityDescription[]> {
|
||||||
|
const data = await this.request<Record<string, unknown>>('/securities', {
|
||||||
|
q: query,
|
||||||
|
});
|
||||||
|
return this.extractTable(data, 'securities').map((s) => ({
|
||||||
|
secid: s.secid as string,
|
||||||
|
isin: s.isin as string,
|
||||||
|
name: s.name as string,
|
||||||
|
shortName: s.shortName as string,
|
||||||
|
latName: (s.latName as string) || null,
|
||||||
|
listLevel: parseInt(s.listLevel as string, 10) || 0,
|
||||||
|
issueSize: parseInt(s.issuesize as string, 10) || 0,
|
||||||
|
faceValue: parseFloat(s.facevalue as string) || 0,
|
||||||
|
faceUnit: (s.faceunit as string) || '',
|
||||||
|
issueDate: (s.issuedate as string) || '',
|
||||||
|
typeName: (s.typename as string) || '',
|
||||||
|
group: (s.group as string) || '',
|
||||||
|
type: (s.type as string) || '',
|
||||||
|
isQualifiedInvestors: (s.isqualifiedinvestors as string) === '1',
|
||||||
|
morningSession: (s.morningsession as string) === '1',
|
||||||
|
eveningSession: (s.eveningsession as string) === '1',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSecurityDescription(secid: string): Promise<MoexSecurityDescription | null> {
|
||||||
|
const data = await this.request<Record<string, unknown>>(`/securities/${secid}`);
|
||||||
|
const rows = this.extractTable(data, 'description');
|
||||||
|
if (rows.length === 0) return null;
|
||||||
|
const map = new Map(rows.map((r) => [r.name, r.value]));
|
||||||
|
return {
|
||||||
|
secid,
|
||||||
|
isin: (map.get('ISIN') as string) || '',
|
||||||
|
name: (map.get('NAME') as string) || '',
|
||||||
|
shortName: (map.get('SHORTNAME') as string) || '',
|
||||||
|
latName: (map.get('LATNAME') as string) || null,
|
||||||
|
listLevel: parseInt((map.get('LISTLEVEL') as string) || '0', 10),
|
||||||
|
issueSize: parseInt((map.get('ISSUESIZE') as string) || '0', 10),
|
||||||
|
faceValue: parseFloat((map.get('FACEVALUE') as string) || '0'),
|
||||||
|
faceUnit: (map.get('FACEUNIT') as string) || '',
|
||||||
|
issueDate: (map.get('ISSUEDATE') as string) || '',
|
||||||
|
typeName: (map.get('TYPENAME') as string) || '',
|
||||||
|
group: (map.get('GROUP') as string) || '',
|
||||||
|
type: (map.get('TYPE') as string) || '',
|
||||||
|
isQualifiedInvestors: (map.get('ISQUALIFIEDINVESTORS') as string) === '1',
|
||||||
|
morningSession: (map.get('MORNINGSESSION') as string) === '1',
|
||||||
|
eveningSession: (map.get('EVENINGSESSION') as string) === '1',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getShareMarketData(secid: string, boardId = 'TQBR'): Promise<MoexShareMarketData | null> {
|
||||||
|
const data = await this.request<Record<string, unknown>>(
|
||||||
|
`/engines/stock/markets/shares/securities/${secid}`,
|
||||||
|
{ boards: boardId },
|
||||||
|
);
|
||||||
|
const rows = this.extractTable(data, 'securities');
|
||||||
|
const share = rows.find((r) => r.BOARDID === boardId);
|
||||||
|
if (!share) return null;
|
||||||
|
|
||||||
|
const mktRows = this.extractTable(data, 'marketdata');
|
||||||
|
const mkt = mktRows.find((r) => r.BOARDID === boardId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
secid,
|
||||||
|
boardid: boardId,
|
||||||
|
bid: mkt ? parseFloat((mkt.BID as string) || '') : null,
|
||||||
|
offer: mkt ? parseFloat((mkt.OFFER as string) || '') : null,
|
||||||
|
open: mkt ? parseFloat((mkt.OPEN as string) || '') : null,
|
||||||
|
low: mkt ? parseFloat((mkt.LOW as string) || '') : null,
|
||||||
|
high: mkt ? parseFloat((mkt.HIGH as string) || '') : null,
|
||||||
|
last: mkt ? parseFloat((mkt.LAST as string) || '') : parseFloat((share.PREVPRICE as string) || ''),
|
||||||
|
lastChange: mkt ? parseFloat((mkt.LASTCHANGE as string) || '') : null,
|
||||||
|
lastChangePrcnt: mkt ? parseFloat((mkt.LASTCHANGEPRCNT as string) || '') : null,
|
||||||
|
volume: mkt ? parseInt((mkt.VOLTODAY as string) || '0', 10) : 0,
|
||||||
|
value: mkt ? parseFloat((mkt.VALTODAY as string) || '0') : 0,
|
||||||
|
waprice: mkt ? parseFloat((mkt.WAPRICE as string) || '') : null,
|
||||||
|
numtrades: mkt ? parseInt((mkt.NUMTRADES as string) || '0', 10) : 0,
|
||||||
|
issueCapitalization: mkt ? parseFloat((mkt.ISSUECAPITALIZATION as string) || '') : null,
|
||||||
|
tradingStatus: (mkt?.TRADINGSTATUS as string) || '',
|
||||||
|
updateTime: (mkt?.UPDATETIME as string) || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBondData(secid: string, boardId = 'TQCB'): Promise<MoexBondData | null> {
|
||||||
|
const data = await this.request<Record<string, unknown>>(
|
||||||
|
`/engines/stock/markets/bonds/securities/${secid}`,
|
||||||
|
{ boards: boardId },
|
||||||
|
);
|
||||||
|
const rows = this.extractTable(data, 'securities');
|
||||||
|
const bond = rows.find((r) => r.BOARDID === boardId);
|
||||||
|
if (!bond) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
secid,
|
||||||
|
boardid: boardId,
|
||||||
|
shortName: (bond.SHORTNAME as string) || '',
|
||||||
|
prevWaprice: parseFloat((bond.PREVWAPRICE as string) || '') || null,
|
||||||
|
yieldAtPrevWaprice: parseFloat((bond.YIELDATPREVWAPRICE as string) || '') || null,
|
||||||
|
couponValue: bond.COUPONVALUE != null ? parseFloat(bond.COUPONVALUE as string) : null,
|
||||||
|
nextCoupon: (bond.NEXTCOUPON as string) || null,
|
||||||
|
accruedInt: bond.ACCRUEDINT != null ? parseFloat(bond.ACCRUEDINT as string) : null,
|
||||||
|
prevPrice: parseFloat((bond.PREVPRICE as string) || '') || null,
|
||||||
|
lotSize: parseInt((bond.LOTSIZE as string) || '1', 10),
|
||||||
|
faceValue: parseFloat((bond.FACEVALUE as string) || '1000'),
|
||||||
|
matDate: (bond.MATDATE as string) || '',
|
||||||
|
couponPeriod: parseInt((bond.COUPONPERIOD as string) || '0', 10),
|
||||||
|
issueSize: parseInt((bond.ISSUESIZE as string) || '0', 10),
|
||||||
|
isin: (bond.ISIN as string) || '',
|
||||||
|
couponPercent: bond.COUPONPERCENT != null ? parseFloat(bond.COUPONPERCENT as string) : null,
|
||||||
|
offerDate: (bond.OFFERDATE as string) || null,
|
||||||
|
buybackDate: (bond.BUYBACKDATE as string) || null,
|
||||||
|
bondType: (bond.BONDTYPE as string) || '',
|
||||||
|
bondSubType: (bond.BONDSUBTYPE as string) || '',
|
||||||
|
listLevel: parseInt((bond.LISTLEVEL as string) || '0', 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBondMarketData(secid: string, boardId = 'TQCB'): Promise<MoexBondMarketData | null> {
|
||||||
|
const data = await this.request<Record<string, unknown>>(
|
||||||
|
`/engines/stock/markets/bonds/securities/${secid}`,
|
||||||
|
{ boards: boardId },
|
||||||
|
);
|
||||||
|
const mktRows = this.extractTable(data, 'marketdata');
|
||||||
|
const mkt = mktRows.find((r) => r.SECID === secid);
|
||||||
|
if (!mkt) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
secid,
|
||||||
|
bid: mkt.BID != null ? parseFloat(mkt.BID as string) : null,
|
||||||
|
offer: mkt.OFFER != null ? parseFloat(mkt.OFFER as string) : null,
|
||||||
|
open: mkt.OPEN != null ? parseFloat(mkt.OPEN as string) : null,
|
||||||
|
low: mkt.LOW != null ? parseFloat(mkt.LOW as string) : null,
|
||||||
|
high: mkt.HIGH != null ? parseFloat(mkt.HIGH as string) : null,
|
||||||
|
last: mkt.LAST != null ? parseFloat(mkt.LAST as string) : null,
|
||||||
|
yield: mkt.YIELD != null ? parseFloat(mkt.YIELD as string) : null,
|
||||||
|
waprice: mkt.WAPRICE != null ? parseFloat(mkt.WAPRICE as string) : null,
|
||||||
|
yieldAtWaprice: mkt.YIELDATWAPRICE != null ? parseFloat(mkt.YIELDATWAPRICE as string) : null,
|
||||||
|
duration: mkt.DURATION != null ? parseFloat(mkt.DURATION as string) : null,
|
||||||
|
volume: parseInt((mkt.VOLTODAY as string) || '0', 10),
|
||||||
|
value: parseFloat((mkt.VALTODAY as string) || '0'),
|
||||||
|
numtrades: parseInt((mkt.NUMTRADES as string) || '0', 10),
|
||||||
|
tradingStatus: (mkt.TRADINGSTATUS as string) || '',
|
||||||
|
updateTime: (mkt.UPDATETIME as string) || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDividends(secid: string): Promise<MoexDividend[]> {
|
||||||
|
const data = await this.request<Record<string, unknown>>(`/securities/${secid}/dividends`);
|
||||||
|
return this.extractTable(data, 'dividends').map((d) => ({
|
||||||
|
secid: d.secid as string,
|
||||||
|
isin: d.isin as string,
|
||||||
|
registryCloseDate: d.registryclosedate as string,
|
||||||
|
value: parseFloat(d.value as string),
|
||||||
|
currencyId: (d.currencyid as string) || 'RUB',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCandles(
|
||||||
|
engine: 'stock',
|
||||||
|
market: 'shares' | 'bonds',
|
||||||
|
secid: string,
|
||||||
|
interval: 1 | 10 | 60 | 24,
|
||||||
|
from: string,
|
||||||
|
till: string,
|
||||||
|
): Promise<MoexCandle[]> {
|
||||||
|
const data = await this.request<Record<string, unknown>>(
|
||||||
|
`/engines/${engine}/markets/${market}/securities/${secid}/candles`,
|
||||||
|
{
|
||||||
|
interval: String(interval),
|
||||||
|
from,
|
||||||
|
till,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return this.extractTable(data, 'candles').map((c) => ({
|
||||||
|
open: parseFloat(c.open as string),
|
||||||
|
close: parseFloat(c.close as string),
|
||||||
|
high: parseFloat(c.high as string),
|
||||||
|
low: parseFloat(c.low as string),
|
||||||
|
value: parseFloat(c.value as string),
|
||||||
|
volume: parseInt(c.volume as string, 10),
|
||||||
|
begin: c.begin as string,
|
||||||
|
end: c.end as string,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHistory(
|
||||||
|
secid: string,
|
||||||
|
from: string,
|
||||||
|
till: string,
|
||||||
|
): Promise<MoexHistoryEntry[]> {
|
||||||
|
const data = await this.request<Record<string, unknown>>(
|
||||||
|
`/engines/stock/markets/shares/securities/${secid}`,
|
||||||
|
{ from, till },
|
||||||
|
);
|
||||||
|
const tableName = Object.keys(data).find(
|
||||||
|
(k) => k.startsWith('history') && !k.includes('cursor'),
|
||||||
|
);
|
||||||
|
if (!tableName) return [];
|
||||||
|
return this.extractTable(data, tableName).map((h) => ({
|
||||||
|
tradeDate: h.TRADEDATE as string,
|
||||||
|
open: h.OPEN != null ? parseFloat(h.OPEN as string) : null,
|
||||||
|
low: h.LOW != null ? parseFloat(h.LOW as string) : null,
|
||||||
|
high: h.HIGH != null ? parseFloat(h.HIGH as string) : null,
|
||||||
|
close: h.CLOSE != null ? parseFloat(h.CLOSE as string) : null,
|
||||||
|
waprice: h.WAPRICE != null ? parseFloat(h.WAPRICE as string) : null,
|
||||||
|
volume: parseInt((h.VOLUME as string) || '0', 10),
|
||||||
|
value: parseFloat((h.VALUE as string) || '0'),
|
||||||
|
numtrades: parseInt((h.NUMTRADES as string) || '0', 10),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBondHistory(
|
||||||
|
secid: string,
|
||||||
|
from: string,
|
||||||
|
till: string,
|
||||||
|
): Promise<MoexBondHistoryEntry[]> {
|
||||||
|
const data = await this.request<Record<string, unknown>>(
|
||||||
|
`/engines/stock/markets/bonds/securities/${secid}`,
|
||||||
|
{ from, till },
|
||||||
|
);
|
||||||
|
const tableName = Object.keys(data).find(
|
||||||
|
(k) => k.startsWith('history') && !k.includes('cursor'),
|
||||||
|
);
|
||||||
|
if (!tableName) return [];
|
||||||
|
return this.extractTable(data, tableName).map((h) => ({
|
||||||
|
tradeDate: h.TRADEDATE as string,
|
||||||
|
close: h.CLOSE != null ? parseFloat(h.CLOSE as string) : null,
|
||||||
|
legalClosePrice:
|
||||||
|
h.LEGALCLOSEPRICE != null ? parseFloat(h.LEGALCLOSEPRICE as string) : null,
|
||||||
|
waprice: h.WAPRICE != null ? parseFloat(h.WAPRICE as string) : null,
|
||||||
|
yieldClose: h.YIELDCLOSE != null ? parseFloat(h.YIELDCLOSE as string) : null,
|
||||||
|
duration: h.DURATION != null ? parseFloat(h.DURATION as string) : null,
|
||||||
|
accruedInt: h.ACCINT != null ? parseFloat(h.ACCINT as string) : null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
131
apps/backend/src/modules/moex-client/moex-client.types.ts
Normal file
131
apps/backend/src/modules/moex-client/moex-client.types.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
export interface MoexSecurityDescription {
|
||||||
|
secid: string;
|
||||||
|
isin: string;
|
||||||
|
name: string;
|
||||||
|
shortName: string;
|
||||||
|
latName: string | null;
|
||||||
|
listLevel: number;
|
||||||
|
issueSize: number;
|
||||||
|
faceValue: number;
|
||||||
|
faceUnit: string;
|
||||||
|
issueDate: string;
|
||||||
|
typeName: string;
|
||||||
|
group: string;
|
||||||
|
type: string;
|
||||||
|
isQualifiedInvestors: boolean;
|
||||||
|
morningSession: boolean;
|
||||||
|
eveningSession: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoexShareMarketData {
|
||||||
|
secid: string;
|
||||||
|
boardid: string;
|
||||||
|
bid: number | null;
|
||||||
|
offer: number | null;
|
||||||
|
open: number | null;
|
||||||
|
low: number | null;
|
||||||
|
high: number | null;
|
||||||
|
last: number | null;
|
||||||
|
lastChange: number | null;
|
||||||
|
lastChangePrcnt: number | null;
|
||||||
|
volume: number;
|
||||||
|
value: number;
|
||||||
|
waprice: number | null;
|
||||||
|
numtrades: number;
|
||||||
|
issueCapitalization: number | null;
|
||||||
|
tradingStatus: string;
|
||||||
|
updateTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoexBondData {
|
||||||
|
secid: string;
|
||||||
|
boardid: string;
|
||||||
|
shortName: string;
|
||||||
|
prevWaprice: number | null;
|
||||||
|
yieldAtPrevWaprice: number | null;
|
||||||
|
couponValue: number | null;
|
||||||
|
nextCoupon: string | null;
|
||||||
|
accruedInt: number | null;
|
||||||
|
prevPrice: number | null;
|
||||||
|
lotSize: number;
|
||||||
|
faceValue: number;
|
||||||
|
matDate: string;
|
||||||
|
couponPeriod: number;
|
||||||
|
issueSize: number;
|
||||||
|
isin: string;
|
||||||
|
couponPercent: number | null;
|
||||||
|
offerDate: string | null;
|
||||||
|
buybackDate: string | null;
|
||||||
|
bondType: string;
|
||||||
|
bondSubType: string;
|
||||||
|
listLevel: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoexBondMarketData {
|
||||||
|
secid: string;
|
||||||
|
bid: number | null;
|
||||||
|
offer: number | null;
|
||||||
|
open: number | null;
|
||||||
|
low: number | null;
|
||||||
|
high: number | null;
|
||||||
|
last: number | null;
|
||||||
|
yield: number | null;
|
||||||
|
waprice: number | null;
|
||||||
|
yieldAtWaprice: number | null;
|
||||||
|
duration: number | null;
|
||||||
|
volume: number;
|
||||||
|
value: number;
|
||||||
|
numtrades: number;
|
||||||
|
tradingStatus: string;
|
||||||
|
updateTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoexDividend {
|
||||||
|
secid: string;
|
||||||
|
isin: string;
|
||||||
|
registryCloseDate: string;
|
||||||
|
value: number;
|
||||||
|
currencyId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoexCandle {
|
||||||
|
open: number;
|
||||||
|
close: number;
|
||||||
|
high: number;
|
||||||
|
low: number;
|
||||||
|
value: number;
|
||||||
|
volume: number;
|
||||||
|
begin: string;
|
||||||
|
end: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoexHistoryEntry {
|
||||||
|
tradeDate: string;
|
||||||
|
open: number | null;
|
||||||
|
low: number | null;
|
||||||
|
high: number | null;
|
||||||
|
close: number | null;
|
||||||
|
waprice: number | null;
|
||||||
|
volume: number;
|
||||||
|
value: number;
|
||||||
|
numtrades: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoexBondHistoryEntry {
|
||||||
|
tradeDate: string;
|
||||||
|
close: number | null;
|
||||||
|
legalClosePrice: number | null;
|
||||||
|
waprice: number | null;
|
||||||
|
yieldClose: number | null;
|
||||||
|
duration: number | null;
|
||||||
|
accruedInt: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoexBoard {
|
||||||
|
secid: string;
|
||||||
|
boardid: string;
|
||||||
|
title: string;
|
||||||
|
isPrimary: boolean;
|
||||||
|
isTraded: boolean;
|
||||||
|
currencyid: string;
|
||||||
|
}
|
||||||
@ -10,7 +10,8 @@
|
|||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"]
|
||||||
}
|
},
|
||||||
|
"types": ["vitest/globals"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"]
|
"include": ["src/**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
22
apps/backend/vitest.config.ts
Normal file
22
apps/backend/vitest.config.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import swc from 'unplugin-swc';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
swc.vite({
|
||||||
|
jsc: {
|
||||||
|
target: 'es2022',
|
||||||
|
parser: {
|
||||||
|
syntax: 'typescript',
|
||||||
|
decorators: true,
|
||||||
|
},
|
||||||
|
transform: {
|
||||||
|
decoratorMetadata: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
379
package-lock.json
generated
379
package-lock.json
generated
@ -37,12 +37,14 @@
|
|||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.0.0",
|
||||||
"@nestjs/testing": "^10.0.0",
|
"@nestjs/testing": "^10.0.0",
|
||||||
|
"@swc/core": "^1.15.41",
|
||||||
"@types/express": "^4.17.0",
|
"@types/express": "^4.17.0",
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
"@typescript-eslint/parser": "^7.0.0",
|
"@typescript-eslint/parser": "^7.0.0",
|
||||||
"eslint": "^8.0.0",
|
"eslint": "^8.0.0",
|
||||||
"typescript": "^5.3.0",
|
"typescript": "^5.3.0",
|
||||||
|
"unplugin-swc": "^1.5.9",
|
||||||
"vitest": "^1.0.0"
|
"vitest": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -943,6 +945,17 @@
|
|||||||
"@jridgewell/trace-mapping": "^0.3.24"
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jridgewell/remapping": {
|
||||||
|
"version": "2.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||||
|
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/resolve-uri": {
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
@ -1576,6 +1589,49 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@rollup/pluginutils": {
|
||||||
|
"version": "5.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz",
|
||||||
|
"integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "^1.0.0",
|
||||||
|
"estree-walker": "^2.0.2",
|
||||||
|
"picomatch": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/pluginutils/node_modules/estree-walker": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/pluginutils/node_modules/picomatch": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.62.0",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz",
|
||||||
@ -1933,6 +1989,268 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@swc/core": {
|
||||||
|
"version": "1.15.41",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.41.tgz",
|
||||||
|
"integrity": "sha512-03nQq/082QRJJiOvp3FGbgxTGyyxMxohPTjhk/W9bD2J0tk4ukITI7goOhOO2WbaHn/lsPmo/zf8+DIXhwpgYQ==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@swc/counter": "^0.1.3",
|
||||||
|
"@swc/types": "^0.1.26"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/swc"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@swc/core-darwin-arm64": "1.15.41",
|
||||||
|
"@swc/core-darwin-x64": "1.15.41",
|
||||||
|
"@swc/core-linux-arm-gnueabihf": "1.15.41",
|
||||||
|
"@swc/core-linux-arm64-gnu": "1.15.41",
|
||||||
|
"@swc/core-linux-arm64-musl": "1.15.41",
|
||||||
|
"@swc/core-linux-ppc64-gnu": "1.15.41",
|
||||||
|
"@swc/core-linux-s390x-gnu": "1.15.41",
|
||||||
|
"@swc/core-linux-x64-gnu": "1.15.41",
|
||||||
|
"@swc/core-linux-x64-musl": "1.15.41",
|
||||||
|
"@swc/core-win32-arm64-msvc": "1.15.41",
|
||||||
|
"@swc/core-win32-ia32-msvc": "1.15.41",
|
||||||
|
"@swc/core-win32-x64-msvc": "1.15.41"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@swc/helpers": ">=0.5.17"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@swc/helpers": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-darwin-arm64": {
|
||||||
|
"version": "1.15.41",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.41.tgz",
|
||||||
|
"integrity": "sha512-kREh6J5paQFvP3i7f/4FbqRNOJREutVFVOkder4GVyCBQ39YmER55cW/y1NNjwrchzFqgYswFn0mMDCqbqKzrw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-darwin-x64": {
|
||||||
|
"version": "1.15.41",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.41.tgz",
|
||||||
|
"integrity": "sha512-N8B56ESFazZAWZyIkecADSPCwlLEinW7QLMEeotCpv4J7VXwfH+OLkmRL8o96UZ+1355fwHxDTS6/wK7yucvkA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.15.41",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.41.tgz",
|
||||||
|
"integrity": "sha512-6XrId2fyle0mS5xxON8rU84mPd2Cq1kDJRj+4BnQKTd7u+2kSA6Ww+JkOP0iTNqOqt9OXhPOEAjBHAuonWcdCg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||||
|
"version": "1.15.41",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.41.tgz",
|
||||||
|
"integrity": "sha512-ynLIarxlkVnqHn1D0fKOVht6mNU5ks6lrH+MY3kkS+XFaGGgDxFZVjWKJlkYTKm3RCvBTfA8Ng5fLufXheMRKQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-linux-arm64-musl": {
|
||||||
|
"version": "1.15.41",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.41.tgz",
|
||||||
|
"integrity": "sha512-dXu/5vd4gh8symyhRF+4G7gOPkjmb4pONhh7sl+6GSiW0LOKZlfu5kXmyFbTz9smOT7jgr002qY9b1nujjXt2A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-linux-ppc64-gnu": {
|
||||||
|
"version": "1.15.41",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.41.tgz",
|
||||||
|
"integrity": "sha512-XGO6zVPXoPE0gf/XnI4jBbafNT13AYgoh6ns0JCSdOetI/kqVf0vhpz7NuNgAzZrMVCsmieqjPoTwViDgh4mOQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-linux-s390x-gnu": {
|
||||||
|
"version": "1.15.41",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.41.tgz",
|
||||||
|
"integrity": "sha512-0WUglRwyZtW+iMi7J3iFdrCxreZZIKf4egTwEQfIYRsqFax69A0OrFj+NIoFSE03xBT/IFRrg+S8K6f9Ky+4hA==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-linux-x64-gnu": {
|
||||||
|
"version": "1.15.41",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.41.tgz",
|
||||||
|
"integrity": "sha512-VxkuQK59c0tHm6uJZCUrS3cyA2JhGGfdU6e41SZz0x/JS+4Sm7C1mIc97In14vkZJopEt7yXA2TouCqZDSygEA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-linux-x64-musl": {
|
||||||
|
"version": "1.15.41",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.41.tgz",
|
||||||
|
"integrity": "sha512-/0qXIu1ZxggLuovLb22vFfKHq2AA4n6Whw5UwmVCHk4pkw7KWnPIQpMCEqUMPsNkFJig7PPp/TSYFu8ZEb2rtQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-win32-arm64-msvc": {
|
||||||
|
"version": "1.15.41",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.41.tgz",
|
||||||
|
"integrity": "sha512-Y481sMNZM6rECh9VO4+y26N1lWEDAyxnBZskUf37fl90uHE946VHfmiVQWT0uMFOhyJJFovGTRuF4W82dwewUg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-win32-ia32-msvc": {
|
||||||
|
"version": "1.15.41",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.41.tgz",
|
||||||
|
"integrity": "sha512-BAchBD5qeUzy3hiPSLJtaaoSm4blCLyYffOF1bGE4ETcV+OisqjUAwDQMJj++4bTpvMCDzwC+Bj3PmQyBCtscw==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/core-win32-x64-msvc": {
|
||||||
|
"version": "1.15.41",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.41.tgz",
|
||||||
|
"integrity": "sha512-WOkA+fJ/ViVBQDsSV9JC52NACTe5PhlurA6viASDZGb7HR3KS01ZG7RZ+Bg6SVQFIoq3gSbTsskQVe6EbHFAYw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@swc/counter": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@swc/types": {
|
||||||
|
"version": "0.1.26",
|
||||||
|
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz",
|
||||||
|
"integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@swc/counter": "^0.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tokenizer/inflate": {
|
"node_modules/@tokenizer/inflate": {
|
||||||
"version": "0.2.7",
|
"version": "0.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz",
|
||||||
@ -5460,6 +5778,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/load-tsconfig": {
|
||||||
|
"version": "0.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz",
|
||||||
|
"integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/loader-runner": {
|
"node_modules/loader-runner": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz",
|
||||||
@ -7740,6 +8068,50 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/unplugin": {
|
||||||
|
"version": "2.3.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
||||||
|
"integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/remapping": "^2.3.5",
|
||||||
|
"acorn": "^8.15.0",
|
||||||
|
"picomatch": "^4.0.3",
|
||||||
|
"webpack-virtual-modules": "^0.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unplugin-swc": {
|
||||||
|
"version": "1.5.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/unplugin-swc/-/unplugin-swc-1.5.9.tgz",
|
||||||
|
"integrity": "sha512-RKwK3yf0M+MN17xZfF14bdKqfx0zMXYdtOdxLiE6jHAoidupKq3jGdJYANyIM1X/VmABhh1WpdO+/f4+Ol89+g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@rollup/pluginutils": "^5.3.0",
|
||||||
|
"load-tsconfig": "^0.2.5",
|
||||||
|
"unplugin": "^2.3.11"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@swc/core": "^1.2.108"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unplugin/node_modules/picomatch": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||||
@ -8059,6 +8431,13 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/webpack-virtual-modules": {
|
||||||
|
"version": "0.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
|
||||||
|
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/webpack/node_modules/eslint-scope": {
|
"node_modules/webpack/node_modules/eslint-scope": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user