Complete project scaffold with working auth, REST API, Prisma/SQLite schema, Docker config, and React frontend for both Rack Planner and Service Mapper modules. Both server and client pass TypeScript strict mode with zero errors. Initial migration applied. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
148 lines
4.2 KiB
TypeScript
148 lines
4.2 KiB
TypeScript
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<boolean> {
|
||
const modules = await prisma.module.findMany({
|
||
where: { rackId, ...(excludeModuleId ? { id: { not: excludeModuleId } } : {}) },
|
||
select: { uPosition: true, uSize: true },
|
||
});
|
||
|
||
const occupied = new Set<number>();
|
||
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 } });
|
||
}
|
||
|
||
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 } } },
|
||
});
|
||
}
|