Create protected board defaults

This commit is contained in:
2026-07-30 19:30:53 +02:00
parent 012308984d
commit 9d4d4082fe
10 changed files with 504 additions and 156 deletions
@@ -2,14 +2,12 @@ import { and, asc, eq, inArray } from "drizzle-orm";
import {
assertDistributionBoardDeleteProjectCommand,
assertDistributionBoardInsertProjectCommand,
createDistributionBoardDeleteProjectCommand,
createDistributionBoardInsertProjectCommand,
distributionBoardDeleteCommandType,
distributionBoardInsertCommandType,
invertDistributionBoardStructureProjectCommand,
normalizeDistributionBoardStructureProjectCommand,
type DistributionBoardStructureProjectCommand,
type DistributionBoardStructureSnapshot,
legacyDistributionBoardStructureCommandSchemaVersion,
type NormalizedDistributionBoardStructureSnapshot,
} from "../../domain/models/distribution-board-structure-project-command.model.js";
import {
assertDistributionBoardUpdateProjectCommand,
@@ -24,11 +22,11 @@ import type {
DistributionBoardStructureProjectCommandStore,
ExecuteDistributionBoardStructureCommandInput,
} from "../../domain/ports/distribution-board-structure-project-command.store.js";
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js";
import type { AppDatabase } from "../database-context.js";
import { circuitLists } from "../schema/circuit-lists.js";
import { circuitSections } from "../schema/circuit-sections.js";
import { circuits } from "../schema/circuits.js";
import { distributionBoardComponents } from "../schema/distribution-board-components.js";
import { distributionBoards } from "../schema/distribution-boards.js";
import { floors } from "../schema/floors.js";
import { projects } from "../schema/projects.js";
@@ -69,31 +67,15 @@ export class DistributionBoardStructureProjectCommandRepository
projectId: string,
command: DistributionBoardStructureProjectCommand
): DistributionBoardStructureProjectCommand {
const normalized =
const structure =
normalizeDistributionBoardStructureProjectCommand(command);
if (normalized.type === distributionBoardInsertCommandType) {
assertDistributionBoardInsertProjectCommand(normalized);
const inverse = this.insert(
database,
projectId,
normalized.payload.structure
);
return command.schemaVersion ===
legacyDistributionBoardStructureCommandSchemaVersion
? toLegacyStructureCommand(inverse)
: inverse;
if (command.type === distributionBoardInsertCommandType) {
this.insert(database, projectId, structure);
return invertDistributionBoardStructureProjectCommand(command);
}
if (normalized.type === distributionBoardDeleteCommandType) {
assertDistributionBoardDeleteProjectCommand(normalized);
const inverse = this.delete(
database,
projectId,
normalized.payload.structure
);
return command.schemaVersion ===
legacyDistributionBoardStructureCommandSchemaVersion
? toLegacyStructureCommand(inverse)
: inverse;
if (command.type === distributionBoardDeleteCommandType) {
this.delete(database, projectId, structure);
return invertDistributionBoardStructureProjectCommand(command);
}
throw new Error("Unsupported distribution-board structure command.");
}
@@ -101,7 +83,7 @@ export class DistributionBoardStructureProjectCommandRepository
private insert(
database: AppDatabase,
projectId: string,
structure: DistributionBoardStructureSnapshot
structure: NormalizedDistributionBoardStructureSnapshot
) {
if (
structure.distributionBoard.projectId !== projectId ||
@@ -160,7 +142,26 @@ export class DistributionBoardStructureProjectCommandRepository
)
.limit(1)
.get();
if (existingBoard || existingList || existingSection) {
const existingComponent =
structure.components.length === 0
? undefined
: database
.select({ id: distributionBoardComponents.id })
.from(distributionBoardComponents)
.where(
inArray(
distributionBoardComponents.id,
structure.components.map((component) => component.id)
)
)
.limit(1)
.get();
if (
existingBoard ||
existingList ||
existingSection ||
existingComponent
) {
throw new Error("Distribution-board structure id already exists.");
}
@@ -171,15 +172,20 @@ export class DistributionBoardStructureProjectCommandRepository
database.insert(circuitLists).values(structure.circuitList).run();
database
.insert(circuitSections)
.values(structure.sections.map(withInitialGroupIdentity))
.values(structure.sections)
.run();
return createDistributionBoardDeleteProjectCommand(structure);
if (structure.components.length > 0) {
database
.insert(distributionBoardComponents)
.values(structure.components)
.run();
}
}
private delete(
database: AppDatabase,
projectId: string,
structure: DistributionBoardStructureSnapshot
structure: NormalizedDistributionBoardStructureSnapshot
) {
const board = database
.select()
@@ -202,11 +208,30 @@ export class DistributionBoardStructureProjectCommandRepository
asc(circuitSections.id)
)
.all();
const components = database
.select()
.from(distributionBoardComponents)
.where(
eq(
distributionBoardComponents.circuitListId,
structure.circuitList.id
)
)
.orderBy(
asc(distributionBoardComponents.sortOrder),
asc(distributionBoardComponents.id)
)
.all();
if (
!board ||
!list ||
board.projectId !== projectId ||
!structureMatches(structure, { board, list, sections })
!structureMatches(structure, {
board,
list,
sections,
components,
})
) {
throw new Error(
"Distribution-board structure changed before deletion."
@@ -239,7 +264,6 @@ export class DistributionBoardStructureProjectCommandRepository
if (deleted.changes !== 1) {
throw new Error("Distribution board could not be deleted.");
}
return createDistributionBoardInsertProjectCommand(structure);
}
private update(
@@ -354,28 +378,6 @@ function assertSupplyTypeEnabled(
}
}
function toLegacyStructureCommand(
command:
| ReturnType<typeof createDistributionBoardInsertProjectCommand>
| ReturnType<typeof createDistributionBoardDeleteProjectCommand>
): DistributionBoardStructureProjectCommand {
const {
floorId: _floorId,
supplyType: _supplyType,
...distributionBoard
} = command.payload.structure.distributionBoard;
return {
schemaVersion: legacyDistributionBoardStructureCommandSchemaVersion,
type: command.type,
payload: {
structure: {
...command.payload.structure,
distributionBoard,
},
},
} as DistributionBoardStructureProjectCommand;
}
function getDistributionBoardFieldValue<
TField extends DistributionBoardUpdateField,
>(
@@ -386,11 +388,12 @@ function getDistributionBoardFieldValue<
}
function structureMatches(
expected: DistributionBoardStructureSnapshot,
expected: NormalizedDistributionBoardStructureSnapshot,
actual: {
board: typeof distributionBoards.$inferSelect;
list: typeof circuitLists.$inferSelect;
sections: Array<typeof circuitSections.$inferSelect>;
components: Array<typeof distributionBoardComponents.$inferSelect>;
}
) {
const {
@@ -404,7 +407,8 @@ function structureMatches(
actualDistributionBoard
) ||
!sameRecord(expected.circuitList, actual.list) ||
expected.sections.length !== actual.sections.length
expected.sections.length !== actual.sections.length ||
expected.components.length !== actual.components.length
) {
return false;
}
@@ -413,59 +417,32 @@ function structureMatches(
left.sortOrder - right.sortOrder ||
left.id.localeCompare(right.id)
);
return expectedSections.every((section, index) => {
const actualSection = actual.sections[index];
if (
actualSection.category !== initialCategoryBySectionKey(section.key) ||
actualSection.groupNumber !== initialGroupNumberBySectionKey(section.key)
) {
return false;
}
const {
category: _category,
groupNumber: _groupNumber,
...comparableActualSection
} = actualSection;
return sameRecord(section, comparableActualSection);
});
}
function withInitialGroupIdentity(
section: DistributionBoardStructureSnapshot["sections"][number]
): typeof circuitSections.$inferInsert {
return {
...section,
category: initialCategoryBySectionKey(section.key),
groupNumber: initialGroupNumberBySectionKey(section.key),
};
}
function initialCategoryBySectionKey(
key: string
): CircuitGroupCategory | null {
if (
key === "lighting" ||
key === "single_phase" ||
key === "three_phase"
) {
return key;
}
return null;
}
function initialGroupNumberBySectionKey(key: string): number | null {
return initialCategoryBySectionKey(key) === null ? null : 1;
}
function sameRecord(
expected: Record<string, unknown>,
actual: Record<string, unknown>
) {
const expectedEntries = Object.entries(expected);
const sectionsMatch = expectedSections.every((section, index) =>
sameRecord(section, actual.sections[index])
);
const expectedComponents = [...expected.components].sort(
(left, right) =>
left.sortOrder - right.sortOrder ||
left.id.localeCompare(right.id)
);
return (
expectedEntries.length === Object.keys(actual).length &&
expectedEntries.every(
([key, value]) => actual[key] === value
sectionsMatch &&
expectedComponents.every((component, index) =>
sameRecord(component, actual.components[index])
)
);
}
function sameRecord(
expected: object,
actual: object
) {
const expectedEntries = Object.entries(expected);
const actualRecord = actual as Record<string, unknown>;
return (
expectedEntries.length === Object.keys(actual).length &&
expectedEntries.every(
([key, value]) => actualRecord[key] === value
)
);
}