import { pool } from '../config/database'; import { CreateConcentratorInput, UpdateConcentratorInput } from '../validators/concentrator.validator'; /** * Concentrator types */ export type ConcentratorType = 'LORA' | 'LORAWAN' | 'GRANDES'; /** * Concentrator entity interface */ export interface Concentrator { id: string; serial_number: string; name: string | null; project_id: string; location: string | null; type: ConcentratorType; status: 'online' | 'offline' | 'maintenance' | 'unknown'; ip_address: string | null; firmware_version: string | null; created_at: Date; updated_at: Date; } /** * Concentrator with gateway count */ export interface ConcentratorWithCount extends Concentrator { gateway_count: number; } /** * Filter options for concentrators */ export interface ConcentratorFilters { project_id?: string; status?: string; type?: ConcentratorType; } /** * Pagination options */ export interface PaginationOptions { page?: number; limit?: number; sortBy?: string; sortOrder?: 'asc' | 'desc'; } /** * Paginated result */ export interface PaginatedResult { data: T[]; pagination: { page: number; limit: number; total: number; totalPages: number; hasNextPage: boolean; hasPreviousPage: boolean; }; } /** * Get all concentrators with optional filters and pagination * @param filters - Optional filter criteria * @param pagination - Optional pagination options * @returns Paginated list of concentrators */ export async function getAll( filters?: ConcentratorFilters, pagination?: PaginationOptions ): Promise> { const page = pagination?.page || 1; const limit = pagination?.limit || 10; const offset = (page - 1) * limit; const sortBy = pagination?.sortBy || 'created_at'; const sortOrder = pagination?.sortOrder || 'desc'; // Build WHERE clause const conditions: string[] = []; const params: unknown[] = []; let paramIndex = 1; if (filters?.project_id) { conditions.push(`project_id = $${paramIndex}`); params.push(filters.project_id); paramIndex++; } if (filters?.status) { conditions.push(`status = $${paramIndex}`); params.push(filters.status); paramIndex++; } if (filters?.type) { conditions.push(`type = $${paramIndex}`); params.push(filters.type); paramIndex++; } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; // Validate sort column to prevent SQL injection const allowedSortColumns = ['id', 'serial_number', 'name', 'status', 'created_at', 'updated_at']; const safeSortBy = allowedSortColumns.includes(sortBy) ? sortBy : 'created_at'; const safeSortOrder = sortOrder === 'asc' ? 'ASC' : 'DESC'; // Get total count const countQuery = `SELECT COUNT(*) FROM concentrators ${whereClause}`; const countResult = await pool.query(countQuery, params); const total = parseInt(countResult.rows[0].count, 10); // Get data const dataQuery = ` SELECT * FROM concentrators ${whereClause} ORDER BY ${safeSortBy} ${safeSortOrder} LIMIT $${paramIndex} OFFSET $${paramIndex + 1} `; params.push(limit, offset); const dataResult = await pool.query(dataQuery, params); const totalPages = Math.ceil(total / limit); return { data: dataResult.rows, pagination: { page, limit, total, totalPages, hasNextPage: page < totalPages, hasPreviousPage: page > 1, }, }; } /** * Get a single concentrator by ID with gateway count * @param id - Concentrator UUID * @returns Concentrator with gateway count or null */ export async function getById(id: string): Promise { const query = ` SELECT c.*, COALESCE(COUNT(g.id), 0)::int as gateway_count FROM concentrators c LEFT JOIN gateways g ON g.concentrator_id = c.id WHERE c.id = $1 GROUP BY c.id `; const result = await pool.query(query, [id]); return result.rows[0] || null; } /** * Create a new concentrator * @param data - Concentrator creation data * @returns Created concentrator */ export async function create(data: CreateConcentratorInput): Promise { const query = ` INSERT INTO concentrators (serial_number, name, project_id, location, type, status, ip_address, firmware_version) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING * `; const params = [ data.serial_number, data.name || null, data.project_id, data.location || null, data.type || 'LORA', data.status || 'ACTIVE', data.ip_address || null, data.firmware_version || null, ]; const result = await pool.query(query, params); return result.rows[0]; } /** * Update an existing concentrator * @param id - Concentrator UUID * @param data - Update data * @returns Updated concentrator or null if not found */ export async function update(id: string, data: UpdateConcentratorInput): Promise { // Build SET clause dynamically const updates: string[] = []; const params: unknown[] = []; let paramIndex = 1; if (data.serial_number !== undefined) { updates.push(`serial_number = $${paramIndex}`); params.push(data.serial_number); paramIndex++; } if (data.name !== undefined) { updates.push(`name = $${paramIndex}`); params.push(data.name); paramIndex++; } if (data.project_id !== undefined) { updates.push(`project_id = $${paramIndex}`); params.push(data.project_id); paramIndex++; } if (data.location !== undefined) { updates.push(`location = $${paramIndex}`); params.push(data.location); paramIndex++; } if (data.type !== undefined) { updates.push(`type = $${paramIndex}`); params.push(data.type); paramIndex++; } if (data.status !== undefined) { updates.push(`status = $${paramIndex}`); params.push(data.status); paramIndex++; } if (data.ip_address !== undefined) { updates.push(`ip_address = $${paramIndex}`); params.push(data.ip_address); paramIndex++; } if (data.firmware_version !== undefined) { updates.push(`firmware_version = $${paramIndex}`); params.push(data.firmware_version); paramIndex++; } if (updates.length === 0) { // No updates provided, return existing record const existing = await getById(id); return existing; } updates.push(`updated_at = NOW()`); const query = ` UPDATE concentrators SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING * `; params.push(id); const result = await pool.query(query, params); return result.rows[0] || null; } /** * Delete a concentrator * Checks for dependent gateways before deletion * @param id - Concentrator UUID * @returns True if deleted, throws error if has dependencies */ export async function remove(id: string): Promise { // Check for dependent gateways const gatewayCheck = await pool.query( 'SELECT COUNT(*) FROM gateways WHERE concentrator_id = $1', [id] ); const gatewayCount = parseInt(gatewayCheck.rows[0].count, 10); if (gatewayCount > 0) { throw new Error(`Cannot delete concentrator: ${gatewayCount} gateway(s) are associated with it`); } const result = await pool.query( 'DELETE FROM concentrators WHERE id = $1 RETURNING id', [id] ); return result.rowCount !== null && result.rowCount > 0; }