16 Commits

Author SHA1 Message Date
Первов Артем
b2d3bbddfc BUR-61 Изменил чтение поля value в ingest.controller 2026-07-06 17:02:35 +03:00
54c8bf2e01 Merge pull request 'BUR-61 current.value сделан nullable в бд. Соответствующие сущности скорректированы под изменение бд' (#5) from bur-61 into dev
Reviewed-on: #5
2026-07-05 19:08:44 +00:00
Первов Артем
22529ba0e8 BUR-61 current.value сделан nullable в бд. Соответствующие сущности скорректированы под изменение бд 2026-07-05 22:03:47 +03:00
387f6c0613 Merge pull request 'BUR-58 Добавлена поддержка работы с камерой' (#4) from bur-58 into dev
Reviewed-on: #4
2026-07-05 04:08:02 +00:00
Первов Артем
203bdf193e BUR-58 Добавлена поддержка работы с камерой 2026-07-05 06:57:54 +03:00
Первов Артем
3bf4cebbe6 Merge branch 'main' into dev 2026-07-04 18:01:41 +03:00
Первов Артем
907c24c45b docker-compose.dev.yml добавил. Временное решение. 2026-07-04 14:26:22 +03:00
Первов Артем
b057ea154a Удалил docker-compose.dev.yml. поменял наименование контейнера. Убрал дебаг логи. 2026-07-04 14:06:53 +03:00
Первов Артем
0e2a7eeeec docker-compose для дев окружения 2026-07-03 21:21:21 +03:00
Первов Артем
c702946393 обновлено логирование 2026-07-03 19:33:28 +03:00
Первов Артем
5260b8a9c5 Merge branch 'dev' of https://git.greact.ru/APervov/cloud-v3 into dev 2026-07-03 17:35:13 +03:00
Первов Артем
30bd7a0d28 добавлен лог для входящих запросов 2026-07-03 17:34:32 +03:00
32ea0d7c4d Merge branch 'dev' 2026-07-03 13:16:43 +05:00
c22d1b5b8c Merge pull request 'CORS problem solution' (#2) from cors into dev
Reviewed-on: #2
2026-07-03 07:58:02 +00:00
56e90a54d8 CORS problem solution 2026-07-03 12:54:23 +05:00
15fe566d53 Merge pull request 'Убран лишний код. Скорректированы docker и package файлы. Переработано взаимодействие с бд - упрощена логика.' (#1) from dev into main
Reviewed-on: #1
Reviewed-by: Никита Бизяев <cap7goo@gmail.com>
2026-07-01 19:07:59 +00:00
24 changed files with 174 additions and 43 deletions

View File

@@ -1,10 +1,9 @@
PORT=3101
branch=
DATABASE_URL=postgres://greact:pG3526l4@194.36.208.86:5433/cloud
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
# Optional. When set, POST /ingest requires x-api-key: <value>.
# Optional. When set, POST /api/ingest requires x-api-key: <value>.
INGEST_API_KEY=
# Poll interval for GET /current/events SSE snapshots.
# Poll interval for GET /api/current/events SSE snapshots.
CURRENT_EVENTS_POLL_MS=1000

View File

@@ -1,5 +1,4 @@
PORT=3101
DATABASE_URL=postgres://greact:pG3526l4@194.36.208.86:5433/cloud
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
INGEST_API_KEY=
CURRENT_EVENTS_POLL_MS=1000

View File

@@ -28,15 +28,17 @@ Historical chart ranges use continuous aggregates:
## API
- `GET /health`
- `GET /edge`
- `GET /tag?edge=edge5&search=pressure`
- `GET /current?edge=edge5&tags=tag1,tag2`
- `GET /history?edge=edge5&tags=tag1,tag2&from=2026-06-01T00:00:00Z&to=2026-06-02T00:00:00Z`
- `POST /ingest`
- `POST /ingest/:edge`
All routes are served under the `/api` prefix.
`POST /ingest` accepts one point:
- `GET /api/health`
- `GET /api/edge`
- `GET /api/tag?edge=edge5&search=pressure`
- `GET /api/current?edge=edge5&tags=tag1,tag2`
- `GET /api/history?edge=edge5&tags=tag1,tag2&from=2026-06-01T00:00:00Z&to=2026-06-02T00:00:00Z`
- `POST /api/ingest`
- `POST /api/ingest/:edge`
`POST /api/ingest` accepts one point:
```json
{
@@ -47,7 +49,7 @@ Historical chart ranges use continuous aggregates:
}
```
`POST /ingest/:edge` accepts a compact edge payload:
`POST /api/ingest/:edge` accepts a compact edge payload:
```json
{

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,13 +1,12 @@
services:
drill-cloud-v3:
container_name: drill-cloud-v3${branch:+-${branch}}
container_name: container-drill-cloud-v3${branch:+-${branch}}
build: .
restart: unless-stopped
environment:
NODE_ENV: production
PORT: ${PORT}
DATABASE_URL: ${DATABASE_URL}
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS}
INGEST_API_KEY: ${INGEST_API_KEY}
CURRENT_EVENTS_POLL_MS: ${CURRENT_EVENTS_POLL_MS}
expose:

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

@@ -0,0 +1,2 @@
ALTER TABLE current
ALTER COLUMN value DROP NOT NULL;

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

@@ -0,0 +1,11 @@
import { applyDecorators } from '@nestjs/common';
import { Transform } from 'class-transformer';
import { IsNumber, ValidateIf, ValidationOptions } from 'class-validator';
export function IsNullableNumber(options?: ValidationOptions): PropertyDecorator {
return applyDecorators(
Transform(({ value }) => (value === null ? null : Number(value))),
ValidateIf((_, value) => value !== null),
IsNumber({}, options),
);
}

View File

@@ -1,7 +1,7 @@
export type CurrentRow = {
edge: string;
tag: string;
value: number;
value: number | null;
createdAt: Date;
updatedAt: Date;
name: string | null;

View File

@@ -1,7 +1,7 @@
export type CurrentItemDto = {
edge: string;
tag: string;
value: number;
value: number | null;
createdAt: Date;
updatedAt: Date;
time: Date;

View File

@@ -5,5 +5,5 @@ export class IngestEdgeValuesDto {
timestamp!: string;
@IsObject()
values!: Record<string, number>;
values!: Record<string, number | null>;
}

View File

@@ -1,5 +1,5 @@
import { Type } from 'class-transformer';
import { IsDateString, IsNotEmpty, IsNumber, IsString } from 'class-validator';
import { IsDateString, IsNotEmpty, IsString } from 'class-validator';
import { IsNullableNumber } from '../../common/is-nullable-number.decorator';
export class IngestPointDto {
@IsNotEmpty()
@@ -13,7 +13,6 @@ export class IngestPointDto {
@IsDateString()
timestamp!: string;
@Type(() => Number)
@IsNumber()
value!: number;
@IsNullableNumber()
value!: number | null;
}

View File

@@ -9,19 +9,20 @@ import { IngestService } from './ingest.service';
export class IngestController {
constructor(private readonly ingest: IngestService) {}
/** Принимает одну сырую точку и сохраняет ее как запись истории/current. */
@Post()
ingestPoint(@Body() point: IngestPointDto) {
return this.ingest.ingestPoint(point);
}
/** Принимает компактное тело запроса по edge и сохраняет каждое значение построчно. */
@Post(':edge')
ingestEdgeValues(@Param('edge') edge: string, @Body() body: IngestEdgeValuesDto) {
ingestEdgeValues(
@Param('edge') edge: string,
@Body() body: IngestEdgeValuesDto,
) {
const points = Object.entries(body.values).map(([tag, value]) => ({
edge,
tag,
value: Number(value),
value,
timestamp: body.timestamp,
}));

View File

@@ -10,15 +10,17 @@ type IngestResult = {
export class IngestService {
constructor(private readonly db: DbService) {}
/** Пишет одну входящую точку в history и обновляет текущий снимок current для UI/SSE. */
/** Пишет числовую точку в history и всегда обновляет текущий снимок current для UI/SSE. */
async ingestPoint(point: IngestPointDto): Promise<IngestResult> {
await this.db.query(
`
INSERT INTO history (timestamp, edge, tag, value)
VALUES ($1, $2, $3, $4)
`,
[point.timestamp, point.edge, point.tag, point.value],
);
if (point.value !== null) {
await this.db.query(
`
INSERT INTO history (timestamp, edge, tag, value)
VALUES ($1, $2, $3, $4)
`,
[point.timestamp, point.edge, point.tag, point.value],
);
}
await this.db.query(
`

View File

@@ -3,17 +3,10 @@ import { NestFactory } from '@nestjs/core';
import compression from 'compression';
import { AppModule } from './app.module';
/** Запускает HTTP API со сжатием, CORS и глобальной валидацией DTO. */
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(compression());
app.enableCors({
origin: process.env.CORS_ALLOWED_ORIGINS,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-api-key'],
});
app.setGlobalPrefix('api');
app.useGlobalPipes(
new ValidationPipe({