import { prisma } from '../lib/prisma'; import { AppError } from '../types/index'; import { MODULE_PORT_DEFAULTS, MODULE_U_DEFAULTS, type ModuleType, type PortType } from '../lib/constants'; const moduleInclude = { ports: { orderBy: { portNumber: 'asc' as const }, include: { vlans: { include: { vlan: true } }, }, }, }; /** Check whether a U-range is occupied in a rack, optionally excluding one module (for moves). */ async function hasCollision( rackId: string, uPosition: number, uSize: number, excludeModuleId?: string ): Promise { const modules = await prisma.module.findMany({ where: { rackId, ...(excludeModuleId ? { id: { not: excludeModuleId } } : {}) }, select: { uPosition: true, uSize: true }, }); const occupied = new Set(); for (const m of modules) { for (let u = m.uPosition; u < m.uPosition + m.uSize; u++) occupied.add(u); } for (let u = uPosition; u < uPosition + uSize; u++) { if (occupied.has(u)) return true; } return false; } export async function createModule( rackId: string, data: { name: string; type: ModuleType; uPosition: number; uSize?: number; manufacturer?: string; model?: string; ipAddress?: string; notes?: string; portCount?: number; portType?: PortType; } ) { const rack = await prisma.rack.findUnique({ where: { id: rackId } }); if (!rack) throw new AppError('Rack not found', 404, 'NOT_FOUND'); const uSize = data.uSize ?? MODULE_U_DEFAULTS[data.type] ?? 1; if (data.uPosition < 1 || data.uPosition + uSize - 1 > rack.totalU) { throw new AppError( `Module does not fit within rack (U1–U${rack.totalU})`, 400, 'OUT_OF_BOUNDS' ); } if (await hasCollision(rackId, data.uPosition, uSize)) { throw new AppError('U-slot collision: another module occupies that space', 409, 'COLLISION'); } const portCount = data.portCount ?? MODULE_PORT_DEFAULTS[data.type] ?? 0; const portType: PortType = data.portType ?? 'ETHERNET'; return prisma.module.create({ data: { rackId, name: data.name, type: data.type, uPosition: data.uPosition, uSize, manufacturer: data.manufacturer, model: data.model, ipAddress: data.ipAddress, notes: data.notes, ports: { create: Array.from({ length: portCount }, (_, i) => ({ portNumber: i + 1, portType, })), }, }, include: moduleInclude, }); } export async function updateModule( id: string, data: Partial<{ name: string; uPosition: number; uSize: number; manufacturer: string; model: string; ipAddress: string; notes: string; }> ) { const existing = await prisma.module.findUnique({ where: { id } }); if (!existing) throw new AppError('Module not found', 404, 'NOT_FOUND'); const newPosition = data.uPosition ?? existing.uPosition; const newSize = data.uSize ?? existing.uSize; if (data.uPosition !== undefined || data.uSize !== undefined) { const rack = await prisma.rack.findUnique({ where: { id: existing.rackId } }); if (!rack) throw new AppError('Rack not found', 404, 'NOT_FOUND'); if (newPosition < 1 || newPosition + newSize - 1 > rack.totalU) { throw new AppError( `Module does not fit within rack (U1–U${rack.totalU})`, 400, 'OUT_OF_BOUNDS' ); } if (await hasCollision(existing.rackId, newPosition, newSize, id)) { throw new AppError('U-slot collision', 409, 'COLLISION'); } } return prisma.module.update({ where: { id }, data, include: moduleInclude }); } export async function deleteModule(id: string) { const existing = await prisma.module.findUnique({ where: { id } }); if (!existing) throw new AppError('Module not found', 404, 'NOT_FOUND'); return prisma.module.delete({ where: { id } }); } /** * Move a module to a new rack and/or U-position. * Ports and VLAN assignments move with the module (they're linked by moduleId). */ export async function moveModule( id: string, targetRackId: string, targetUPosition: number ) { const existing = await prisma.module.findUnique({ where: { id } }); if (!existing) throw new AppError('Module not found', 404, 'NOT_FOUND'); const targetRack = await prisma.rack.findUnique({ where: { id: targetRackId } }); if (!targetRack) throw new AppError('Target rack not found', 404, 'NOT_FOUND'); if (targetUPosition < 1 || targetUPosition + existing.uSize - 1 > targetRack.totalU) { throw new AppError( `Module does not fit within target rack (U1–U${targetRack.totalU})`, 400, 'OUT_OF_BOUNDS' ); } // Collision check in target rack, excluding self (handles same-rack moves) const excludeInTarget = targetRackId === existing.rackId ? id : undefined; if (await hasCollision(targetRackId, targetUPosition, existing.uSize, excludeInTarget)) { throw new AppError('U-slot collision in target rack', 409, 'COLLISION'); } return prisma.module.update({ where: { id }, data: { rackId: targetRackId, uPosition: targetUPosition }, include: moduleInclude, }); } export async function getModulePorts(id: string) { const existing = await prisma.module.findUnique({ where: { id } }); if (!existing) throw new AppError('Module not found', 404, 'NOT_FOUND'); return prisma.port.findMany({ where: { moduleId: id }, orderBy: { portNumber: 'asc' }, include: { vlans: { include: { vlan: true } } }, }); }