Persist circuit reorders

This commit is contained in:
2026-07-25 21:22:10 +02:00
parent 08a2775a88
commit abcb468807
26 changed files with 914 additions and 372 deletions
@@ -1,8 +1,18 @@
import { and, eq } from "drizzle-orm";
import {
assertCircuitSectionReorderProjectCommand,
circuitSectionReorderCommandType,
createCircuitSectionReorderProjectCommand,
type CircuitSectionReorderAssignment,
type CircuitSectionReorderProjectCommand,
} from "../../domain/models/circuit-section-reorder-project-command.model.js";
import {
assertCircuitSectionsReorderProjectCommand,
circuitSectionsReorderCommandType,
createCircuitSectionsReorderProjectCommand,
type CircuitSectionsReorderProjectCommand,
type CircuitSectionsReorderSection,
} from "../../domain/models/circuit-sections-reorder-project-command.model.js";
import type {
CircuitSectionReorderProjectCommandStore,
ExecuteCircuitSectionReorderCommandInput,
@@ -14,106 +24,49 @@ import { circuits } from "../schema/circuits.js";
import { applyProjectHistoryTransition } from "./project-history.persistence.js";
import { appendProjectRevision } from "./project-revision.persistence.js";
type ReorderHistoryCommand =
| CircuitSectionReorderProjectCommand
| CircuitSectionsReorderProjectCommand;
export class CircuitSectionReorderProjectCommandRepository
implements CircuitSectionReorderProjectCommandStore
{
constructor(private readonly database: AppDatabase) {}
execute(input: ExecuteCircuitSectionReorderCommandInput) {
assertCircuitSectionReorderProjectCommand(input.command);
this.assertSupportedCommand(input.command);
return this.database.transaction((tx) => {
const section = tx
.select({
id: circuitSections.id,
circuitListId: circuitSections.circuitListId,
projectId: circuitLists.projectId,
})
.from(circuitSections)
.innerJoin(
circuitLists,
eq(circuitLists.id, circuitSections.circuitListId)
)
.where(eq(circuitSections.id, input.command.payload.sectionId))
.get();
if (!section || section.projectId !== input.projectId) {
throw new Error(
"Circuit section does not belong to project."
const commandSections = this.getCommandSections(input.command);
const inverseSections = commandSections.map((section) => {
this.assertCompleteCurrentSection(
tx,
input.projectId,
section
);
return {
sectionId: section.sectionId,
assignments: section.assignments.map((assignment) => ({
circuitId: assignment.circuitId,
expectedSortOrder: assignment.targetSortOrder,
targetSortOrder: assignment.expectedSortOrder,
})),
};
});
for (const section of commandSections) {
this.applyAssignments(tx, section);
}
const persistedCircuits = tx
.select({
id: circuits.id,
circuitListId: circuits.circuitListId,
sortOrder: circuits.sortOrder,
})
.from(circuits)
.where(eq(circuits.sectionId, section.id))
.all();
const assignments = input.command.payload.assignments;
if (
persistedCircuits.length !== assignments.length ||
persistedCircuits.some(
(circuit) =>
circuit.circuitListId !== section.circuitListId
)
) {
throw new Error(
"Circuit reorder must include every circuit in the section."
);
}
const circuitsById = new Map(
persistedCircuits.map((circuit) => [circuit.id, circuit])
);
for (const assignment of assignments) {
const circuit = circuitsById.get(assignment.circuitId);
if (
!circuit ||
circuit.sortOrder !== assignment.expectedSortOrder
) {
throw new Error(
"Circuit changed before section reorder execution."
);
}
}
const inverse = createCircuitSectionReorderProjectCommand(
section.id,
assignments.map((assignment) => ({
circuitId: assignment.circuitId,
expectedSortOrder: assignment.targetSortOrder,
targetSortOrder: assignment.expectedSortOrder,
}))
);
for (const assignment of assignments) {
if (
assignment.expectedSortOrder === assignment.targetSortOrder
) {
continue;
}
const updated = tx
.update(circuits)
.set({ sortOrder: assignment.targetSortOrder })
.where(
and(
eq(circuits.id, assignment.circuitId),
eq(circuits.sectionId, section.id),
eq(
circuits.sortOrder,
assignment.expectedSortOrder
)
const inverse =
input.command.type === circuitSectionReorderCommandType
? createCircuitSectionReorderProjectCommand(
inverseSections[0].sectionId,
inverseSections[0].assignments
)
)
.run();
if (updated.changes !== 1) {
throw new Error(
"Circuit changed during section reorder execution."
);
}
}
: createCircuitSectionsReorderProjectCommand(
inverseSections
);
const revision = appendProjectRevision(tx, {
projectId: input.projectId,
expectedRevision: input.expectedRevision,
@@ -132,4 +85,125 @@ export class CircuitSectionReorderProjectCommandRepository
return { revision, inverse };
});
}
private assertSupportedCommand(
command: ReorderHistoryCommand
) {
if (command.type === circuitSectionReorderCommandType) {
assertCircuitSectionReorderProjectCommand(command);
return;
}
if (command.type === circuitSectionsReorderCommandType) {
assertCircuitSectionsReorderProjectCommand(command);
return;
}
throw new Error("Unsupported circuit section reorder command.");
}
private getCommandSections(
command: ReorderHistoryCommand
): CircuitSectionsReorderSection[] {
if (command.type === circuitSectionReorderCommandType) {
return [
{
sectionId: command.payload.sectionId,
assignments: command.payload.assignments,
},
];
}
return command.payload.sections;
}
private assertCompleteCurrentSection(
database: AppDatabase,
projectId: string,
sectionCommand: CircuitSectionsReorderSection
) {
const section = database
.select({
id: circuitSections.id,
circuitListId: circuitSections.circuitListId,
projectId: circuitLists.projectId,
})
.from(circuitSections)
.innerJoin(
circuitLists,
eq(circuitLists.id, circuitSections.circuitListId)
)
.where(eq(circuitSections.id, sectionCommand.sectionId))
.get();
if (!section || section.projectId !== projectId) {
throw new Error(
"Circuit section does not belong to project."
);
}
const persistedCircuits = database
.select({
id: circuits.id,
circuitListId: circuits.circuitListId,
sortOrder: circuits.sortOrder,
})
.from(circuits)
.where(eq(circuits.sectionId, section.id))
.all();
const assignments = sectionCommand.assignments;
if (
persistedCircuits.length !== assignments.length ||
persistedCircuits.some(
(circuit) =>
circuit.circuitListId !== section.circuitListId
)
) {
throw new Error(
"Circuit reorder must include every circuit in the section."
);
}
const circuitsById = new Map(
persistedCircuits.map((circuit) => [circuit.id, circuit])
);
for (const assignment of assignments) {
const circuit = circuitsById.get(assignment.circuitId);
if (
!circuit ||
circuit.sortOrder !== assignment.expectedSortOrder
) {
throw new Error(
"Circuit changed before section reorder execution."
);
}
}
}
private applyAssignments(
database: AppDatabase,
sectionCommand: {
sectionId: string;
assignments: CircuitSectionReorderAssignment[];
}
) {
for (const assignment of sectionCommand.assignments) {
if (
assignment.expectedSortOrder === assignment.targetSortOrder
) {
continue;
}
const updated = database
.update(circuits)
.set({ sortOrder: assignment.targetSortOrder })
.where(
and(
eq(circuits.id, assignment.circuitId),
eq(circuits.sectionId, sectionCommand.sectionId),
eq(circuits.sortOrder, assignment.expectedSortOrder)
)
)
.run();
if (updated.changes !== 1) {
throw new Error(
"Circuit changed during section reorder execution."
);
}
}
}
}