Documentation
This commit is contained in:
@@ -39,6 +39,7 @@ export class CircuitWriteService {
|
||||
this.numberingService = deps?.numberingService ?? new CircuitNumberingService();
|
||||
}
|
||||
|
||||
// Ensures writes never connect a section to the wrong circuit list.
|
||||
private async assertSectionInList(sectionId: string, circuitListId: string) {
|
||||
const section = await this.circuitSectionRepository.findById(sectionId);
|
||||
if (!section) {
|
||||
@@ -50,6 +51,7 @@ export class CircuitWriteService {
|
||||
return section;
|
||||
}
|
||||
|
||||
// Enforces BMK uniqueness inside one circuit list.
|
||||
private async assertUniqueEquipmentIdentifier(
|
||||
circuitListId: string,
|
||||
equipmentIdentifier: string,
|
||||
@@ -65,6 +67,7 @@ export class CircuitWriteService {
|
||||
}
|
||||
}
|
||||
|
||||
// Validates linked project-device id against owning project of the circuit list.
|
||||
private async assertValidLinkedProjectDevice(circuitId: string, linkedProjectDeviceId?: string) {
|
||||
if (!linkedProjectDeviceId) {
|
||||
return;
|
||||
@@ -184,6 +187,7 @@ export class CircuitWriteService {
|
||||
overriddenFields: input.overriddenFields,
|
||||
});
|
||||
|
||||
// Reserve circuits become active as soon as at least one device row exists.
|
||||
if (Boolean(circuit.isReserve)) {
|
||||
await this.circuitRepository.update(circuit.id, {
|
||||
sectionId: circuit.sectionId,
|
||||
@@ -247,6 +251,7 @@ export class CircuitWriteService {
|
||||
}
|
||||
await this.deviceRowRepository.delete(rowId);
|
||||
const remaining = await this.deviceRowRepository.countByCircuit(current.circuitId);
|
||||
// When last row is removed, keep circuit and mark it reserve instead of deleting it.
|
||||
if (remaining === 0) {
|
||||
await this.circuitRepository.update(circuit.id, {
|
||||
sectionId: circuit.sectionId,
|
||||
@@ -287,6 +292,7 @@ export class CircuitWriteService {
|
||||
throw new Error("Invalid target circuit id.");
|
||||
}
|
||||
|
||||
// Placeholder-target move creates a new circuit explicitly; no implicit renumbering of others.
|
||||
if (!targetCircuit) {
|
||||
if (!input.targetSectionId || !input.createNewCircuit) {
|
||||
throw new Error("Invalid move target.");
|
||||
@@ -321,6 +327,7 @@ export class CircuitWriteService {
|
||||
await this.deviceRowRepository.moveToCircuit(rowId, targetCircuit.id, (targetCount + 1) * 10);
|
||||
|
||||
const sourceRemaining = await this.deviceRowRepository.countByCircuit(sourceCircuit.id);
|
||||
// Source circuit becomes reserve when all rows are moved away.
|
||||
if (sourceRemaining === 0) {
|
||||
await this.circuitRepository.update(sourceCircuit.id, {
|
||||
sectionId: sourceCircuit.sectionId,
|
||||
@@ -342,6 +349,7 @@ export class CircuitWriteService {
|
||||
});
|
||||
}
|
||||
|
||||
// Target circuit is no longer reserve once it receives moved rows.
|
||||
if (Boolean(targetCircuit.isReserve)) {
|
||||
await this.circuitRepository.update(targetCircuit.id, {
|
||||
sectionId: targetCircuit.sectionId,
|
||||
@@ -367,6 +375,8 @@ export class CircuitWriteService {
|
||||
}
|
||||
|
||||
async moveDeviceRowsBulk(input: MoveCircuitDeviceRowsBulkInput) {
|
||||
// Bulk move keeps input order and resolves all source circuits first so undo can
|
||||
// restore per-source assignment deterministically.
|
||||
const uniqueRowIds = [...new Set(input.rowIds)];
|
||||
if (uniqueRowIds.length === 0) {
|
||||
throw new Error("No device rows provided.");
|
||||
@@ -400,6 +410,7 @@ export class CircuitWriteService {
|
||||
throw new Error("Invalid target circuit id.");
|
||||
}
|
||||
|
||||
// Bulk placeholder move creates exactly one new circuit as common target.
|
||||
if (!targetCircuit) {
|
||||
if (!input.targetSectionId || !input.createNewCircuit) {
|
||||
throw new Error("Invalid move target.");
|
||||
@@ -423,6 +434,7 @@ export class CircuitWriteService {
|
||||
}
|
||||
}
|
||||
|
||||
// Any source circuit emptied by bulk move is preserved as reserve circuit.
|
||||
for (const sourceCircuit of sourceCircuits.values()) {
|
||||
if (sourceCircuit.circuitListId !== targetCircuit.circuitListId) {
|
||||
throw new Error("All moved rows must belong to same circuit list as target.");
|
||||
@@ -493,6 +505,8 @@ export class CircuitWriteService {
|
||||
}
|
||||
|
||||
async renumberSection(sectionId: string) {
|
||||
// Explicit renumber operation for one section only.
|
||||
// Never renumbers other sections and never runs implicitly during move/sort operations.
|
||||
const section = await this.circuitSectionRepository.findById(sectionId);
|
||||
if (!section) {
|
||||
throw new Error("Invalid section id.");
|
||||
@@ -514,6 +528,7 @@ export class CircuitWriteService {
|
||||
finalAssignments.push({ id: circuit.id, equipmentIdentifier: candidate });
|
||||
index += 1;
|
||||
}
|
||||
// Uses safe two-phase identifier update to avoid UNIQUE collisions during swaps.
|
||||
await this.circuitRepository.updateEquipmentIdentifiersSafely(
|
||||
section.circuitListId,
|
||||
finalAssignments,
|
||||
@@ -551,6 +566,7 @@ export class CircuitWriteService {
|
||||
}
|
||||
|
||||
async reorderCircuitsInSection(sectionId: string, input: ReorderSectionCircuitsInput) {
|
||||
// Reorder updates sortOrder only. BMKs remain unchanged; users may renumber explicitly later.
|
||||
const section = await this.circuitSectionRepository.findById(sectionId);
|
||||
if (!section) {
|
||||
throw new Error("Invalid section id.");
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface LegacyConsumerForPlanning {
|
||||
phaseCount: number | null;
|
||||
}
|
||||
|
||||
// Accepts only normalized BMK-like legacy circuit numbers used for grouping.
|
||||
export function normalizeCircuitNumber(value: string | null): string | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
@@ -20,6 +21,8 @@ export function normalizeCircuitNumber(value: string | null): string | null {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
// Best-effort fallback when no valid circuit number exists. Keeps migration deterministic
|
||||
// by preferring explicit category/phase cues over random assignment.
|
||||
export function inferSectionKeyFromLegacyInput(consumer: LegacyConsumerForPlanning): string | null {
|
||||
const category = (consumer.category ?? "").toLowerCase();
|
||||
if (category.includes("light") || category.includes("beleuchtung")) {
|
||||
@@ -42,6 +45,7 @@ export function inferSectionKeyFromLegacyInput(consumer: LegacyConsumerForPlanni
|
||||
return null;
|
||||
}
|
||||
|
||||
// Prefix-based section inference for already normalized equipment identifiers.
|
||||
export function inferSectionKeyFromEquipmentIdentifier(equipmentIdentifier: string): string | null {
|
||||
if (equipmentIdentifier.startsWith("-1F")) {
|
||||
return "lighting";
|
||||
@@ -54,4 +58,3 @@ export function inferSectionKeyFromEquipmentIdentifier(equipmentIdentifier: stri
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ export class LegacyConsumerMigrationService {
|
||||
private readonly roomRepository = new RoomRepository();
|
||||
|
||||
async migrateCircuitList(projectId: string, circuitListId: string): Promise<LegacyMigrationReport> {
|
||||
// Migration is additive: legacy consumers are preserved, and circuit-first entities are created
|
||||
// with mapping records so transition remains auditable and reversible.
|
||||
const list = await this.circuitListRepository.findById(projectId, circuitListId);
|
||||
if (!list) {
|
||||
throw new Error("Circuit list not found in project.");
|
||||
@@ -73,6 +75,7 @@ export class LegacyConsumerMigrationService {
|
||||
warnings: [],
|
||||
};
|
||||
|
||||
// Idempotency guard: skip consumers already mapped in previous migration run.
|
||||
const migratedRows = await db
|
||||
.select({ consumerId: legacyConsumerCircuitMigrations.consumerId })
|
||||
.from(legacyConsumerCircuitMigrations)
|
||||
@@ -80,6 +83,8 @@ export class LegacyConsumerMigrationService {
|
||||
const migratedConsumerIds = new Set(migratedRows.map((row) => row.consumerId));
|
||||
const consumersToMigrate = legacyConsumers.filter((consumer) => !migratedConsumerIds.has(consumer.id));
|
||||
|
||||
// Legacy rows are grouped by normalized circuit number so duplicates become
|
||||
// multiple device rows within one circuit instead of duplicate circuits.
|
||||
const byNormalizedCircuitNumber = new Map<string, LegacyConsumerRow[]>();
|
||||
const withoutNormalizedCircuitNumber: LegacyConsumerRow[] = [];
|
||||
for (const consumer of consumersToMigrate) {
|
||||
@@ -94,6 +99,8 @@ export class LegacyConsumerMigrationService {
|
||||
byNormalizedCircuitNumber.get(normalized)!.push(consumer);
|
||||
}
|
||||
|
||||
// Duplicate normalized circuit numbers are expected and represented as one circuit
|
||||
// with multiple circuit_device_rows.
|
||||
report.groupedDuplicateCircuitNumbers = [...byNormalizedCircuitNumber.entries()]
|
||||
.filter(([, grouped]) => grouped.length > 1)
|
||||
.map(([normalizedCircuitNumber, grouped]) => ({ normalizedCircuitNumber, count: grouped.length }));
|
||||
@@ -105,6 +112,7 @@ export class LegacyConsumerMigrationService {
|
||||
isGeneratedIdentifier: boolean;
|
||||
}> = [];
|
||||
|
||||
// Stable normalized circuit numbers keep existing intent where available.
|
||||
for (const [normalizedCircuitNumber, grouped] of byNormalizedCircuitNumber.entries()) {
|
||||
groups.push({
|
||||
equipmentIdentifier: normalizedCircuitNumber,
|
||||
@@ -114,6 +122,8 @@ export class LegacyConsumerMigrationService {
|
||||
});
|
||||
}
|
||||
|
||||
// Missing/invalid circuit numbers are migrated as single-row groups with
|
||||
// generated identifiers and best-effort section inference.
|
||||
for (const consumer of withoutNormalizedCircuitNumber) {
|
||||
groups.push({
|
||||
equipmentIdentifier: null,
|
||||
|
||||
Reference in New Issue
Block a user