Decouple legacy migration service
This commit is contained in:
@@ -30,6 +30,9 @@ It must support circuits, device rows, project devices, drag-and-drop restructur
|
|||||||
directly; do not reintroduce a standalone runtime revision repository.
|
directly; do not reintroduce a standalone runtime revision repository.
|
||||||
- Runtime domain services receive narrow reader/store dependencies explicitly;
|
- Runtime domain services receive narrow reader/store dependencies explicitly;
|
||||||
concrete SQLite repositories are instantiated only under `src/server/composition`.
|
concrete SQLite repositories are instantiated only under `src/server/composition`.
|
||||||
|
- The upgrade-only legacy migration service follows the same dependency rule;
|
||||||
|
its concrete repositories are instantiated only in
|
||||||
|
`scripts/db-migrate-legacy-consumers.ts`.
|
||||||
- General Circuit, CircuitDeviceRow, CircuitList and DistributionBoard
|
- General Circuit, CircuitDeviceRow, CircuitList and DistributionBoard
|
||||||
repositories expose only active reads. Runtime writes belong in typed command
|
repositories expose only active reads. Runtime writes belong in typed command
|
||||||
repositories; direct integration fixtures belong under `tests/support`.
|
repositories; direct integration fixtures belong under `tests/support`.
|
||||||
|
|||||||
@@ -249,6 +249,9 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät.
|
|||||||
- `npm run db:backup` erzeugt ein konsistentes und verifiziertes Online-Backup.
|
- `npm run db:backup` erzeugt ein konsistentes und verifiziertes Online-Backup.
|
||||||
- Angewendete Migrationen werden niemals nachträglich verändert.
|
- Angewendete Migrationen werden niemals nachträglich verändert.
|
||||||
- `db:migrate:legacy-consumers` ist Upgrade-Werkzeug, kein Anwendungspfad.
|
- `db:migrate:legacy-consumers` ist Upgrade-Werkzeug, kein Anwendungspfad.
|
||||||
|
Der fachliche Migrationsdienst kennt nur schmale Reader-/Store-Ports unter
|
||||||
|
`src/domain/ports`; konkrete SQLite-Repositories werden ausschließlich im
|
||||||
|
CLI-Skript zusammengesetzt.
|
||||||
- Allgemeine Circuit-, Gerätezeilen-, CircuitList- und DistributionBoard-
|
- Allgemeine Circuit-, Gerätezeilen-, CircuitList- und DistributionBoard-
|
||||||
Repositories stellen im Anwendungspfad nur noch benötigte Leseabfragen bereit.
|
Repositories stellen im Anwendungspfad nur noch benötigte Leseabfragen bereit.
|
||||||
Fachliche Schreibvorgänge liegen in den typisierten Command-Repositories;
|
Fachliche Schreibvorgänge liegen in den typisierten Command-Repositories;
|
||||||
|
|||||||
@@ -340,7 +340,8 @@ Implemented foundation:
|
|||||||
- circuit-device-row transaction tests cover both successful commits and forced SQLite rollbacks
|
- circuit-device-row transaction tests cover both successful commits and forced SQLite rollbacks
|
||||||
- project-device row synchronization has real multi-row SQLite commit and rollback coverage
|
- project-device row synchronization has real multi-row SQLite commit and rollback coverage
|
||||||
- BMK swaps and section reorder have real SQLite commit and rollback coverage
|
- BMK swaps and section reorder have real SQLite commit and rollback coverage
|
||||||
- the legacy consumer migration receives its database dependency at the script entry point
|
- the legacy consumer migration service depends only on narrow domain ports;
|
||||||
|
all concrete repositories are composed at the script entry point
|
||||||
- legacy circuit, row, mapping and report writes have real late-failure rollback coverage
|
- legacy circuit, row, mapping and report writes have real late-failure rollback coverage
|
||||||
- circuit and device-row persistence value mapping is separated from the general repositories
|
- circuit and device-row persistence value mapping is separated from the general repositories
|
||||||
- obsolete direct Circuit, CircuitDeviceRow, CircuitList, DistributionBoard and
|
- obsolete direct Circuit, CircuitDeviceRow, CircuitList, DistributionBoard and
|
||||||
|
|||||||
+2
-2
@@ -14,8 +14,8 @@
|
|||||||
"build:api": "tsc -p tsconfig.json",
|
"build:api": "tsc -p tsconfig.json",
|
||||||
"build:web": "next build",
|
"build:web": "next build",
|
||||||
"start": "node dist/server/index.js",
|
"start": "node dist/server/index.js",
|
||||||
"test": "tsx --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board-structure-project-command.repository.test.ts tests/project-location-structure-project-command.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.persistence.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-version-history.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
"test": "tsx --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.service.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board-structure-project-command.repository.test.ts tests/project-location-structure-project-command.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.persistence.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-version-history.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
||||||
"test:watch": "tsx --watch --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board-structure-project-command.repository.test.ts tests/project-location-structure-project-command.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.persistence.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-version-history.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
"test:watch": "tsx --watch --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.service.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board-structure-project-command.repository.test.ts tests/project-location-structure-project-command.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.persistence.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-version-history.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:backup": "tsx scripts/db-backup.ts",
|
"db:backup": "tsx scripts/db-backup.ts",
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
import { db } from "../src/db/client.js";
|
import { db } from "../src/db/client.js";
|
||||||
|
import { CircuitRepository } from "../src/db/repositories/circuit.repository.js";
|
||||||
import { CircuitListRepository } from "../src/db/repositories/circuit-list.repository.js";
|
import { CircuitListRepository } from "../src/db/repositories/circuit-list.repository.js";
|
||||||
|
import { CircuitSectionRepository } from "../src/db/repositories/circuit-section.repository.js";
|
||||||
import { LegacyConsumerMigrationRepository } from "../src/db/repositories/legacy-consumer-migration.repository.js";
|
import { LegacyConsumerMigrationRepository } from "../src/db/repositories/legacy-consumer-migration.repository.js";
|
||||||
import { ProjectRepository } from "../src/db/repositories/project.repository.js";
|
import { ProjectRepository } from "../src/db/repositories/project.repository.js";
|
||||||
|
import { RoomRepository } from "../src/db/repositories/room.repository.js";
|
||||||
import { LegacyConsumerMigrationService } from "../src/domain/services/legacy-consumer-migration.service.js";
|
import { LegacyConsumerMigrationService } from "../src/domain/services/legacy-consumer-migration.service.js";
|
||||||
|
|
||||||
const projectRepository = new ProjectRepository();
|
const projectRepository = new ProjectRepository();
|
||||||
const circuitListRepository = new CircuitListRepository();
|
const circuitListRepository = new CircuitListRepository();
|
||||||
const migrationRepository = new LegacyConsumerMigrationRepository(db);
|
const migrationRepository = new LegacyConsumerMigrationRepository(db);
|
||||||
const migrationService = new LegacyConsumerMigrationService(
|
const migrationService = new LegacyConsumerMigrationService({
|
||||||
migrationRepository
|
circuitListReader: circuitListRepository,
|
||||||
);
|
sectionStore: new CircuitSectionRepository(),
|
||||||
|
circuitReader: new CircuitRepository(),
|
||||||
|
roomReader: new RoomRepository(),
|
||||||
|
migrationStore: migrationRepository,
|
||||||
|
});
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
const projects = await projectRepository.list();
|
const projects = await projectRepository.list();
|
||||||
|
|||||||
@@ -1,38 +1,17 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { eq, inArray, isNull } from "drizzle-orm";
|
import { eq, inArray, isNull } from "drizzle-orm";
|
||||||
|
import type {
|
||||||
|
LegacyMigrationCircuitInput,
|
||||||
|
LegacyMigrationReportInput,
|
||||||
|
} from "../../domain/ports/legacy-consumer-migration.store.js";
|
||||||
import type { AppDatabase } from "../database-context.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import { consumers } from "../schema/consumers.js";
|
import { consumers } from "../schema/consumers.js";
|
||||||
import { legacyConsumerCircuitMigrations } from "../schema/legacy-consumer-circuit-migrations.js";
|
import { legacyConsumerCircuitMigrations } from "../schema/legacy-consumer-circuit-migrations.js";
|
||||||
import { legacyConsumerMigrationReports } from "../schema/legacy-consumer-migration-report.js";
|
import { legacyConsumerMigrationReports } from "../schema/legacy-consumer-migration-report.js";
|
||||||
import {
|
import { toCircuitDeviceRowCreateValues } from "./circuit-device-row.persistence.js";
|
||||||
toCircuitDeviceRowCreateValues,
|
import { toCircuitCreateValues } from "./circuit.persistence.js";
|
||||||
type CircuitDeviceRowCreateInput,
|
|
||||||
} from "./circuit-device-row.persistence.js";
|
|
||||||
import {
|
|
||||||
toCircuitCreateValues,
|
|
||||||
type CircuitCreatePersistenceInput,
|
|
||||||
} from "./circuit.persistence.js";
|
|
||||||
|
|
||||||
export interface LegacyMigrationCircuitPersistenceInput {
|
|
||||||
circuit: CircuitCreatePersistenceInput;
|
|
||||||
deviceRows: Array<
|
|
||||||
Omit<CircuitDeviceRowCreateInput, "circuitId"> & { legacyConsumerId: string }
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LegacyMigrationReportPersistenceInput {
|
|
||||||
legacyConsumerCount: number;
|
|
||||||
createdCircuitCount: number;
|
|
||||||
createdDeviceRowCount: number;
|
|
||||||
duplicateGroupedCount: number;
|
|
||||||
generatedIdentifierCount: number;
|
|
||||||
unassignedRowCount: number;
|
|
||||||
warningsJson: string;
|
|
||||||
generatedIdentifiersJson: string;
|
|
||||||
duplicateGroupsJson: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LegacyConsumerMigrationRepository {
|
export class LegacyConsumerMigrationRepository {
|
||||||
constructor(private readonly database: AppDatabase) {}
|
constructor(private readonly database: AppDatabase) {}
|
||||||
@@ -69,8 +48,8 @@ export class LegacyConsumerMigrationRepository {
|
|||||||
|
|
||||||
persistCircuitListMigration(input: {
|
persistCircuitListMigration(input: {
|
||||||
circuitListId: string;
|
circuitListId: string;
|
||||||
circuits: LegacyMigrationCircuitPersistenceInput[];
|
circuits: LegacyMigrationCircuitInput[];
|
||||||
report: LegacyMigrationReportPersistenceInput;
|
report: LegacyMigrationReportInput;
|
||||||
}) {
|
}) {
|
||||||
if (input.circuits.some((entry) => entry.circuit.circuitListId !== input.circuitListId)) {
|
if (input.circuits.some((entry) => entry.circuit.circuitListId !== input.circuitListId)) {
|
||||||
throw new Error("All migrated circuits must belong to the target circuit list.");
|
throw new Error("All migrated circuits must belong to the target circuit list.");
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
export interface LegacyConsumerMigrationCircuitListReader {
|
||||||
|
findById(
|
||||||
|
projectId: string,
|
||||||
|
circuitListId: string
|
||||||
|
): Promise<{ id: string } | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyConsumerMigrationSection {
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
prefix: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyConsumerMigrationSectionStore {
|
||||||
|
createDefaults(circuitListId: string): Promise<void>;
|
||||||
|
listByCircuitList(
|
||||||
|
circuitListId: string
|
||||||
|
): Promise<LegacyConsumerMigrationSection[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyConsumerMigrationCircuitReader {
|
||||||
|
listByCircuitList(circuitListId: string): Promise<
|
||||||
|
Array<{
|
||||||
|
sectionId: string;
|
||||||
|
equipmentIdentifier: string;
|
||||||
|
sortOrder: number;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyConsumerMigrationRoomReader {
|
||||||
|
listByProject(projectId: string): Promise<
|
||||||
|
Array<{
|
||||||
|
id: string;
|
||||||
|
roomNumber: string;
|
||||||
|
roomName: string;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyConsumerSource {
|
||||||
|
id: string;
|
||||||
|
projectDeviceId: string | null;
|
||||||
|
roomId: string | null;
|
||||||
|
circuitNumber: string | null;
|
||||||
|
description: string | null;
|
||||||
|
name: string;
|
||||||
|
category: string | null;
|
||||||
|
deviceType: string | null;
|
||||||
|
phaseType: string | null;
|
||||||
|
tradeOrCostGroup: string | null;
|
||||||
|
protectionType: string | null;
|
||||||
|
protectionRatedCurrent: number | null;
|
||||||
|
protectionCharacteristic: string | null;
|
||||||
|
cableType: string | null;
|
||||||
|
cableCrossSection: string | null;
|
||||||
|
comment: string | null;
|
||||||
|
quantity: number;
|
||||||
|
installedPowerPerUnitKw: number;
|
||||||
|
demandFactor: number;
|
||||||
|
voltageV: number | null;
|
||||||
|
phaseCount: number | null;
|
||||||
|
powerFactor: number | null;
|
||||||
|
note: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyMigrationDeviceRowInput {
|
||||||
|
linkedProjectDeviceId?: string;
|
||||||
|
legacyConsumerId: string;
|
||||||
|
sortOrder: number;
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
phaseType?: string;
|
||||||
|
connectionKind?: string;
|
||||||
|
costGroup?: string;
|
||||||
|
category?: string;
|
||||||
|
level?: string;
|
||||||
|
roomId?: string;
|
||||||
|
roomNumberSnapshot?: string;
|
||||||
|
roomNameSnapshot?: string;
|
||||||
|
quantity: number;
|
||||||
|
powerPerUnit: number;
|
||||||
|
simultaneityFactor: number;
|
||||||
|
cosPhi?: number;
|
||||||
|
remark?: string;
|
||||||
|
overriddenFields?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyMigrationCircuitInput {
|
||||||
|
circuit: {
|
||||||
|
circuitListId: string;
|
||||||
|
sectionId: string;
|
||||||
|
equipmentIdentifier: string;
|
||||||
|
displayName?: string;
|
||||||
|
sortOrder: number;
|
||||||
|
protectionType?: string;
|
||||||
|
protectionRatedCurrent?: number;
|
||||||
|
protectionCharacteristic?: string;
|
||||||
|
cableType?: string;
|
||||||
|
cableCrossSection?: string;
|
||||||
|
cableLength?: number;
|
||||||
|
voltage?: number;
|
||||||
|
controlRequirement?: string;
|
||||||
|
remark?: string;
|
||||||
|
rcdAssignment?: string;
|
||||||
|
terminalDesignation?: string;
|
||||||
|
status?: string;
|
||||||
|
isReserve?: boolean;
|
||||||
|
};
|
||||||
|
deviceRows: LegacyMigrationDeviceRowInput[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyMigrationReportInput {
|
||||||
|
legacyConsumerCount: number;
|
||||||
|
createdCircuitCount: number;
|
||||||
|
createdDeviceRowCount: number;
|
||||||
|
duplicateGroupedCount: number;
|
||||||
|
generatedIdentifierCount: number;
|
||||||
|
unassignedRowCount: number;
|
||||||
|
warningsJson: string;
|
||||||
|
generatedIdentifiersJson: string;
|
||||||
|
duplicateGroupsJson: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyConsumerMigrationStore {
|
||||||
|
listSourceConsumersByCircuitList(
|
||||||
|
circuitListId: string
|
||||||
|
): Promise<LegacyConsumerSource[]>;
|
||||||
|
listMigratedConsumerIds(circuitListId: string): Promise<string[]>;
|
||||||
|
persistCircuitListMigration(input: {
|
||||||
|
circuitListId: string;
|
||||||
|
circuits: LegacyMigrationCircuitInput[];
|
||||||
|
report: LegacyMigrationReportInput;
|
||||||
|
}): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyConsumerMigrationDependencies {
|
||||||
|
circuitListReader: LegacyConsumerMigrationCircuitListReader;
|
||||||
|
sectionStore: LegacyConsumerMigrationSectionStore;
|
||||||
|
circuitReader: LegacyConsumerMigrationCircuitReader;
|
||||||
|
roomReader: LegacyConsumerMigrationRoomReader;
|
||||||
|
migrationStore: LegacyConsumerMigrationStore;
|
||||||
|
}
|
||||||
@@ -1,25 +1,19 @@
|
|||||||
import { CircuitRepository } from "../../db/repositories/circuit.repository.js";
|
|
||||||
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
|
|
||||||
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
|
|
||||||
import {
|
|
||||||
LegacyConsumerMigrationRepository,
|
|
||||||
type LegacyMigrationCircuitPersistenceInput,
|
|
||||||
} from "../../db/repositories/legacy-consumer-migration.repository.js";
|
|
||||||
import { RoomRepository } from "../../db/repositories/room.repository.js";
|
|
||||||
import type { LegacyMigrationReport } from "../models/circuit-tree.model.js";
|
import type { LegacyMigrationReport } from "../models/circuit-tree.model.js";
|
||||||
|
import type {
|
||||||
|
LegacyConsumerMigrationDependencies,
|
||||||
|
LegacyConsumerSource,
|
||||||
|
LegacyMigrationCircuitInput,
|
||||||
|
} from "../ports/legacy-consumer-migration.store.js";
|
||||||
import {
|
import {
|
||||||
inferSectionKeyFromEquipmentIdentifier,
|
inferSectionKeyFromEquipmentIdentifier,
|
||||||
inferSectionKeyFromLegacyInput,
|
inferSectionKeyFromLegacyInput,
|
||||||
normalizeCircuitNumber,
|
normalizeCircuitNumber,
|
||||||
} from "./legacy-consumer-migration-planner.js";
|
} from "./legacy-consumer-migration-planner.js";
|
||||||
|
|
||||||
type LegacyConsumerRow = Awaited<
|
function parseEquipmentSequence(
|
||||||
ReturnType<
|
equipmentIdentifier: string,
|
||||||
LegacyConsumerMigrationRepository["listSourceConsumersByCircuitList"]
|
prefix: string
|
||||||
>
|
): number | null {
|
||||||
>[number];
|
|
||||||
|
|
||||||
function parseEquipmentSequence(equipmentIdentifier: string, prefix: string): number | null {
|
|
||||||
if (!equipmentIdentifier.startsWith(prefix)) {
|
if (!equipmentIdentifier.startsWith(prefix)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -30,43 +24,45 @@ function parseEquipmentSequence(equipmentIdentifier: string, prefix: string): nu
|
|||||||
return Number(suffix);
|
return Number(suffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class LegacyConsumerMigrationService {
|
export class LegacyConsumerMigrationService {
|
||||||
private readonly circuitListRepository = new CircuitListRepository();
|
|
||||||
private readonly sectionRepository = new CircuitSectionRepository();
|
|
||||||
private readonly circuitRepository = new CircuitRepository();
|
|
||||||
private readonly roomRepository = new RoomRepository();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly migrationRepository: LegacyConsumerMigrationRepository
|
private readonly dependencies: LegacyConsumerMigrationDependencies
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async migrateCircuitList(projectId: string, circuitListId: string): Promise<LegacyMigrationReport> {
|
async migrateCircuitList(
|
||||||
|
projectId: string,
|
||||||
|
circuitListId: string
|
||||||
|
): Promise<LegacyMigrationReport> {
|
||||||
// Migration is additive: legacy consumers are preserved, and circuit-first entities are created
|
// Migration is additive: legacy consumers are preserved, and circuit-first entities are created
|
||||||
// with mapping records so transition remains auditable and reversible.
|
// with mapping records so transition remains auditable and reversible.
|
||||||
const list = await this.circuitListRepository.findById(projectId, circuitListId);
|
const list = await this.dependencies.circuitListReader.findById(
|
||||||
|
projectId,
|
||||||
|
circuitListId
|
||||||
|
);
|
||||||
if (!list) {
|
if (!list) {
|
||||||
throw new Error("Circuit list not found in project.");
|
throw new Error("Circuit list not found in project.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.sectionRepository.createDefaults(circuitListId);
|
await this.dependencies.sectionStore.createDefaults(circuitListId);
|
||||||
const sections = await this.sectionRepository.listByCircuitList(circuitListId);
|
const sections =
|
||||||
|
await this.dependencies.sectionStore.listByCircuitList(circuitListId);
|
||||||
const sectionByKey = new Map(sections.map((section) => [section.key, section]));
|
const sectionByKey = new Map(sections.map((section) => [section.key, section]));
|
||||||
const unassignedSection = sectionByKey.get("unassigned");
|
const unassignedSection = sectionByKey.get("unassigned");
|
||||||
if (!unassignedSection) {
|
if (!unassignedSection) {
|
||||||
throw new Error("Unassigned section is required.");
|
throw new Error("Unassigned section is required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingCircuits = await this.circuitRepository.listByCircuitList(circuitListId);
|
const existingCircuits =
|
||||||
|
await this.dependencies.circuitReader.listByCircuitList(circuitListId);
|
||||||
const usedEquipmentIdentifiers = new Set(
|
const usedEquipmentIdentifiers = new Set(
|
||||||
existingCircuits.map((circuit) => circuit.equipmentIdentifier.toUpperCase())
|
existingCircuits.map((circuit) => circuit.equipmentIdentifier.toUpperCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
const legacyConsumers =
|
const legacyConsumers =
|
||||||
await this.migrationRepository.listSourceConsumersByCircuitList(
|
await this.dependencies.migrationStore.listSourceConsumersByCircuitList(
|
||||||
circuitListId
|
circuitListId
|
||||||
);
|
);
|
||||||
const rooms = await this.roomRepository.listByProject(projectId);
|
const rooms = await this.dependencies.roomReader.listByProject(projectId);
|
||||||
const roomById = new Map(rooms.map((room) => [room.id, room]));
|
const roomById = new Map(rooms.map((room) => [room.id, room]));
|
||||||
|
|
||||||
const report: LegacyMigrationReport = {
|
const report: LegacyMigrationReport = {
|
||||||
@@ -82,14 +78,16 @@ export class LegacyConsumerMigrationService {
|
|||||||
|
|
||||||
// Idempotency guard: skip consumers already mapped in previous migration run.
|
// Idempotency guard: skip consumers already mapped in previous migration run.
|
||||||
const migratedConsumerIds = new Set(
|
const migratedConsumerIds = new Set(
|
||||||
await this.migrationRepository.listMigratedConsumerIds(circuitListId)
|
await this.dependencies.migrationStore.listMigratedConsumerIds(
|
||||||
|
circuitListId
|
||||||
|
)
|
||||||
);
|
);
|
||||||
const consumersToMigrate = legacyConsumers.filter((consumer) => !migratedConsumerIds.has(consumer.id));
|
const consumersToMigrate = legacyConsumers.filter((consumer) => !migratedConsumerIds.has(consumer.id));
|
||||||
|
|
||||||
// Legacy rows are grouped by normalized circuit number so duplicates become
|
// Legacy rows are grouped by normalized circuit number so duplicates become
|
||||||
// multiple device rows within one circuit instead of duplicate circuits.
|
// multiple device rows within one circuit instead of duplicate circuits.
|
||||||
const byNormalizedCircuitNumber = new Map<string, LegacyConsumerRow[]>();
|
const byNormalizedCircuitNumber = new Map<string, LegacyConsumerSource[]>();
|
||||||
const withoutNormalizedCircuitNumber: LegacyConsumerRow[] = [];
|
const withoutNormalizedCircuitNumber: LegacyConsumerSource[] = [];
|
||||||
for (const consumer of consumersToMigrate) {
|
for (const consumer of consumersToMigrate) {
|
||||||
const normalized = normalizeCircuitNumber(consumer.circuitNumber ?? null);
|
const normalized = normalizeCircuitNumber(consumer.circuitNumber ?? null);
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
@@ -110,7 +108,7 @@ export class LegacyConsumerMigrationService {
|
|||||||
|
|
||||||
const groups: Array<{
|
const groups: Array<{
|
||||||
equipmentIdentifier: string | null;
|
equipmentIdentifier: string | null;
|
||||||
consumers: LegacyConsumerRow[];
|
consumers: LegacyConsumerSource[];
|
||||||
inferredSectionKey: string | null;
|
inferredSectionKey: string | null;
|
||||||
isGeneratedIdentifier: boolean;
|
isGeneratedIdentifier: boolean;
|
||||||
}> = [];
|
}> = [];
|
||||||
@@ -152,7 +150,7 @@ export class LegacyConsumerMigrationService {
|
|||||||
maxBySectionPrefix.set(section.prefix.toUpperCase(), Math.max(current, sequence));
|
maxBySectionPrefix.set(section.prefix.toUpperCase(), Math.max(current, sequence));
|
||||||
}
|
}
|
||||||
|
|
||||||
const migrationCircuits: LegacyMigrationCircuitPersistenceInput[] = [];
|
const migrationCircuits: LegacyMigrationCircuitInput[] = [];
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
const representative = group.consumers[0];
|
const representative = group.consumers[0];
|
||||||
let section = group.inferredSectionKey ? sectionByKey.get(group.inferredSectionKey) : null;
|
let section = group.inferredSectionKey ? sectionByKey.get(group.inferredSectionKey) : null;
|
||||||
@@ -181,7 +179,7 @@ export class LegacyConsumerMigrationService {
|
|||||||
|
|
||||||
usedEquipmentIdentifiers.add(equipmentIdentifier.toUpperCase());
|
usedEquipmentIdentifiers.add(equipmentIdentifier.toUpperCase());
|
||||||
|
|
||||||
const deviceRows: LegacyMigrationCircuitPersistenceInput["deviceRows"] = [];
|
const deviceRows: LegacyMigrationCircuitInput["deviceRows"] = [];
|
||||||
const circuitSortOrder = nextSortOrder;
|
const circuitSortOrder = nextSortOrder;
|
||||||
nextSortOrder += 10;
|
nextSortOrder += 10;
|
||||||
|
|
||||||
@@ -250,7 +248,7 @@ export class LegacyConsumerMigrationService {
|
|||||||
report.warnings.push(`Consumer ${row.consumerId} was migrated into unassigned section.`);
|
report.warnings.push(`Consumer ${row.consumerId} was migrated into unassigned section.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.migrationRepository.persistCircuitListMigration({
|
this.dependencies.migrationStore.persistCircuitListMigration({
|
||||||
circuitListId,
|
circuitListId,
|
||||||
circuits: migrationCircuits,
|
circuits: migrationCircuits,
|
||||||
report: {
|
report: {
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import type {
|
||||||
|
LegacyConsumerMigrationDependencies,
|
||||||
|
LegacyMigrationCircuitInput,
|
||||||
|
LegacyMigrationReportInput,
|
||||||
|
} from "../src/domain/ports/legacy-consumer-migration.store.js";
|
||||||
|
import { LegacyConsumerMigrationService } from "../src/domain/services/legacy-consumer-migration.service.js";
|
||||||
|
|
||||||
|
describe("legacy consumer migration service", () => {
|
||||||
|
it("plans the migration through injected readers and persists one complete batch", async () => {
|
||||||
|
let defaultsCreatedFor: string | null = null;
|
||||||
|
let persisted:
|
||||||
|
| {
|
||||||
|
circuitListId: string;
|
||||||
|
circuits: LegacyMigrationCircuitInput[];
|
||||||
|
report: LegacyMigrationReportInput;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
const dependencies: LegacyConsumerMigrationDependencies = {
|
||||||
|
circuitListReader: {
|
||||||
|
async findById(projectId, circuitListId) {
|
||||||
|
assert.equal(projectId, "project-1");
|
||||||
|
assert.equal(circuitListId, "list-1");
|
||||||
|
return { id: circuitListId };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sectionStore: {
|
||||||
|
async createDefaults(circuitListId) {
|
||||||
|
defaultsCreatedFor = circuitListId;
|
||||||
|
},
|
||||||
|
async listByCircuitList() {
|
||||||
|
return [
|
||||||
|
{ id: "section-1", key: "single_phase", prefix: "-2F" },
|
||||||
|
{ id: "section-2", key: "unassigned", prefix: "-XF" },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
circuitReader: {
|
||||||
|
async listByCircuitList() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
sectionId: "section-1",
|
||||||
|
equipmentIdentifier: "-2F2",
|
||||||
|
sortOrder: 10,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
roomReader: {
|
||||||
|
async listByProject() {
|
||||||
|
return [
|
||||||
|
{ id: "room-1", roomNumber: "1.01", roomName: "Besprechung" },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
migrationStore: {
|
||||||
|
async listSourceConsumersByCircuitList() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "consumer-1",
|
||||||
|
projectDeviceId: null,
|
||||||
|
roomId: "room-1",
|
||||||
|
circuitNumber: null,
|
||||||
|
description: "Steckdosen Besprechung",
|
||||||
|
name: "Steckdose",
|
||||||
|
category: null,
|
||||||
|
deviceType: null,
|
||||||
|
phaseType: null,
|
||||||
|
tradeOrCostGroup: "KG 440",
|
||||||
|
protectionType: null,
|
||||||
|
protectionRatedCurrent: null,
|
||||||
|
protectionCharacteristic: null,
|
||||||
|
cableType: null,
|
||||||
|
cableCrossSection: null,
|
||||||
|
comment: null,
|
||||||
|
quantity: 2,
|
||||||
|
installedPowerPerUnitKw: 0.2,
|
||||||
|
demandFactor: 0.8,
|
||||||
|
voltageV: 230,
|
||||||
|
phaseCount: 1,
|
||||||
|
powerFactor: 0.95,
|
||||||
|
note: "Bestand",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
async listMigratedConsumerIds() {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
persistCircuitListMigration(input) {
|
||||||
|
persisted = input;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const report = await new LegacyConsumerMigrationService(
|
||||||
|
dependencies
|
||||||
|
).migrateCircuitList("project-1", "list-1");
|
||||||
|
|
||||||
|
assert.equal(defaultsCreatedFor, "list-1");
|
||||||
|
assert.deepEqual(report.generatedIdentifiers, ["-2F3"]);
|
||||||
|
assert.equal(report.createdCircuitCount, 1);
|
||||||
|
assert.equal(report.createdDeviceRowCount, 1);
|
||||||
|
assert.equal(persisted?.circuitListId, "list-1");
|
||||||
|
assert.equal(persisted?.circuits[0].circuit.sectionId, "section-1");
|
||||||
|
assert.equal(
|
||||||
|
persisted?.circuits[0].circuit.equipmentIdentifier,
|
||||||
|
"-2F3"
|
||||||
|
);
|
||||||
|
assert.deepEqual(persisted?.circuits[0].deviceRows[0], {
|
||||||
|
linkedProjectDeviceId: undefined,
|
||||||
|
legacyConsumerId: "consumer-1",
|
||||||
|
sortOrder: 10,
|
||||||
|
name: "Steckdose",
|
||||||
|
displayName: "Steckdosen Besprechung",
|
||||||
|
phaseType: undefined,
|
||||||
|
connectionKind: undefined,
|
||||||
|
costGroup: "KG 440",
|
||||||
|
category: undefined,
|
||||||
|
roomId: "room-1",
|
||||||
|
roomNumberSnapshot: "1.01",
|
||||||
|
roomNameSnapshot: "Besprechung",
|
||||||
|
quantity: 2,
|
||||||
|
powerPerUnit: 0.2,
|
||||||
|
simultaneityFactor: 0.8,
|
||||||
|
cosPhi: 0.95,
|
||||||
|
remark: "Bestand",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user