Compare commits
1 Commits
dev
...
docker-imp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e98348cac |
@@ -1,5 +1,4 @@
|
|||||||
PORT=3101
|
PORT=3101
|
||||||
branch=
|
|
||||||
DATABASE_URL=postgres://greact:pG3526l4@194.36.208.86:5433/cloud
|
DATABASE_URL=postgres://greact:pG3526l4@194.36.208.86:5433/cloud
|
||||||
|
|
||||||
# Optional. When set, POST /api/ingest requires x-api-key: <value>.
|
# Optional. When set, POST /api/ingest requires x-api-key: <value>.
|
||||||
|
|||||||
@@ -1,228 +0,0 @@
|
|||||||
# Развертывание базы данных
|
|
||||||
|
|
||||||
Инструкция описывает минимальный порядок подготовки PostgreSQL-базы для `drill-cloud-v3`: создать базу, применить схему, зарегистрировать буровую и проверить ingest.
|
|
||||||
|
|
||||||
## Требования
|
|
||||||
|
|
||||||
- PostgreSQL с расширением TimescaleDB.
|
|
||||||
- Консольная утилита `psql`.
|
|
||||||
- Доступ к роли, которая может создавать БД, расширения и таблицы.
|
|
||||||
- Открытая интерактивная сессия `psql`.
|
|
||||||
|
|
||||||
Пример входа в `psql`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
psql -U postgres -W postgres
|
|
||||||
```
|
|
||||||
|
|
||||||
## 1. Создать базу
|
|
||||||
|
|
||||||
Если база уже создана, шаг можно пропустить.
|
|
||||||
|
|
||||||
Команды выполняются внутри `psql`:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT current_database();
|
|
||||||
|
|
||||||
CREATE DATABASE "cloud-beta-dev";
|
|
||||||
|
|
||||||
\c cloud-beta-dev
|
|
||||||
|
|
||||||
SELECT current_database();
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. Применить базовую миграцию
|
|
||||||
|
|
||||||
Базовая схема лежит в:
|
|
||||||
|
|
||||||
```text
|
|
||||||
migrations/0000_cloud_beta_schema.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
Она создает:
|
|
||||||
|
|
||||||
- `edge` - справочник буровых;
|
|
||||||
- `tag` - справочник тегов;
|
|
||||||
- `current` - последние значения тегов;
|
|
||||||
- `history` - история числовых значений;
|
|
||||||
- `camera` - видеопотоки буровых;
|
|
||||||
- индексы и первичные ключи;
|
|
||||||
- TimescaleDB hypertable для `history`.
|
|
||||||
|
|
||||||
Внутри `psql` применяем файл через `\i`:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
\i 'C:/Users/myart/Drill/cloud/migrations/0000_cloud_beta_schema.sql'
|
|
||||||
```
|
|
||||||
|
|
||||||
Важно: внутри `psql` для Windows-путей используем `/`, а не `\`. Иначе `\Users` будет воспринято как psql-команда.
|
|
||||||
|
|
||||||
Проверка:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
\dt public.*
|
|
||||||
|
|
||||||
SELECT hypertable_schema, hypertable_name
|
|
||||||
FROM timescaledb_information.hypertables;
|
|
||||||
```
|
|
||||||
|
|
||||||
Ожидаемо среди таблиц есть `edge`, `tag`, `current`, `history`, `camera`, а `history` отображается как hypertable.
|
|
||||||
|
|
||||||
## 3. Зарегистрировать буровую
|
|
||||||
|
|
||||||
Минимальная запись в `edge`:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
INSERT INTO edge (id, name, parent_id)
|
|
||||||
VALUES ('edge-demo', 'Демо буровая', NULL)
|
|
||||||
ON CONFLICT (id)
|
|
||||||
DO UPDATE SET
|
|
||||||
name = EXCLUDED.name,
|
|
||||||
parent_id = EXCLUDED.parent_id;
|
|
||||||
```
|
|
||||||
|
|
||||||
Проверка:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT id, name, parent_id
|
|
||||||
FROM edge
|
|
||||||
ORDER BY name;
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Зарегистрировать теги
|
|
||||||
|
|
||||||
Тег должен быть в таблице `tag`, если UI должен показывать русское имя, единицу измерения и метаданные.
|
|
||||||
|
|
||||||
Минимальный пример:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
INSERT INTO tag (
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
min,
|
|
||||||
max,
|
|
||||||
comment,
|
|
||||||
unit_of_measurement,
|
|
||||||
precision,
|
|
||||||
tag_group
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
'BN1_10V_ControlVoltage_Fault',
|
|
||||||
'Ошибка контрольного напряжения 10В',
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
0,
|
|
||||||
'Электрика'
|
|
||||||
)
|
|
||||||
ON CONFLICT (id)
|
|
||||||
DO UPDATE SET
|
|
||||||
name = EXCLUDED.name,
|
|
||||||
min = EXCLUDED.min,
|
|
||||||
max = EXCLUDED.max,
|
|
||||||
comment = EXCLUDED.comment,
|
|
||||||
unit_of_measurement = EXCLUDED.unit_of_measurement,
|
|
||||||
precision = EXCLUDED.precision,
|
|
||||||
tag_group = EXCLUDED.tag_group;
|
|
||||||
```
|
|
||||||
|
|
||||||
Важно:
|
|
||||||
|
|
||||||
- `tag.id` должен совпадать с `tag`, который приходит в ingest.
|
|
||||||
- `tag.name` используется как основное имя в UI.
|
|
||||||
- `unit_of_measurement` отображается рядом со значением.
|
|
||||||
- `min` и `max` используются для статусов/границ, если они заданы.
|
|
||||||
|
|
||||||
## 5. Зарегистрировать камеры
|
|
||||||
|
|
||||||
Камеры хранятся в `camera`.
|
|
||||||
|
|
||||||
Пример:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
INSERT INTO camera (edge, protocol, source)
|
|
||||||
VALUES ('edge-demo', 'wss', 'beta.video.drill.greact.ru/ws')
|
|
||||||
ON CONFLICT (edge, protocol, source) DO NOTHING;
|
|
||||||
```
|
|
||||||
|
|
||||||
Проверка:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT edge, protocol, source
|
|
||||||
FROM camera
|
|
||||||
ORDER BY edge, source;
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6. Настроить backend
|
|
||||||
|
|
||||||
В `.env` backend-сервиса указать целевую БД:
|
|
||||||
|
|
||||||
```env
|
|
||||||
DATABASE_URL=postgresql://user:password@host:5432/cloud-beta-dev
|
|
||||||
INGEST_API_KEY=secret-key
|
|
||||||
PORT=3101
|
|
||||||
```
|
|
||||||
|
|
||||||
Запуск локально:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
npm run start:dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## 7. Smoke-test ingest
|
|
||||||
|
|
||||||
Отправить числовое значение:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST "http://localhost:3101/api/ingest" \
|
|
||||||
-H "content-type: application/json" \
|
|
||||||
-H "x-api-key: secret-key" \
|
|
||||||
-d '{"edge":"edge-demo","tag":"BN1_10V_ControlVoltage_Fault","timestamp":"2026-07-06T10:00:00.000Z","value":0}'
|
|
||||||
```
|
|
||||||
|
|
||||||
Проверить `current`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl "http://localhost:3101/api/current?edge=edge-demo"
|
|
||||||
```
|
|
||||||
|
|
||||||
Проверить БД внутри `psql`:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT edge, tag, value, "updatedAt"
|
|
||||||
FROM current
|
|
||||||
WHERE edge = 'edge-demo';
|
|
||||||
```
|
|
||||||
|
|
||||||
## 8. Поведение `NULL`
|
|
||||||
|
|
||||||
Ingest принимает явный `null`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"edge": "edge-demo",
|
|
||||||
"tag": "BN1_10V_ControlVoltage_Fault",
|
|
||||||
"timestamp": "2026-07-06T10:00:00.000Z",
|
|
||||||
"value": null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Правила:
|
|
||||||
|
|
||||||
- `current.value` обновляется в `NULL`;
|
|
||||||
- в `history` такая точка не пишется;
|
|
||||||
- на фронтенде current-виджет показывает `NULL` как `—`;
|
|
||||||
- `0` остается обычным числом и не считается `NULL`.
|
|
||||||
|
|
||||||
## 9. Короткая проверка после развертывания
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl "http://localhost:3101/api/health"
|
|
||||||
curl "http://localhost:3101/api/edge"
|
|
||||||
curl "http://localhost:3101/api/current?edge=edge-demo"
|
|
||||||
curl "http://localhost:3101/api/camera?edge=edge-demo"
|
|
||||||
```
|
|
||||||
|
|
||||||
Если эти запросы отвечают корректно, база готова для работы backend и UI.
|
|
||||||
19
README.md
19
README.md
@@ -4,23 +4,27 @@ Lightweight Drill Cloud API for an existing PostgreSQL/TimescaleDB database.
|
|||||||
|
|
||||||
## What is intentionally absent
|
## What is intentionally absent
|
||||||
|
|
||||||
|
- No migrations.
|
||||||
- No `tag-translation` module.
|
- No `tag-translation` module.
|
||||||
- No alarm log writes.
|
- No alarm log writes.
|
||||||
- No automatic `edge` or `tag` upserts during ingest.
|
- No automatic `edge` or `tag` upserts during ingest.
|
||||||
|
|
||||||
## Database
|
## Database
|
||||||
|
|
||||||
Database deployment, edge registration and migrations are described in [DATABASE_DEPLOYMENT.md](./DATABASE_DEPLOYMENT.md).
|
|
||||||
|
|
||||||
The current `cloud` database schema uses these public tables:
|
The current `cloud` database schema uses these public tables:
|
||||||
|
|
||||||
- `edge(id, name, parent_id)`
|
- `edge(id, name, parent_id, tag_ids)`
|
||||||
- `tag(id, name, min, max, comment, unit_of_measurement, precision, tag_group)`
|
- `tag(id, name, min, max, comment, unit_of_measurement, edge_ids, precision, tag_group)`
|
||||||
- `current(id, edge, tag, value, createdAt, updatedAt)`
|
- `current(id, edge, tag, value, createdAt, updatedAt)`
|
||||||
- `history(edge, timestamp, tag, value, createdAt)`
|
- `history(id, edge, timestamp, tag, value, createdAt)`
|
||||||
- `camera(edge, protocol, source)`
|
|
||||||
|
|
||||||
`history` is expected to be a TimescaleDB hypertable partitioned by `timestamp`.
|
`history` is expected to be a TimescaleDB hypertable partitioned by `timestamp`.
|
||||||
|
Historical chart ranges use continuous aggregates:
|
||||||
|
|
||||||
|
- `history_1m`
|
||||||
|
- `history_5m`
|
||||||
|
- `history_1h`
|
||||||
|
- `history_1d`
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
@@ -31,7 +35,6 @@ All routes are served under the `/api` prefix.
|
|||||||
- `GET /api/tag?edge=edge5&search=pressure`
|
- `GET /api/tag?edge=edge5&search=pressure`
|
||||||
- `GET /api/current?edge=edge5&tags=tag1,tag2`
|
- `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`
|
- `GET /api/history?edge=edge5&tags=tag1,tag2&from=2026-06-01T00:00:00Z&to=2026-06-02T00:00:00Z`
|
||||||
- `GET /api/camera?edge=edge5`
|
|
||||||
- `POST /api/ingest`
|
- `POST /api/ingest`
|
||||||
- `POST /api/ingest/:edge`
|
- `POST /api/ingest/:edge`
|
||||||
|
|
||||||
@@ -46,8 +49,6 @@ All routes are served under the `/api` prefix.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`value` may be `null`. In that case `current` is updated, but `history` is not.
|
|
||||||
|
|
||||||
`POST /api/ingest/:edge` accepts a compact edge payload:
|
`POST /api/ingest/:edge` accepts a compact edge payload:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
services:
|
services:
|
||||||
drill-cloud-v3:
|
drill-cloud-v3:
|
||||||
container_name: container-drill-cloud-v3${branch:+-${branch}}
|
|
||||||
build: .
|
build: .
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
-- Base schema copied from cloud-beta.
|
|
||||||
-- Creates application tables, indexes and TimescaleDB hypertable for history.
|
|
||||||
|
|
||||||
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
|
||||||
|
|
||||||
CREATE SCHEMA IF NOT EXISTS public;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS public.edge (
|
|
||||||
id varchar(100) NOT NULL,
|
|
||||||
name text NOT NULL,
|
|
||||||
parent_id varchar(100),
|
|
||||||
CONSTRAINT edge_pkey PRIMARY KEY (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS public.tag (
|
|
||||||
id varchar(100) NOT NULL,
|
|
||||||
name text NOT NULL,
|
|
||||||
min double precision,
|
|
||||||
max double precision,
|
|
||||||
comment text NOT NULL,
|
|
||||||
unit_of_measurement text NOT NULL,
|
|
||||||
precision integer,
|
|
||||||
tag_group varchar(255),
|
|
||||||
CONSTRAINT tag_pkey PRIMARY KEY (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS public.current (
|
|
||||||
id integer GENERATED BY DEFAULT AS IDENTITY,
|
|
||||||
edge varchar(100) NOT NULL,
|
|
||||||
tag varchar(100) NOT NULL,
|
|
||||||
value double precision,
|
|
||||||
"createdAt" timestamp(3) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
||||||
"updatedAt" timestamp(3) without time zone NOT NULL,
|
|
||||||
CONSTRAINT current_pkey PRIMARY KEY (id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS public.history (
|
|
||||||
edge varchar(100) NOT NULL,
|
|
||||||
"timestamp" timestamp(3) without time zone NOT NULL,
|
|
||||||
tag varchar(100) NOT NULL,
|
|
||||||
value double precision NOT NULL,
|
|
||||||
"createdAt" timestamp(3) without time zone DEFAULT now() NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
SELECT create_hypertable(
|
|
||||||
'public.history',
|
|
||||||
'timestamp',
|
|
||||||
chunk_time_interval => INTERVAL '1 day',
|
|
||||||
if_not_exists => TRUE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS public.camera (
|
|
||||||
edge varchar NOT NULL,
|
|
||||||
protocol varchar NOT NULL,
|
|
||||||
source varchar NOT NULL,
|
|
||||||
CONSTRAINT camera_pkey PRIMARY KEY (edge, protocol, source)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS current_edge_tag_key
|
|
||||||
ON public.current USING btree (edge, tag);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS current_edge_idx
|
|
||||||
ON public.current USING btree (edge);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS current_tag_idx
|
|
||||||
ON public.current USING btree (tag);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS history_edge_tag_timestamp_idx
|
|
||||||
ON public.history USING btree (edge, tag, "timestamp" DESC);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS camera_edge_idx
|
|
||||||
ON public.camera USING btree (edge);
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE current
|
|
||||||
ALTER COLUMN value DROP NOT NULL;
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { CameraModule } from './camera/camera.module';
|
|
||||||
import { CurrentModule } from './current/current.module';
|
import { CurrentModule } from './current/current.module';
|
||||||
import { DbModule } from './db/db.module';
|
import { DbModule } from './db/db.module';
|
||||||
import { EdgeModule } from './edge/edge.module';
|
import { EdgeModule } from './edge/edge.module';
|
||||||
@@ -11,7 +10,6 @@ import { TagModule } from './tag/tag.module';
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
DbModule,
|
DbModule,
|
||||||
CameraModule,
|
|
||||||
IngestModule,
|
IngestModule,
|
||||||
EdgeModule,
|
EdgeModule,
|
||||||
CurrentModule,
|
CurrentModule,
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
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,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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 {}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export type CameraRow = {
|
|
||||||
protocol: string;
|
|
||||||
source: string;
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export type CameraItemDto = {
|
|
||||||
protocol: string;
|
|
||||||
source: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CameraResponseDto = {
|
|
||||||
edge: string;
|
|
||||||
items: CameraItemDto[];
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { IsNotEmpty, IsString } from 'class-validator';
|
|
||||||
|
|
||||||
export class GetCamerasDto {
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
edge!: string;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
export type CurrentRow = {
|
export type CurrentRow = {
|
||||||
edge: string;
|
edge: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
value: number | null;
|
value: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export type CurrentItemDto = {
|
export type CurrentItemDto = {
|
||||||
edge: string;
|
edge: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
value: number | null;
|
value: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
time: Date;
|
time: Date;
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ export class IngestEdgeValuesDto {
|
|||||||
timestamp!: string;
|
timestamp!: string;
|
||||||
|
|
||||||
@IsObject()
|
@IsObject()
|
||||||
values!: Record<string, number | null>;
|
values!: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IsDateString, IsNotEmpty, IsString } from 'class-validator';
|
import { Type } from 'class-transformer';
|
||||||
import { IsNullableNumber } from '../../common/is-nullable-number.decorator';
|
import { IsDateString, IsNotEmpty, IsNumber, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class IngestPointDto {
|
export class IngestPointDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@@ -13,6 +13,7 @@ export class IngestPointDto {
|
|||||||
@IsDateString()
|
@IsDateString()
|
||||||
timestamp!: string;
|
timestamp!: string;
|
||||||
|
|
||||||
@IsNullableNumber()
|
@Type(() => Number)
|
||||||
value!: number | null;
|
@IsNumber()
|
||||||
|
value!: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export class IngestController {
|
|||||||
const points = Object.entries(body.values).map(([tag, value]) => ({
|
const points = Object.entries(body.values).map(([tag, value]) => ({
|
||||||
edge,
|
edge,
|
||||||
tag,
|
tag,
|
||||||
value,
|
value: Number(value),
|
||||||
timestamp: body.timestamp,
|
timestamp: body.timestamp,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -10,17 +10,15 @@ type IngestResult = {
|
|||||||
export class IngestService {
|
export class IngestService {
|
||||||
constructor(private readonly db: DbService) {}
|
constructor(private readonly db: DbService) {}
|
||||||
|
|
||||||
/** Пишет числовую точку в history и всегда обновляет текущий снимок current для UI/SSE. */
|
/** Пишет одну входящую точку в history и обновляет текущий снимок current для UI/SSE. */
|
||||||
async ingestPoint(point: IngestPointDto): Promise<IngestResult> {
|
async ingestPoint(point: IngestPointDto): Promise<IngestResult> {
|
||||||
if (point.value !== null) {
|
await this.db.query(
|
||||||
await this.db.query(
|
`
|
||||||
`
|
INSERT INTO history (timestamp, edge, tag, value)
|
||||||
INSERT INTO history (timestamp, edge, tag, value)
|
VALUES ($1, $2, $3, $4)
|
||||||
VALUES ($1, $2, $3, $4)
|
`,
|
||||||
`,
|
[point.timestamp, point.edge, point.tag, point.value],
|
||||||
[point.timestamp, point.edge, point.tag, point.value],
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.db.query(
|
await this.db.query(
|
||||||
`
|
`
|
||||||
|
|||||||
Reference in New Issue
Block a user