183 lines
5.8 KiB
TypeScript
183 lines
5.8 KiB
TypeScript
import { ProjectHistoryOperationUnavailableError } from "../errors/project-history-operation-unavailable.error.js";
|
|
import {
|
|
assertCircuitDeviceRowUpdateProjectCommand,
|
|
circuitDeviceRowUpdateCommandType,
|
|
} from "../models/circuit-device-row-project-command.model.js";
|
|
import {
|
|
assertCircuitDeviceRowDeleteProjectCommand,
|
|
assertCircuitDeviceRowInsertProjectCommand,
|
|
circuitDeviceRowDeleteCommandType,
|
|
circuitDeviceRowInsertCommandType,
|
|
} from "../models/circuit-device-row-structure-project-command.model.js";
|
|
import {
|
|
assertCircuitUpdateProjectCommand,
|
|
circuitUpdateCommandType,
|
|
} from "../models/circuit-project-command.model.js";
|
|
import {
|
|
assertCircuitDeleteProjectCommand,
|
|
assertCircuitInsertProjectCommand,
|
|
circuitDeleteCommandType,
|
|
circuitInsertCommandType,
|
|
} from "../models/circuit-structure-project-command.model.js";
|
|
import {
|
|
assertSerializedProjectCommand,
|
|
type SerializedProjectCommand,
|
|
} from "../models/project-command.model.js";
|
|
import type { CircuitDeviceRowProjectCommandStore } from "../ports/circuit-device-row-project-command.store.js";
|
|
import type { CircuitDeviceRowStructureProjectCommandStore } from "../ports/circuit-device-row-structure-project-command.store.js";
|
|
import type { CircuitProjectCommandStore } from "../ports/circuit-project-command.store.js";
|
|
import type { CircuitStructureProjectCommandStore } from "../ports/circuit-structure-project-command.store.js";
|
|
import type {
|
|
ExecuteProjectHistoryCommandInput,
|
|
ExecutedProjectCommand,
|
|
ExecuteUserProjectCommandInput,
|
|
ProjectCommandExecutor,
|
|
} from "../ports/project-command.executor.js";
|
|
import type {
|
|
ProjectHistoryDirection,
|
|
ProjectHistoryStore,
|
|
} from "../ports/project-history.store.js";
|
|
import type { ProjectRevisionSource } from "../ports/project-revision.store.js";
|
|
|
|
interface DispatchProjectCommandInput {
|
|
projectId: string;
|
|
expectedRevision: number;
|
|
source: ProjectRevisionSource;
|
|
description?: string;
|
|
actorId?: string;
|
|
historyTargetChangeSetId?: string;
|
|
command: SerializedProjectCommand;
|
|
}
|
|
|
|
export class ProjectCommandService implements ProjectCommandExecutor {
|
|
constructor(
|
|
private readonly circuitStore: CircuitProjectCommandStore,
|
|
private readonly deviceRowStore: CircuitDeviceRowProjectCommandStore,
|
|
private readonly deviceRowStructureStore: CircuitDeviceRowStructureProjectCommandStore,
|
|
private readonly circuitStructureStore: CircuitStructureProjectCommandStore,
|
|
private readonly historyStore: ProjectHistoryStore
|
|
) {}
|
|
|
|
executeUser(
|
|
input: ExecuteUserProjectCommandInput
|
|
): ExecutedProjectCommand {
|
|
assertExpectedRevision(input.expectedRevision);
|
|
assertSerializedProjectCommand(input.command);
|
|
return this.dispatch({
|
|
...input,
|
|
source: "user",
|
|
command: input.command,
|
|
});
|
|
}
|
|
|
|
undo(
|
|
input: ExecuteProjectHistoryCommandInput
|
|
): ExecutedProjectCommand {
|
|
return this.executeHistory(input, "undo");
|
|
}
|
|
|
|
redo(
|
|
input: ExecuteProjectHistoryCommandInput
|
|
): ExecutedProjectCommand {
|
|
return this.executeHistory(input, "redo");
|
|
}
|
|
|
|
private executeHistory(
|
|
input: ExecuteProjectHistoryCommandInput,
|
|
direction: ProjectHistoryDirection
|
|
) {
|
|
assertExpectedRevision(input.expectedRevision);
|
|
const step = this.historyStore.getNextCommand(
|
|
input.projectId,
|
|
direction
|
|
);
|
|
if (!step) {
|
|
throw new ProjectHistoryOperationUnavailableError(
|
|
input.projectId,
|
|
direction
|
|
);
|
|
}
|
|
|
|
return this.dispatch({
|
|
...input,
|
|
source: direction,
|
|
description: `${direction === "undo" ? "Undo" : "Redo"} ${step.command.type}`,
|
|
historyTargetChangeSetId: step.changeSetId,
|
|
command: step.command,
|
|
});
|
|
}
|
|
|
|
private dispatch(
|
|
input: DispatchProjectCommandInput
|
|
): ExecutedProjectCommand {
|
|
const revision = this.executeTypedCommand(input);
|
|
const history = this.historyStore.getState(input.projectId);
|
|
if (!history) {
|
|
throw new Error("Project history disappeared after command execution.");
|
|
}
|
|
return { revision, history };
|
|
}
|
|
|
|
private executeTypedCommand(input: DispatchProjectCommandInput) {
|
|
switch (input.command.type) {
|
|
case circuitUpdateCommandType: {
|
|
assertCircuitUpdateProjectCommand(input.command);
|
|
return this.circuitStore.executeUpdate({
|
|
...input,
|
|
command: input.command,
|
|
}).revision;
|
|
}
|
|
case circuitDeviceRowUpdateCommandType: {
|
|
assertCircuitDeviceRowUpdateProjectCommand(input.command);
|
|
return this.deviceRowStore.executeUpdate({
|
|
...input,
|
|
command: input.command,
|
|
}).revision;
|
|
}
|
|
case circuitDeviceRowInsertCommandType: {
|
|
assertCircuitDeviceRowInsertProjectCommand(input.command);
|
|
return this.deviceRowStructureStore.execute({
|
|
...input,
|
|
command: input.command,
|
|
}).revision;
|
|
}
|
|
case circuitDeviceRowDeleteCommandType: {
|
|
assertCircuitDeviceRowDeleteProjectCommand(input.command);
|
|
return this.deviceRowStructureStore.execute({
|
|
...input,
|
|
command: input.command,
|
|
}).revision;
|
|
}
|
|
case circuitInsertCommandType: {
|
|
assertCircuitInsertProjectCommand(input.command);
|
|
return this.circuitStructureStore.execute({
|
|
...input,
|
|
command: input.command,
|
|
}).revision;
|
|
}
|
|
case circuitDeleteCommandType: {
|
|
assertCircuitDeleteProjectCommand(input.command);
|
|
return this.circuitStructureStore.execute({
|
|
...input,
|
|
command: input.command,
|
|
}).revision;
|
|
}
|
|
default:
|
|
throw new Error(
|
|
`Unsupported project command type: ${input.command.type}.`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function assertExpectedRevision(expectedRevision: number) {
|
|
if (
|
|
!Number.isSafeInteger(expectedRevision) ||
|
|
expectedRevision < 0
|
|
) {
|
|
throw new Error(
|
|
"Expected project revision must be a non-negative integer."
|
|
);
|
|
}
|
|
}
|