From 8dba351eb77921e7c0080267a99a08ccf03d7c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B2=D0=BE=D0=B2=20=D0=90=D1=80=D1=82?= =?UTF-8?q?=D0=B5=D0=BC?= Date: Mon, 6 Jul 2026 22:49:34 +0300 Subject: [PATCH] =?UTF-8?q?BUR-55=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B3=D0=BB=D0=B0=D0=B2=D0=BD=D0=B0=D1=8F?= =?UTF-8?q?=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B8=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B0=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=86=D0=B8=D1=8F=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D1=80=D0=B0=D0=B7=D0=B2=D0=BE=D1=80=D0=B0=D1=87=D0=B8?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D1=8E=20=D0=91=D0=94.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DATABASE_DEPLOYMENT.md | 228 ++++++++++++++++++++++++++ README.md | 19 +-- migrations/0000_cloud_beta_schema.sql | 72 ++++++++ 3 files changed, 309 insertions(+), 10 deletions(-) create mode 100644 DATABASE_DEPLOYMENT.md create mode 100644 migrations/0000_cloud_beta_schema.sql diff --git a/DATABASE_DEPLOYMENT.md b/DATABASE_DEPLOYMENT.md new file mode 100644 index 0000000..75eb9ad --- /dev/null +++ b/DATABASE_DEPLOYMENT.md @@ -0,0 +1,228 @@ +# Развертывание базы данных + +Инструкция описывает минимальный порядок подготовки 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. \ No newline at end of file diff --git a/README.md b/README.md index a5cda8a..3e65dc7 100644 --- a/README.md +++ b/README.md @@ -4,27 +4,23 @@ Lightweight Drill Cloud API for an existing PostgreSQL/TimescaleDB database. ## What is intentionally absent -- No migrations. - No `tag-translation` module. - No alarm log writes. - No automatic `edge` or `tag` upserts during ingest. ## 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: -- `edge(id, name, parent_id, tag_ids)` -- `tag(id, name, min, max, comment, unit_of_measurement, edge_ids, precision, tag_group)` +- `edge(id, name, parent_id)` +- `tag(id, name, min, max, comment, unit_of_measurement, precision, tag_group)` - `current(id, edge, tag, value, createdAt, updatedAt)` -- `history(id, edge, timestamp, tag, value, createdAt)` +- `history(edge, timestamp, tag, value, createdAt)` +- `camera(edge, protocol, source)` `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 @@ -35,6 +31,7 @@ All routes are served under the `/api` prefix. - `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` +- `GET /api/camera?edge=edge5` - `POST /api/ingest` - `POST /api/ingest/:edge` @@ -49,6 +46,8 @@ 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: ```json diff --git a/migrations/0000_cloud_beta_schema.sql b/migrations/0000_cloud_beta_schema.sql new file mode 100644 index 0000000..1604c6d --- /dev/null +++ b/migrations/0000_cloud_beta_schema.sql @@ -0,0 +1,72 @@ +-- 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);