93 lines
3.3 KiB
TypeScript
93 lines
3.3 KiB
TypeScript
import { BadRequestException, Injectable } from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import { getIntegerConfig } from '../common/config-number';
|
|
import { normalizeRequiredText } from '../common/normalize-text';
|
|
import { GetHistoryDto } from './dto/get-history.dto';
|
|
import { chooseHistorySource } from './history-source';
|
|
import { createHistoryResponse } from './history.mapper';
|
|
import { HistoryRepository } from './history.repository';
|
|
import { HistoryResponse } from './history.types';
|
|
|
|
@Injectable()
|
|
export class HistoryService {
|
|
constructor(
|
|
private readonly repository: HistoryRepository,
|
|
private readonly config: ConfigService,
|
|
) {}
|
|
|
|
// Use-case для графиков: выбирает latest/raw/aggregate и возвращает единый API-формат.
|
|
async findSeries(query: GetHistoryDto): Promise<HistoryResponse> {
|
|
const edge = normalizeRequiredText(query.edge, 'edge');
|
|
const targetPoints = this.getTargetPoints(query.targetPoints);
|
|
const tags = await this.repository.resolveTags(edge, query.tags);
|
|
|
|
if (!tags.length) {
|
|
return createHistoryResponse({
|
|
edge,
|
|
source: 'empty',
|
|
targetPoints,
|
|
});
|
|
}
|
|
|
|
if (!query.from && !query.to) {
|
|
const rows = await this.repository.findLatest(edge, tags, targetPoints);
|
|
return createHistoryResponse({
|
|
edge,
|
|
source: 'latest',
|
|
targetPoints,
|
|
rows,
|
|
});
|
|
}
|
|
|
|
const { from, to } = this.parseRange(query);
|
|
const source = chooseHistorySource(from, to, targetPoints);
|
|
const rows =
|
|
source.name === 'raw'
|
|
? await this.repository.findRawRange(edge, tags, from, to, targetPoints)
|
|
: await this.repository.findAggregateRange(
|
|
edge,
|
|
tags,
|
|
from,
|
|
to,
|
|
targetPoints,
|
|
source,
|
|
query.valueMode ?? 'avg',
|
|
);
|
|
|
|
return createHistoryResponse({
|
|
edge,
|
|
from,
|
|
to,
|
|
source: source.name,
|
|
resolutionSeconds: source.intervalSeconds || null,
|
|
targetPoints,
|
|
valueMode: source.name === 'raw' ? 'raw' : query.valueMode ?? 'avg',
|
|
rows,
|
|
});
|
|
}
|
|
|
|
// Частично заданный диапазон поддерживается: open-ended сторона получает безопасный default.
|
|
private parseRange(query: GetHistoryDto): { from: Date; to: Date } {
|
|
const from = query.from ? new Date(query.from) : new Date(0);
|
|
const to = query.to ? new Date(query.to) : new Date();
|
|
|
|
if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime())) {
|
|
throw new BadRequestException('Invalid from/to date.');
|
|
}
|
|
|
|
if (from > to) {
|
|
throw new BadRequestException('from cannot be greater than to.');
|
|
}
|
|
|
|
return { from, to };
|
|
}
|
|
|
|
// Ограничивает плотность ответа, чтобы API не возвращал больше точек, чем способен отрисовать UI.
|
|
private getTargetPoints(requested?: number): number {
|
|
const defaultValue = getIntegerConfig(this.config, 'HISTORY_TARGET_POINTS', 2000);
|
|
const maxValue = getIntegerConfig(this.config, 'HISTORY_MAX_TARGET_POINTS', 2000);
|
|
const value = requested ?? defaultValue;
|
|
return Math.min(Math.max(Math.floor(value), 100), maxValue);
|
|
}
|
|
}
|