first commit

This commit is contained in:
Первов Артем
2026-06-19 08:18:52 +03:00
commit 05a9e0e535
61 changed files with 11950 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
export type EdgeItemDto = {
id: string;
name: string;
parentId: string | null;
tagIds: string[];
tagCount: number;
currentTagCount: number;
liveTagCount: number;
lastDataAt: Date | null;
};
export type EdgeResponseDto = {
items: EdgeItemDto[];
};

View File

@@ -0,0 +1,15 @@
import { Transform } from 'class-transformer';
import { IsOptional, IsString } from 'class-validator';
import { normalizeOptionalText } from '../../common/normalize-text';
export class GetEdgesDto {
@IsOptional()
@Transform(({ value }) => normalizeOptionalText(value))
@IsString()
parentId?: string;
@IsOptional()
@Transform(({ value }) => normalizeOptionalText(value))
@IsString()
search?: string;
}

View File

@@ -0,0 +1,17 @@
import { Controller, Get, Header, Query } from '@nestjs/common';
import { GetEdgesDto } from './dto/get-edges.dto';
import { EdgeService } from './edge.service';
@Controller('edge')
export class EdgeController {
constructor(private readonly edge: EdgeService) {}
/** Возвращает список edge с опциональными фильтрами по parent и тексту. */
@Get()
@Header('Cache-Control', 'no-store')
@Header('Pragma', 'no-cache')
@Header('Expires', '0')
findAll(@Query() query: GetEdgesDto) {
return this.edge.findAll(query);
}
}

18
src/edge/edge.mapper.ts Normal file
View File

@@ -0,0 +1,18 @@
import { EdgeResponseDto } from './dto/edge-response.dto';
import { EdgeRow } from './edge.types';
/** Преобразует строки БД edge в стабильный DTO для API. */
export function createEdgeResponse(rows: EdgeRow[]): EdgeResponseDto {
return {
items: rows.map((row) => ({
id: row.id,
name: row.name,
parentId: row.parent_id,
tagIds: row.tag_ids ?? [],
tagCount: row.tag_count,
currentTagCount: row.current_tag_count,
liveTagCount: row.live_tag_count,
lastDataAt: row.last_data_at,
})),
};
}

10
src/edge/edge.module.ts Normal file
View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { EdgeController } from './edge.controller';
import { EdgeRepository } from './edge.repository';
import { EdgeService } from './edge.service';
@Module({
controllers: [EdgeController],
providers: [EdgeService, EdgeRepository],
})
export class EdgeModule {}

View File

@@ -0,0 +1,42 @@
import { Injectable } from '@nestjs/common';
import { DbService } from '../db/db.service';
import { EdgeRow } from './edge.types';
@Injectable()
export class EdgeRepository {
constructor(private readonly db: DbService) {}
/** Читает каталог edge с легкими счетчиками свежести из текущих значений. */
async findAll(parentId: string | null, search: string | null): Promise<EdgeRow[]> {
const result = await this.db.query<EdgeRow>(
`
SELECT
e.id,
e.name,
e.parent_id,
e.tag_ids,
cardinality(e.tag_ids)::integer AS tag_count,
count(DISTINCT c.tag)::integer AS current_tag_count,
count(DISTINCT c.tag) FILTER (
WHERE c."updatedAt" >= now() - INTERVAL '30 seconds'
)::integer AS live_tag_count,
max(c."updatedAt") AS last_data_at
FROM edge AS e
LEFT JOIN current AS c
ON c.edge = e.id
AND (cardinality(e.tag_ids) = 0 OR c.tag = ANY(e.tag_ids))
WHERE ($1::varchar IS NULL OR e.parent_id = $1::varchar)
AND (
$2::text IS NULL
OR e.id ILIKE $2::text
OR e.name ILIKE $2::text
)
GROUP BY e.id, e.name, e.parent_id, e.tag_ids
ORDER BY e.name ASC, e.id ASC
`,
[parentId, search],
);
return result.rows;
}
}

18
src/edge/edge.service.ts Normal file
View File

@@ -0,0 +1,18 @@
import { Injectable } from '@nestjs/common';
import { EdgeResponseDto } from './dto/edge-response.dto';
import { GetEdgesDto } from './dto/get-edges.dto';
import { createEdgeResponse } from './edge.mapper';
import { EdgeRepository } from './edge.repository';
@Injectable()
export class EdgeService {
constructor(private readonly repository: EdgeRepository) {}
/** Нормализует фильтры и возвращает каталог edge для UI. */
async findAll(query: GetEdgesDto = {}): Promise<EdgeResponseDto> {
const parentId = query.parentId ?? null;
const search = query.search ? `%${query.search}%` : null;
const rows = await this.repository.findAll(parentId, search);
return createEdgeResponse(rows);
}
}

10
src/edge/edge.types.ts Normal file
View File

@@ -0,0 +1,10 @@
export type EdgeRow = {
id: string;
name: string;
parent_id: string | null;
tag_ids: string[];
tag_count: number;
current_tag_count: number;
live_tag_count: number;
last_data_at: Date | null;
};