8 Commits

14 changed files with 127 additions and 54 deletions

19
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,19 @@
services:
drill-cloud-v3-dev:
container_name: container-drill-cloud-v3${branch:+-${branch}}
build: .
restart: unless-stopped
environment:
NODE_ENV: production
PORT: ${PORT}
DATABASE_URL: ${DATABASE_URL}
INGEST_API_KEY: ${INGEST_API_KEY}
CURRENT_EVENTS_POLL_MS: ${CURRENT_EVENTS_POLL_MS}
expose:
- "3101"
networks:
- proxy
networks:
proxy:
external: true

View File

@@ -1,6 +1,6 @@
services:
drill-cloud-v3:
container_name: drill-cloud-v3${branch:+-${branch}}
container_name: container-drill-cloud-v3${branch:+-${branch}}
build: .
restart: unless-stopped
environment:
@@ -8,7 +8,6 @@ services:
PORT: ${PORT}
DATABASE_URL: ${DATABASE_URL}
INGEST_API_KEY: ${INGEST_API_KEY}
INGEST_DEBUG_LOG: ${INGEST_DEBUG_LOG:-}
CURRENT_EVENTS_POLL_MS: ${CURRENT_EVENTS_POLL_MS}
expose:
- "3101"

View File

@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS camera (
edge varchar NOT NULL,
protocol varchar NOT NULL,
source varchar NOT NULL,
PRIMARY KEY (edge, protocol, source)
);
CREATE INDEX IF NOT EXISTS camera_edge_idx ON camera (edge);

View File

@@ -1,4 +1,5 @@
import { Module } from '@nestjs/common';
import { CameraModule } from './camera/camera.module';
import { CurrentModule } from './current/current.module';
import { DbModule } from './db/db.module';
import { EdgeModule } from './edge/edge.module';
@@ -10,6 +11,7 @@ import { TagModule } from './tag/tag.module';
@Module({
imports: [
DbModule,
CameraModule,
IngestModule,
EdgeModule,
CurrentModule,

View File

@@ -0,0 +1,16 @@
import { Controller, Get, Header, Query } from '@nestjs/common';
import { CameraService } from './camera.service';
import { GetCamerasDto } from './dto/get-cameras.dto';
@Controller('camera')
export class CameraController {
constructor(private readonly camera: CameraService) {}
@Get()
@Header('Cache-Control', 'no-store')
@Header('Pragma', 'no-cache')
@Header('Expires', '0')
findByEdge(@Query() query: GetCamerasDto) {
return this.camera.findByEdge(query);
}
}

View File

@@ -0,0 +1,12 @@
import { CameraResponseDto } from './dto/camera-response.dto';
import { CameraRow } from './camera.types';
export function createCameraResponse(edge: string, rows: CameraRow[]): CameraResponseDto {
return {
edge,
items: rows.map((row) => ({
protocol: row.protocol,
source: row.source,
})),
};
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { CameraController } from './camera.controller';
import { CameraRepository } from './camera.repository';
import { CameraService } from './camera.service';
@Module({
controllers: [CameraController],
providers: [CameraService, CameraRepository],
})
export class CameraModule {}

View File

@@ -0,0 +1,22 @@
import { Injectable } from '@nestjs/common';
import { DbService } from '../db/db.service';
import { CameraRow } from './camera.types';
@Injectable()
export class CameraRepository {
constructor(private readonly db: DbService) {}
async findByEdge(edge: string): Promise<CameraRow[]> {
const result = await this.db.query<CameraRow>(
`
SELECT protocol, source
FROM camera
WHERE edge = $1
ORDER BY source ASC
`,
[edge],
);
return result.rows;
}
}

View File

@@ -0,0 +1,15 @@
import { Injectable } from '@nestjs/common';
import { CameraResponseDto } from './dto/camera-response.dto';
import { GetCamerasDto } from './dto/get-cameras.dto';
import { createCameraResponse } from './camera.mapper';
import { CameraRepository } from './camera.repository';
@Injectable()
export class CameraService {
constructor(private readonly repository: CameraRepository) {}
async findByEdge(query: GetCamerasDto): Promise<CameraResponseDto> {
const rows = await this.repository.findByEdge(query.edge);
return createCameraResponse(query.edge, rows);
}
}

View File

@@ -0,0 +1,4 @@
export type CameraRow = {
protocol: string;
source: string;
};

View File

@@ -0,0 +1,9 @@
export type CameraItemDto = {
protocol: string;
source: string;
};
export type CameraResponseDto = {
edge: string;
items: CameraItemDto[];
};

View File

@@ -0,0 +1,7 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class GetCamerasDto {
@IsNotEmpty()
@IsString()
edge!: string;
}

View File

@@ -1,5 +1,4 @@
import { Body, Controller, Logger, Param, Post, Req, UseGuards } from '@nestjs/common';
import type { Request } from 'express';
import { Body, Controller, Param, Post, UseGuards } from '@nestjs/common';
import { IngestApiKeyGuard } from '../common/ingest-api-key.guard';
import { IngestEdgeValuesDto } from './dto/ingest-edge-values.dto';
import { IngestPointDto } from './dto/ingest-point.dto';
@@ -8,19 +7,10 @@ import { IngestService } from './ingest.service';
@Controller('ingest')
@UseGuards(IngestApiKeyGuard)
export class IngestController {
private readonly logger = new Logger(IngestController.name);
constructor(private readonly ingest: IngestService) {}
@Post()
ingestPoint(@Body() point: IngestPointDto, @Req() request: Request) {
this.logIngestRequest(request, {
kind: 'point',
edge: point.edge,
tag: point.tag,
timestamp: point.timestamp,
});
ingestPoint(@Body() point: IngestPointDto) {
return this.ingest.ingestPoint(point);
}
@@ -28,17 +18,7 @@ export class IngestController {
ingestEdgeValues(
@Param('edge') edge: string,
@Body() body: IngestEdgeValuesDto,
@Req() request: Request,
) {
const tags = Object.keys(body.values);
this.logIngestRequest(request, {
kind: 'edge-values',
edge,
tagCount: tags.length,
firstTags: tags.slice(0, 5),
timestamp: body.timestamp,
});
const points = Object.entries(body.values).map(([tag, value]) => ({
edge,
tag,
@@ -48,33 +28,4 @@ export class IngestController {
return this.ingest.ingestPoints(points);
}
private logIngestRequest(
request: Request,
payload: Record<string, unknown>,
): void {
if (!this.isDebugLogEnabled()) {
return;
}
this.logger.log({
event: 'ingest.received',
method: request.method,
url: request.originalUrl,
host: request.get('host'),
ip: request.ip,
remoteAddress: request.socket.remoteAddress,
forwardedFor: request.get('x-forwarded-for'),
realIp: request.get('x-real-ip'),
userAgent: request.get('user-agent'),
contentLength: request.get('content-length'),
...payload,
});
}
private isDebugLogEnabled(): boolean {
return ['1', 'true', 'yes', 'on'].includes(
(process.env.INGEST_DEBUG_LOG ?? '').toLowerCase(),
);
}
}

View File

@@ -3,7 +3,6 @@ import { NestFactory } from '@nestjs/core';
import compression from 'compression';
import { AppModule } from './app.module';
/** Запускает HTTP API со сжатием и глобальной валидацией DTO. */
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(compression());