Backend (water-api/): - Crear API REST completa con Express + TypeScript - Implementar autenticación JWT con refresh tokens - CRUD completo para: projects, concentrators, meters, gateways, devices, users, roles - Agregar validación con Zod para todas las entidades - Implementar webhooks para The Things Stack (LoRaWAN) - Agregar endpoint de lecturas con filtros y resumen de consumo - Implementar carga masiva de medidores via Excel (.xlsx) Frontend: - Crear cliente HTTP con manejo automático de JWT y refresh - Actualizar todas las APIs para usar nuevo backend - Agregar sistema de autenticación real (login, logout, me) - Agregar selector de tipo (LORA, LoRaWAN, Grandes) en concentradores y medidores - Agregar campo Meter ID en medidores - Crear modal de carga masiva para medidores - Agregar página de consumo con gráficas y filtros - Corregir carga de proyectos independiente de datos existentes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
435 lines
17 KiB
PL/PgSQL
435 lines
17 KiB
PL/PgSQL
-- ============================================================================
|
|
-- Water Project Database Schema
|
|
-- PostgreSQL Migration Script
|
|
-- ============================================================================
|
|
|
|
-- Enable required extensions
|
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
|
|
|
-- ============================================================================
|
|
-- TRIGGER FUNCTION: Auto-update updated_at timestamp
|
|
-- ============================================================================
|
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- ============================================================================
|
|
-- ENUM TYPES
|
|
-- ============================================================================
|
|
CREATE TYPE role_name AS ENUM ('ADMIN', 'OPERATOR', 'VIEWER');
|
|
CREATE TYPE project_status AS ENUM ('ACTIVE', 'INACTIVE', 'COMPLETED');
|
|
CREATE TYPE device_status AS ENUM ('ACTIVE', 'INACTIVE', 'OFFLINE', 'MAINTENANCE', 'ERROR');
|
|
CREATE TYPE meter_type AS ENUM ('WATER', 'GAS', 'ELECTRIC');
|
|
CREATE TYPE reading_type AS ENUM ('AUTOMATIC', 'MANUAL', 'SCHEDULED');
|
|
|
|
-- ============================================================================
|
|
-- TABLE 1: roles
|
|
-- ============================================================================
|
|
CREATE TABLE roles (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name role_name NOT NULL UNIQUE,
|
|
description TEXT,
|
|
permissions JSONB NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_roles_name ON roles(name);
|
|
|
|
CREATE TRIGGER trigger_roles_updated_at
|
|
BEFORE UPDATE ON roles
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
COMMENT ON TABLE roles IS 'User roles with associated permissions';
|
|
|
|
-- ============================================================================
|
|
-- TABLE 2: users
|
|
-- ============================================================================
|
|
CREATE TABLE users (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
email VARCHAR(255) NOT NULL UNIQUE,
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
avatar_url TEXT,
|
|
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE RESTRICT,
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
last_login TIMESTAMP WITH TIME ZONE,
|
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_users_email ON users(email);
|
|
CREATE INDEX idx_users_role_id ON users(role_id);
|
|
CREATE INDEX idx_users_is_active ON users(is_active);
|
|
|
|
CREATE TRIGGER trigger_users_updated_at
|
|
BEFORE UPDATE ON users
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
COMMENT ON TABLE users IS 'Application users with authentication credentials';
|
|
|
|
-- ============================================================================
|
|
-- TABLE 3: projects
|
|
-- ============================================================================
|
|
CREATE TABLE projects (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
area_name VARCHAR(255),
|
|
location TEXT,
|
|
status project_status NOT NULL DEFAULT 'ACTIVE',
|
|
created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_projects_status ON projects(status);
|
|
CREATE INDEX idx_projects_created_by ON projects(created_by);
|
|
CREATE INDEX idx_projects_name ON projects(name);
|
|
|
|
CREATE TRIGGER trigger_projects_updated_at
|
|
BEFORE UPDATE ON projects
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
COMMENT ON TABLE projects IS 'Water monitoring projects';
|
|
|
|
-- ============================================================================
|
|
-- TABLE 4: concentrators
|
|
-- ============================================================================
|
|
CREATE TABLE concentrators (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
serial_number VARCHAR(100) NOT NULL UNIQUE,
|
|
name VARCHAR(255) NOT NULL,
|
|
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
location TEXT,
|
|
status device_status NOT NULL DEFAULT 'ACTIVE',
|
|
ip_address INET,
|
|
firmware_version VARCHAR(50),
|
|
last_communication TIMESTAMP WITH TIME ZONE,
|
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_concentrators_serial_number ON concentrators(serial_number);
|
|
CREATE INDEX idx_concentrators_project_id ON concentrators(project_id);
|
|
CREATE INDEX idx_concentrators_status ON concentrators(status);
|
|
|
|
CREATE TRIGGER trigger_concentrators_updated_at
|
|
BEFORE UPDATE ON concentrators
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
COMMENT ON TABLE concentrators IS 'Data concentrators that aggregate gateway communications';
|
|
|
|
-- ============================================================================
|
|
-- TABLE 5: gateways
|
|
-- ============================================================================
|
|
CREATE TABLE gateways (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
gateway_id VARCHAR(100) NOT NULL UNIQUE,
|
|
name VARCHAR(255) NOT NULL,
|
|
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
concentrator_id UUID REFERENCES concentrators(id) ON DELETE SET NULL,
|
|
location TEXT,
|
|
status device_status NOT NULL DEFAULT 'ACTIVE',
|
|
tts_gateway_id VARCHAR(255),
|
|
tts_status VARCHAR(50),
|
|
tts_last_seen TIMESTAMP WITH TIME ZONE,
|
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_gateways_gateway_id ON gateways(gateway_id);
|
|
CREATE INDEX idx_gateways_project_id ON gateways(project_id);
|
|
CREATE INDEX idx_gateways_concentrator_id ON gateways(concentrator_id);
|
|
CREATE INDEX idx_gateways_status ON gateways(status);
|
|
|
|
CREATE TRIGGER trigger_gateways_updated_at
|
|
BEFORE UPDATE ON gateways
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
COMMENT ON TABLE gateways IS 'LoRaWAN gateways for device communication';
|
|
|
|
-- ============================================================================
|
|
-- TABLE 6: devices
|
|
-- ============================================================================
|
|
CREATE TABLE devices (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
dev_eui VARCHAR(16) NOT NULL UNIQUE,
|
|
name VARCHAR(255) NOT NULL,
|
|
device_type VARCHAR(100),
|
|
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
gateway_id UUID REFERENCES gateways(id) ON DELETE SET NULL,
|
|
status device_status NOT NULL DEFAULT 'ACTIVE',
|
|
tts_device_id VARCHAR(255),
|
|
tts_status VARCHAR(50),
|
|
tts_last_seen TIMESTAMP WITH TIME ZONE,
|
|
app_key VARCHAR(32),
|
|
join_eui VARCHAR(16),
|
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_devices_dev_eui ON devices(dev_eui);
|
|
CREATE INDEX idx_devices_project_id ON devices(project_id);
|
|
CREATE INDEX idx_devices_gateway_id ON devices(gateway_id);
|
|
CREATE INDEX idx_devices_status ON devices(status);
|
|
CREATE INDEX idx_devices_device_type ON devices(device_type);
|
|
|
|
CREATE TRIGGER trigger_devices_updated_at
|
|
BEFORE UPDATE ON devices
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
COMMENT ON TABLE devices IS 'LoRaWAN end devices (sensors/transmitters)';
|
|
|
|
-- ============================================================================
|
|
-- TABLE 7: meters
|
|
-- ============================================================================
|
|
CREATE TABLE meters (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
serial_number VARCHAR(100) NOT NULL UNIQUE,
|
|
name VARCHAR(255) NOT NULL,
|
|
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
device_id UUID REFERENCES devices(id) ON DELETE SET NULL,
|
|
area_name VARCHAR(255),
|
|
location TEXT,
|
|
meter_type meter_type NOT NULL DEFAULT 'WATER',
|
|
status device_status NOT NULL DEFAULT 'ACTIVE',
|
|
last_reading_value NUMERIC(15, 4),
|
|
last_reading_at TIMESTAMP WITH TIME ZONE,
|
|
installation_date DATE,
|
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_meters_serial_number ON meters(serial_number);
|
|
CREATE INDEX idx_meters_project_id ON meters(project_id);
|
|
CREATE INDEX idx_meters_device_id ON meters(device_id);
|
|
CREATE INDEX idx_meters_status ON meters(status);
|
|
CREATE INDEX idx_meters_meter_type ON meters(meter_type);
|
|
CREATE INDEX idx_meters_area_name ON meters(area_name);
|
|
|
|
CREATE TRIGGER trigger_meters_updated_at
|
|
BEFORE UPDATE ON meters
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
COMMENT ON TABLE meters IS 'Physical water meters associated with devices';
|
|
|
|
-- ============================================================================
|
|
-- TABLE 8: meter_readings
|
|
-- ============================================================================
|
|
CREATE TABLE meter_readings (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
meter_id UUID NOT NULL REFERENCES meters(id) ON DELETE CASCADE,
|
|
device_id UUID REFERENCES devices(id) ON DELETE SET NULL,
|
|
reading_value NUMERIC(15, 4) NOT NULL,
|
|
reading_type reading_type NOT NULL DEFAULT 'AUTOMATIC',
|
|
battery_level SMALLINT,
|
|
signal_strength SMALLINT,
|
|
raw_payload TEXT,
|
|
received_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_meter_readings_meter_id ON meter_readings(meter_id);
|
|
CREATE INDEX idx_meter_readings_device_id ON meter_readings(device_id);
|
|
CREATE INDEX idx_meter_readings_received_at ON meter_readings(received_at);
|
|
CREATE INDEX idx_meter_readings_meter_id_received_at ON meter_readings(meter_id, received_at DESC);
|
|
|
|
COMMENT ON TABLE meter_readings IS 'Historical meter reading values';
|
|
|
|
-- ============================================================================
|
|
-- TABLE 9: tts_uplink_logs
|
|
-- ============================================================================
|
|
CREATE TABLE tts_uplink_logs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
device_id UUID REFERENCES devices(id) ON DELETE SET NULL,
|
|
dev_eui VARCHAR(16) NOT NULL,
|
|
raw_payload JSONB NOT NULL,
|
|
decoded_payload JSONB,
|
|
gateway_ids TEXT[],
|
|
rssi SMALLINT,
|
|
snr NUMERIC(5, 2),
|
|
processed BOOLEAN NOT NULL DEFAULT FALSE,
|
|
error_message TEXT,
|
|
received_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX idx_tts_uplink_logs_device_id ON tts_uplink_logs(device_id);
|
|
CREATE INDEX idx_tts_uplink_logs_dev_eui ON tts_uplink_logs(dev_eui);
|
|
CREATE INDEX idx_tts_uplink_logs_received_at ON tts_uplink_logs(received_at);
|
|
CREATE INDEX idx_tts_uplink_logs_processed ON tts_uplink_logs(processed);
|
|
CREATE INDEX idx_tts_uplink_logs_raw_payload ON tts_uplink_logs USING GIN (raw_payload);
|
|
|
|
COMMENT ON TABLE tts_uplink_logs IS 'The Things Stack uplink message logs';
|
|
|
|
-- ============================================================================
|
|
-- TABLE 10: refresh_tokens
|
|
-- ============================================================================
|
|
CREATE TABLE refresh_tokens (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
token_hash VARCHAR(255) NOT NULL UNIQUE,
|
|
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
revoked_at TIMESTAMP WITH TIME ZONE
|
|
);
|
|
|
|
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
|
|
CREATE INDEX idx_refresh_tokens_token_hash ON refresh_tokens(token_hash);
|
|
CREATE INDEX idx_refresh_tokens_expires_at ON refresh_tokens(expires_at);
|
|
|
|
COMMENT ON TABLE refresh_tokens IS 'JWT refresh tokens for user sessions';
|
|
|
|
-- ============================================================================
|
|
-- VIEW: meter_stats_by_project
|
|
-- ============================================================================
|
|
CREATE OR REPLACE VIEW meter_stats_by_project AS
|
|
SELECT
|
|
p.id AS project_id,
|
|
p.name AS project_name,
|
|
p.status AS project_status,
|
|
COUNT(m.id) AS total_meters,
|
|
COUNT(CASE WHEN m.status = 'ACTIVE' THEN 1 END) AS active_meters,
|
|
COUNT(CASE WHEN m.status = 'INACTIVE' THEN 1 END) AS inactive_meters,
|
|
COUNT(CASE WHEN m.status = 'OFFLINE' THEN 1 END) AS offline_meters,
|
|
COUNT(CASE WHEN m.status = 'MAINTENANCE' THEN 1 END) AS maintenance_meters,
|
|
COUNT(CASE WHEN m.status = 'ERROR' THEN 1 END) AS error_meters,
|
|
ROUND(AVG(m.last_reading_value)::NUMERIC, 2) AS avg_last_reading,
|
|
MAX(m.last_reading_at) AS most_recent_reading,
|
|
COUNT(DISTINCT m.area_name) AS unique_areas
|
|
FROM projects p
|
|
LEFT JOIN meters m ON p.id = m.project_id
|
|
GROUP BY p.id, p.name, p.status;
|
|
|
|
COMMENT ON VIEW meter_stats_by_project IS 'Aggregated meter statistics per project';
|
|
|
|
-- ============================================================================
|
|
-- VIEW: device_status_summary
|
|
-- ============================================================================
|
|
CREATE OR REPLACE VIEW device_status_summary AS
|
|
SELECT
|
|
p.id AS project_id,
|
|
p.name AS project_name,
|
|
'concentrator' AS device_category,
|
|
c.status,
|
|
COUNT(*) AS count
|
|
FROM projects p
|
|
LEFT JOIN concentrators c ON p.id = c.project_id
|
|
WHERE c.id IS NOT NULL
|
|
GROUP BY p.id, p.name, c.status
|
|
|
|
UNION ALL
|
|
|
|
SELECT
|
|
p.id AS project_id,
|
|
p.name AS project_name,
|
|
'gateway' AS device_category,
|
|
g.status,
|
|
COUNT(*) AS count
|
|
FROM projects p
|
|
LEFT JOIN gateways g ON p.id = g.project_id
|
|
WHERE g.id IS NOT NULL
|
|
GROUP BY p.id, p.name, g.status
|
|
|
|
UNION ALL
|
|
|
|
SELECT
|
|
p.id AS project_id,
|
|
p.name AS project_name,
|
|
'device' AS device_category,
|
|
d.status,
|
|
COUNT(*) AS count
|
|
FROM projects p
|
|
LEFT JOIN devices d ON p.id = d.project_id
|
|
WHERE d.id IS NOT NULL
|
|
GROUP BY p.id, p.name, d.status
|
|
|
|
UNION ALL
|
|
|
|
SELECT
|
|
p.id AS project_id,
|
|
p.name AS project_name,
|
|
'meter' AS device_category,
|
|
m.status,
|
|
COUNT(*) AS count
|
|
FROM projects p
|
|
LEFT JOIN meters m ON p.id = m.project_id
|
|
WHERE m.id IS NOT NULL
|
|
GROUP BY p.id, p.name, m.status;
|
|
|
|
COMMENT ON VIEW device_status_summary IS 'Summary of device statuses across all device types per project';
|
|
|
|
-- ============================================================================
|
|
-- SEED DATA: Default Roles
|
|
-- ============================================================================
|
|
INSERT INTO roles (name, description, permissions) VALUES
|
|
(
|
|
'ADMIN',
|
|
'Full system administrator with all permissions',
|
|
'{
|
|
"users": {"create": true, "read": true, "update": true, "delete": true},
|
|
"projects": {"create": true, "read": true, "update": true, "delete": true},
|
|
"devices": {"create": true, "read": true, "update": true, "delete": true},
|
|
"meters": {"create": true, "read": true, "update": true, "delete": true},
|
|
"readings": {"create": true, "read": true, "update": true, "delete": true},
|
|
"settings": {"create": true, "read": true, "update": true, "delete": true},
|
|
"reports": {"create": true, "read": true, "export": true}
|
|
}'::JSONB
|
|
),
|
|
(
|
|
'OPERATOR',
|
|
'Operator with management permissions but no system settings',
|
|
'{
|
|
"users": {"create": false, "read": true, "update": false, "delete": false},
|
|
"projects": {"create": true, "read": true, "update": true, "delete": false},
|
|
"devices": {"create": true, "read": true, "update": true, "delete": false},
|
|
"meters": {"create": true, "read": true, "update": true, "delete": false},
|
|
"readings": {"create": true, "read": true, "update": false, "delete": false},
|
|
"settings": {"create": false, "read": true, "update": false, "delete": false},
|
|
"reports": {"create": true, "read": true, "export": true}
|
|
}'::JSONB
|
|
),
|
|
(
|
|
'VIEWER',
|
|
'Read-only access to view data and reports',
|
|
'{
|
|
"users": {"create": false, "read": false, "update": false, "delete": false},
|
|
"projects": {"create": false, "read": true, "update": false, "delete": false},
|
|
"devices": {"create": false, "read": true, "update": false, "delete": false},
|
|
"meters": {"create": false, "read": true, "update": false, "delete": false},
|
|
"readings": {"create": false, "read": true, "update": false, "delete": false},
|
|
"settings": {"create": false, "read": false, "update": false, "delete": false},
|
|
"reports": {"create": false, "read": true, "export": false}
|
|
}'::JSONB
|
|
);
|
|
|
|
-- ============================================================================
|
|
-- SEED DATA: Default Admin User
|
|
-- Password: admin123 (bcrypt hashed)
|
|
-- ============================================================================
|
|
INSERT INTO users (email, password_hash, name, role_id, is_active)
|
|
SELECT
|
|
'admin@waterproject.com',
|
|
'$2b$12$RrlEdRsUiiQYxtUmjOjX.uZU/IpXUFsXsWxDcMny1RUl6RFc.etDm',
|
|
'System Administrator',
|
|
r.id,
|
|
TRUE
|
|
FROM roles r
|
|
WHERE r.name = 'ADMIN';
|
|
|
|
-- ============================================================================
|
|
-- END OF SCHEMA
|
|
-- ============================================================================
|