Verify legacy migration completion
This commit is contained in:
@@ -43,6 +43,11 @@ Run in this order for local database workflows:
|
|||||||
5. Migrate legacy consumers:
|
5. Migrate legacy consumers:
|
||||||
- `npm run db:migrate:legacy-consumers`
|
- `npm run db:migrate:legacy-consumers`
|
||||||
|
|
||||||
|
The migration command finishes with a cutover verification across the complete
|
||||||
|
database. It exits with an error while any legacy consumer lacks a migration
|
||||||
|
mapping, including consumers that cannot be migrated because they have no circuit
|
||||||
|
list assignment. Do not remove or disable the legacy path until this check passes.
|
||||||
|
|
||||||
## Validation Checks
|
## Validation Checks
|
||||||
|
|
||||||
After migration, verify:
|
After migration, verify:
|
||||||
@@ -72,6 +77,6 @@ Actions:
|
|||||||
|
|
||||||
Do not delete legacy consumers yet.
|
Do not delete legacy consumers yet.
|
||||||
|
|
||||||
- legacy endpoints/views may still depend on them
|
- legacy endpoints/views remain enabled only until the cutover verification passes
|
||||||
- migration trace tables reference old/new mapping
|
- migration trace tables reference old/new mapping
|
||||||
- keeping legacy rows allows comparison and rollback validation during transition
|
- keeping legacy rows allows comparison and rollback validation during transition
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import { LegacyConsumerMigrationService } from "../src/domain/services/legacy-co
|
|||||||
|
|
||||||
const projectRepository = new ProjectRepository();
|
const projectRepository = new ProjectRepository();
|
||||||
const circuitListRepository = new CircuitListRepository();
|
const circuitListRepository = new CircuitListRepository();
|
||||||
|
const migrationRepository = new LegacyConsumerMigrationRepository(db);
|
||||||
const migrationService = new LegacyConsumerMigrationService(
|
const migrationService = new LegacyConsumerMigrationService(
|
||||||
new LegacyConsumerMigrationRepository(db)
|
migrationRepository
|
||||||
);
|
);
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
@@ -30,8 +31,21 @@ async function run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const unmigratedConsumers =
|
||||||
|
await migrationRepository.listUnmigratedConsumers();
|
||||||
|
if (unmigratedConsumers.length > 0) {
|
||||||
|
const withoutCircuitList = unmigratedConsumers.filter(
|
||||||
|
(consumer) => !consumer.circuitListId
|
||||||
|
).length;
|
||||||
|
throw new Error(
|
||||||
|
`Cutover verification failed: ${unmigratedConsumers.length} legacy consumer(s) remain unmigrated` +
|
||||||
|
` (${withoutCircuitList} without a circuit list).`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Legacy consumer migration summary:");
|
console.log("Legacy consumer migration summary:");
|
||||||
console.table(reports);
|
console.table(reports);
|
||||||
|
console.log("Cutover verification passed: all legacy consumers are mapped.");
|
||||||
}
|
}
|
||||||
|
|
||||||
run().catch((error) => {
|
run().catch((error) => {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { eq, inArray } from "drizzle-orm";
|
import { eq, inArray, isNull } from "drizzle-orm";
|
||||||
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 { 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 {
|
||||||
@@ -36,6 +37,21 @@ export interface LegacyMigrationReportPersistenceInput {
|
|||||||
export class LegacyConsumerMigrationRepository {
|
export class LegacyConsumerMigrationRepository {
|
||||||
constructor(private readonly database: AppDatabase) {}
|
constructor(private readonly database: AppDatabase) {}
|
||||||
|
|
||||||
|
async listUnmigratedConsumers() {
|
||||||
|
return this.database
|
||||||
|
.select({
|
||||||
|
id: consumers.id,
|
||||||
|
projectId: consumers.projectId,
|
||||||
|
circuitListId: consumers.circuitListId,
|
||||||
|
})
|
||||||
|
.from(consumers)
|
||||||
|
.leftJoin(
|
||||||
|
legacyConsumerCircuitMigrations,
|
||||||
|
eq(legacyConsumerCircuitMigrations.consumerId, consumers.id)
|
||||||
|
)
|
||||||
|
.where(isNull(legacyConsumerCircuitMigrations.consumerId));
|
||||||
|
}
|
||||||
|
|
||||||
async listMigratedConsumerIds(circuitListId: string) {
|
async listMigratedConsumerIds(circuitListId: string) {
|
||||||
const rows = await this.database
|
const rows = await this.database
|
||||||
.select({ consumerId: legacyConsumerCircuitMigrations.consumerId })
|
.select({ consumerId: legacyConsumerCircuitMigrations.consumerId })
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { LegacyConsumerMigrationRepository } from "../src/db/repositories/legacy
|
|||||||
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
|
||||||
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
import { circuitSections } from "../src/db/schema/circuit-sections.js";
|
||||||
import { circuits } from "../src/db/schema/circuits.js";
|
import { circuits } from "../src/db/schema/circuits.js";
|
||||||
|
import { consumers } from "../src/db/schema/consumers.js";
|
||||||
import { legacyConsumerCircuitMigrations } from "../src/db/schema/legacy-consumer-circuit-migrations.js";
|
import { legacyConsumerCircuitMigrations } from "../src/db/schema/legacy-consumer-circuit-migrations.js";
|
||||||
import { legacyConsumerMigrationReports } from "../src/db/schema/legacy-consumer-migration-report.js";
|
import { legacyConsumerMigrationReports } from "../src/db/schema/legacy-consumer-migration-report.js";
|
||||||
import { projects } from "../src/db/schema/projects.js";
|
import { projects } from "../src/db/schema/projects.js";
|
||||||
@@ -97,6 +98,61 @@ function assertNoMigrationWrites(context: DatabaseContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("legacy consumer migration repository", () => {
|
describe("legacy consumer migration repository", () => {
|
||||||
|
it("reports every consumer without a completed migration mapping", async () => {
|
||||||
|
const { context, circuitListId, sectionId } = createTestDatabase();
|
||||||
|
try {
|
||||||
|
context.db
|
||||||
|
.insert(consumers)
|
||||||
|
.values([
|
||||||
|
{
|
||||||
|
id: "consumer-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
circuitListId,
|
||||||
|
name: "Light 1",
|
||||||
|
quantity: 1,
|
||||||
|
installedPowerPerUnitKw: 0.1,
|
||||||
|
demandFactor: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "consumer-2",
|
||||||
|
projectId: "project-1",
|
||||||
|
circuitListId: null,
|
||||||
|
name: "Light 2",
|
||||||
|
quantity: 1,
|
||||||
|
installedPowerPerUnitKw: 0.2,
|
||||||
|
demandFactor: 1,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.run();
|
||||||
|
const repository = new LegacyConsumerMigrationRepository(context.db);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
(await repository.listUnmigratedConsumers()).sort((left, right) =>
|
||||||
|
left.id.localeCompare(right.id)
|
||||||
|
),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "consumer-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
circuitListId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "consumer-2",
|
||||||
|
projectId: "project-1",
|
||||||
|
circuitListId: null,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
repository.persistCircuitListMigration(
|
||||||
|
migrationInput(circuitListId, sectionId)
|
||||||
|
);
|
||||||
|
assert.deepEqual(await repository.listUnmigratedConsumers(), []);
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("commits circuits, rows, mappings and report together", () => {
|
it("commits circuits, rows, mappings and report together", () => {
|
||||||
const { context, circuitListId, sectionId } = createTestDatabase();
|
const { context, circuitListId, sectionId } = createTestDatabase();
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user