first commit
This commit is contained in:
14
src/edge/dto/edge-response.dto.ts
Normal file
14
src/edge/dto/edge-response.dto.ts
Normal 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[];
|
||||
};
|
||||
15
src/edge/dto/get-edges.dto.ts
Normal file
15
src/edge/dto/get-edges.dto.ts
Normal 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;
|
||||
}
|
||||
17
src/edge/edge.controller.ts
Normal file
17
src/edge/edge.controller.ts
Normal 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
18
src/edge/edge.mapper.ts
Normal 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
10
src/edge/edge.module.ts
Normal 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 {}
|
||||
42
src/edge/edge.repository.ts
Normal file
42
src/edge/edge.repository.ts
Normal 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
18
src/edge/edge.service.ts
Normal 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
10
src/edge/edge.types.ts
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user