Add external row quantity foundation
This commit is contained in:
@@ -250,7 +250,9 @@ not change the project revision or undo/redo stacks. Restoring a server-stored
|
|||||||
snapshot verifies its checksum and the current-state hash, replaces supported
|
snapshot verifies its checksum and the current-state hash, replaces supported
|
||||||
project data atomically and records a new `restore` revision with a complete
|
project data atomically and records a new `restore` revision with a complete
|
||||||
inverse command. Restore can therefore be undone and redone after a restart.
|
inverse command. Restore can therefore be undone and redone after a restart.
|
||||||
Current snapshot schema version 4 additionally stores the complete confirmed
|
Current snapshot schema version 5 additionally stores the manual quantity share
|
||||||
|
of every CircuitDeviceRow. Version 4 snapshots are upgraded with
|
||||||
|
`manualQuantity = quantity`; they already contain the complete confirmed
|
||||||
external-model source, import batches, room mappings and objects. Version 3
|
external-model source, import batches, room mappings and objects. Version 3
|
||||||
snapshots are upgraded with an empty external-model state; version 2 snapshots
|
snapshots are upgraded with an empty external-model state; version 2 snapshots
|
||||||
are additionally upgraded with an empty external CSV configuration. Supported
|
are additionally upgraded with an empty external CSV configuration. Supported
|
||||||
@@ -263,7 +265,7 @@ project graph.
|
|||||||
Project-wide Revit CSV configuration uses
|
Project-wide Revit CSV configuration uses
|
||||||
`external-csv-configuration.update`. Its complete expected/target snapshot is
|
`external-csv-configuration.update`. Its complete expected/target snapshot is
|
||||||
persisted through the shared project-command transaction, so create, update,
|
persisted through the shared project-command transaction, so create, update,
|
||||||
delete, Undo and Redo are revision-safe. Snapshot schema 4, restore and portable
|
delete, Undo and Redo are revision-safe. Snapshot schema 5, restore and portable
|
||||||
project duplication preserve the configuration; duplication remaps its internal
|
project duplication preserve the configuration; duplication remaps its internal
|
||||||
id and project link.
|
id and project link.
|
||||||
The project page exposes the Revit CSV modal. It edits the complete
|
The project page exposes the Revit CSV modal. It edits the complete
|
||||||
@@ -277,12 +279,17 @@ immutable import batches with original bytes plus the classified cell matrix,
|
|||||||
normalized source-room mappings and external objects unique by
|
normalized source-room mappings and external objects unique by
|
||||||
`(sourceId, ifcGuid)`. Source values, planning values and overrides are separate.
|
`(sourceId, ifcGuid)`. Source values, planning values and overrides are separate.
|
||||||
The general external-model repository is read-only; runtime writes belong only
|
The general external-model repository is read-only; runtime writes belong only
|
||||||
to typed command adapters. Snapshot/restore and portable project transfer schema version 4
|
to typed command adapters. Snapshot/restore and portable project transfer schema version 5
|
||||||
capture the complete external state and remap every internal UUID and link while
|
capture the complete external state and remap every internal UUID and link while
|
||||||
preserving IFCGUIDs, source values, original bytes and the classified matrix.
|
preserving IFCGUIDs, source values, original bytes and the classified matrix.
|
||||||
Import batches additionally store the monotonic project CSV configuration
|
Import batches additionally store the monotonic project CSV configuration
|
||||||
version introduced by additive migration `0004`; it is distinct from the CSV
|
version introduced by additive migration `0004`; it is distinct from the CSV
|
||||||
configuration JSON schema version.
|
configuration JSON schema version.
|
||||||
|
Phase 14.3 migration `0005` adds `CircuitDeviceRow.manualQuantity`, backfills
|
||||||
|
existing rows from `quantity` and keeps `quantity` as the materialized total.
|
||||||
|
Normal manual row inserts and quantity edits update both values together. Future
|
||||||
|
external-object assignment commands must set the total to `manualQuantity` plus
|
||||||
|
the sum of linked objects' effective quantities in the same transaction.
|
||||||
Confirmed initial state is written only through
|
Confirmed initial state is written only through
|
||||||
`external-import.apply-initial`. The command rechecks configuration version,
|
`external-import.apply-initial`. The command rechecks configuration version,
|
||||||
original-byte SHA-256, parsed matrix, complete IFCGUID/source values, explicit
|
original-byte SHA-256, parsed matrix, complete IFCGUID/source values, explicit
|
||||||
|
|||||||
@@ -416,7 +416,9 @@ wieder her.
|
|||||||
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
|
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
|
||||||
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
|
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
|
||||||
ist für bestehende sowie neu angelegte Verteilungen standardmäßig `1`.
|
ist für bestehende sowie neu angelegte Verteilungen standardmäßig `1`.
|
||||||
Snapshot-Schema 4 enthält zusätzlich den vollständigen externen Modellzustand
|
Snapshot-Schema 5 enthält zusätzlich den manuellen Mengenanteil jeder
|
||||||
|
Gerätezeile. Version 4 wird mit `manualQuantity = quantity` hochgestuft und
|
||||||
|
enthält bereits den vollständigen externen Modellzustand
|
||||||
aus Quelle, Batches, Raum-Mappings und Objekten. Es enthält außerdem die
|
aus Quelle, Batches, Raum-Mappings und Objekten. Es enthält außerdem die
|
||||||
projektweite Revit-CSV-Konfiguration, die Gebäudekennzeichnung sowie den Gleichzeitigkeitsfaktor,
|
projektweite Revit-CSV-Konfiguration, die Gebäudekennzeichnung sowie den Gleichzeitigkeitsfaktor,
|
||||||
Gruppenkategorie und -nummer, Verteilerkomponenten sowie die getrennten
|
Gruppenkategorie und -nummer, Verteilerkomponenten sowie die getrennten
|
||||||
@@ -477,7 +479,7 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät.
|
|||||||
Ein triggergeführtes Register erzwingt bereits eine normalisierte,
|
Ein triggergeführtes Register erzwingt bereits eine normalisierte,
|
||||||
stromkreislistenweite BMK-Eindeutigkeit über Stromkreise und
|
stromkreislistenweite BMK-Eindeutigkeit über Stromkreise und
|
||||||
Verteilerkomponenten. Snapshot- und Transfer-Integration verwenden aktuell
|
Verteilerkomponenten. Snapshot- und Transfer-Integration verwenden aktuell
|
||||||
Snapshot-Schema 4. Persistente Insert/Delete/Update-Commands für
|
Snapshot-Schema 5. Persistente Insert/Delete/Update-Commands für
|
||||||
veränderliche Verteilerkomponenten, Gruppen einschließlich befüllter
|
veränderliche Verteilerkomponenten, Gruppen einschließlich befüllter
|
||||||
Unterbäume sowie vollständige Gruppensortierung sind integriert. Der Editor
|
Unterbäume sowie vollständige Gruppensortierung sind integriert. Der Editor
|
||||||
zeigt die geschützte Struktur an und bearbeitet veränderliche Gruppen- und
|
zeigt die geschützte Struktur an und bearbeitet veränderliche Gruppen- und
|
||||||
@@ -500,7 +502,7 @@ einschließlich BOM, CRLF und Quotierung bytegleich zurück. Der lokale Befehl
|
|||||||
|
|
||||||
`external_csv_configurations` enthält höchstens eine versionierte Konfiguration
|
`external_csv_configurations` enthält höchstens eine versionierte Konfiguration
|
||||||
je Projekt. `external-csv-configuration.update` schreibt Anlage, Änderung oder
|
je Projekt. `external-csv-configuration.update` schreibt Anlage, Änderung oder
|
||||||
Entfernung zusammen mit Revision und persistentem Undo/Redo. Snapshot-Schema 4,
|
Entfernung zusammen mit Revision und persistentem Undo/Redo. Snapshot-Schema 5,
|
||||||
Restore und portabler Projekttransfer enthalten diesen Zustand; beim Duplizieren
|
Restore und portabler Projekttransfer enthalten diesen Zustand; beim Duplizieren
|
||||||
werden Konfigurations-UUID und Projektlink remapped.
|
werden Konfigurations-UUID und Projektlink remapped.
|
||||||
|
|
||||||
@@ -516,7 +518,9 @@ und CircuitDeviceRow bleiben nullable. `ExternalModelStateRepository` liest den
|
|||||||
vollständigen Zustand deterministisch und unterscheidet unbekannte von noch
|
vollständigen Zustand deterministisch und unterscheidet unbekannte von noch
|
||||||
nicht importierten Projekten. Der letzte bestätigte Batch wird aus der
|
nicht importierten Projekten. Der letzte bestätigte Batch wird aus der
|
||||||
Batchreihenfolge abgeleitet statt redundant an der Quelle gespeichert.
|
Batchreihenfolge abgeleitet statt redundant an der Quelle gespeichert.
|
||||||
Snapshot/Transfer v4 erfasst Originalbytes, Matrix und alle internen Links,
|
Snapshot/Transfer v5 erfasst zusätzlich `manualQuantity`; Version 4 wird mit
|
||||||
|
dem bisherigen Gesamtwert als manuellem Anteil hochgestuft. Originalbytes,
|
||||||
|
Matrix und alle internen Links werden weiterhin vollständig erfasst,
|
||||||
stellt sie in Fremdschlüsselreihenfolge wieder her und remappt beim
|
stellt sie in Fremdschlüsselreihenfolge wieder her und remappt beim
|
||||||
Projektduplikat jede interne UUID; IFCGUID und Quelltransport bleiben
|
Projektduplikat jede interne UUID; IFCGUID und Quelltransport bleiben
|
||||||
unverändert. `external-import.apply-initial` ist die atomare Schreibgrenze für
|
unverändert. `external-import.apply-initial` ist die atomare Schreibgrenze für
|
||||||
@@ -529,6 +533,13 @@ Vorschlag entsprechen. Der Command erzeugt und verknüpft ausdrücklich keine
|
|||||||
CircuitDeviceRow; Undo entfernt den vollständigen unveränderten Importzustand,
|
CircuitDeviceRow; Undo entfernt den vollständigen unveränderten Importzustand,
|
||||||
Redo verwendet dieselben UUIDs und Bytes. Bloße Vorschau und Importplanung
|
Redo verwendet dieselben UUIDs und Bytes. Bloße Vorschau und Importplanung
|
||||||
erzeugen weiterhin keine dieser Zeilen.
|
erzeugen weiterhin keine dieser Zeilen.
|
||||||
|
|
||||||
|
Migration `0005` trennt bei `CircuitDeviceRow` den manuellen Mengenanteil vom
|
||||||
|
weiterhin materialisierten Gesamtwert. Bestehende Daten werden mit
|
||||||
|
`manualQuantity = quantity` übernommen. Die Fachregel für spätere externe
|
||||||
|
Zuordnungs-Commands lautet `quantity = manualQuantity + Summe der
|
||||||
|
effectiveQuantity aller verknüpften ExternalModelObjects`; Link und beide
|
||||||
|
Mengenwerte müssen innerhalb desselben Projekt-Commands wechseln.
|
||||||
`GET` und `PUT /api/projects/:projectId/external-csv/configuration` lesen oder
|
`GET` und `PUT /api/projects/:projectId/external-csv/configuration` lesen oder
|
||||||
ändern die Konfiguration; der PUT plant Identität und nächsten
|
ändern die Konfiguration; der PUT plant Identität und nächsten
|
||||||
Konfigurationsstand serverseitig und verwendet den typisierten Command.
|
Konfigurationsstand serverseitig und verwendet den typisierten Command.
|
||||||
|
|||||||
@@ -313,6 +313,11 @@ Kombination aus Originalbytes und Matrix als Entscheidungen bestätigt.
|
|||||||
### 14.3 – Editor und explizite Zuordnung
|
### 14.3 – Editor und explizite Zuordnung
|
||||||
|
|
||||||
1. `manualQuantity` und Mengeninvarianten migrieren und testen.
|
1. `manualQuantity` und Mengeninvarianten migrieren und testen.
|
||||||
|
**Erledigt:** Migration `0005` übernimmt für jede bestehende Gerätezeile
|
||||||
|
`manualQuantity = quantity`. Neue normale Zeilen und Mengenänderungen führen
|
||||||
|
beide Werte gemeinsam; Snapshot/Transfer v5 erhält den Anteil und stuft v4
|
||||||
|
verlustfrei hoch. Die reine Mengenregel validiert den materialisierten Wert
|
||||||
|
als manuellen Anteil plus Summe der unteilbaren externen Objektmengen.
|
||||||
2. Commands für vorhandene Row, neue Row, neuen Circuit und Lösen ergänzen.
|
2. Commands für vorhandene Row, neue Row, neuen Circuit und Lösen ergänzen.
|
||||||
3. Verteilungsbezogenen Drawer, Filter und Vorschau der Mengenwirkung ergänzen.
|
3. Verteilungsbezogenen Drawer, Filter und Vorschau der Mengenwirkung ergänzen.
|
||||||
4. Einzel- und Mehrfach-Drag-and-drop samt Warnungen, Undo/Redo und Reload
|
4. Einzel- und Mehrfach-Drag-and-drop samt Warnungen, Undo/Redo und Reload
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ const projectDeviceColumns = new Set(
|
|||||||
const missingProjectDeviceColumns = requiredProjectDeviceColumns.filter(
|
const missingProjectDeviceColumns = requiredProjectDeviceColumns.filter(
|
||||||
(name) => !projectDeviceColumns.has(name)
|
(name) => !projectDeviceColumns.has(name)
|
||||||
);
|
);
|
||||||
|
const requiredCircuitDeviceRowColumns = ["manual_quantity"];
|
||||||
|
const circuitDeviceRowColumns = new Set(
|
||||||
|
db.prepare("PRAGMA table_info(circuit_device_rows)").all().map((column) => column.name)
|
||||||
|
);
|
||||||
|
const missingCircuitDeviceRowColumns = requiredCircuitDeviceRowColumns.filter(
|
||||||
|
(name) => !circuitDeviceRowColumns.has(name)
|
||||||
|
);
|
||||||
const removedProjectDeviceColumns = [
|
const removedProjectDeviceColumns = [
|
||||||
"installed_power_per_unit_kw",
|
"installed_power_per_unit_kw",
|
||||||
"demand_factor",
|
"demand_factor",
|
||||||
@@ -131,6 +138,7 @@ console.log("Database:", dbPath);
|
|||||||
console.log("Required tables:", requiredTables.join(", "));
|
console.log("Required tables:", requiredTables.join(", "));
|
||||||
console.log("Existing tables:", [...existing].join(", ") || "(none)");
|
console.log("Existing tables:", [...existing].join(", ") || "(none)");
|
||||||
console.log("Required project-device columns:", requiredProjectDeviceColumns.join(", "));
|
console.log("Required project-device columns:", requiredProjectDeviceColumns.join(", "));
|
||||||
|
console.log("Required circuit-device-row columns:", requiredCircuitDeviceRowColumns.join(", "));
|
||||||
console.log("Required circuit columns:", requiredCircuitColumns.join(", "));
|
console.log("Required circuit columns:", requiredCircuitColumns.join(", "));
|
||||||
console.log(
|
console.log(
|
||||||
"Required circuit-section columns:",
|
"Required circuit-section columns:",
|
||||||
@@ -171,6 +179,14 @@ if (missingCircuitColumns.length > 0) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (missingCircuitDeviceRowColumns.length > 0) {
|
||||||
|
console.error(
|
||||||
|
"Missing circuit-device-row columns:",
|
||||||
|
missingCircuitDeviceRowColumns.join(", ")
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
if (remainingRemovedCircuitColumns.length > 0) {
|
if (remainingRemovedCircuitColumns.length > 0) {
|
||||||
console.error(
|
console.error(
|
||||||
"Transitional circuit protection columns still present:",
|
"Transitional circuit protection columns still present:",
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE `circuit_device_rows` ADD `manual_quantity` integer DEFAULT 0 NOT NULL;
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE `circuit_device_rows` SET `manual_quantity` = `quantity`;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,13 @@
|
|||||||
"when": 1785684457208,
|
"when": 1785684457208,
|
||||||
"tag": "0004_illegal_the_stranger",
|
"tag": "0004_illegal_the_stranger",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 5,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1785687503453,
|
||||||
|
"tag": "0005_stale_gorilla_man",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -82,6 +82,14 @@ export class CircuitDeviceRowProjectCommandRepository
|
|||||||
change.value,
|
change.value,
|
||||||
])
|
])
|
||||||
) as CircuitDeviceRowPatchInput;
|
) as CircuitDeviceRowPatchInput;
|
||||||
|
const targetQuantity = patch.quantity ?? current.quantity;
|
||||||
|
const targetManualQuantity =
|
||||||
|
patch.manualQuantity ?? current.manualQuantity;
|
||||||
|
if (targetManualQuantity > targetQuantity) {
|
||||||
|
throw new Error(
|
||||||
|
"Device-row manual quantity must not exceed total quantity."
|
||||||
|
);
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
input.source === "user" &&
|
input.source === "user" &&
|
||||||
patch.phaseType !== undefined &&
|
patch.phaseType !== undefined &&
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { circuitLists } from "../schema/circuit-lists.js";
|
|||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import {
|
import {
|
||||||
assertCircuitDeviceRowReferencesInProject,
|
assertCircuitDeviceRowReferencesInProject,
|
||||||
|
toCircuitDeviceRowInsertValues,
|
||||||
toCircuitDeviceRowSnapshot,
|
toCircuitDeviceRowSnapshot,
|
||||||
} from "./circuit-device-row-structure.persistence.js";
|
} from "./circuit-device-row-structure.persistence.js";
|
||||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
@@ -92,7 +93,10 @@ export class CircuitDeviceRowStructureProjectCommandRepository
|
|||||||
throw new Error("Circuit device-row id already exists.");
|
throw new Error("Circuit device-row id already exists.");
|
||||||
}
|
}
|
||||||
|
|
||||||
database.insert(circuitDeviceRows).values(row).run();
|
database
|
||||||
|
.insert(circuitDeviceRows)
|
||||||
|
.values(toCircuitDeviceRowInsertValues(row))
|
||||||
|
.run();
|
||||||
const circuitUpdate = database
|
const circuitUpdate = database
|
||||||
.update(circuits)
|
.update(circuits)
|
||||||
.set({ isReserve: 0 })
|
.set({ isReserve: 0 })
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export function toCircuitDeviceRowSnapshot(
|
|||||||
roomNumberSnapshot: row.roomNumberSnapshot,
|
roomNumberSnapshot: row.roomNumberSnapshot,
|
||||||
roomNameSnapshot: row.roomNameSnapshot,
|
roomNameSnapshot: row.roomNameSnapshot,
|
||||||
quantity: row.quantity,
|
quantity: row.quantity,
|
||||||
|
manualQuantity: row.manualQuantity,
|
||||||
powerPerUnit: row.powerPerUnit,
|
powerPerUnit: row.powerPerUnit,
|
||||||
simultaneityFactor: row.simultaneityFactor,
|
simultaneityFactor: row.simultaneityFactor,
|
||||||
cosPhi: row.cosPhi,
|
cosPhi: row.cosPhi,
|
||||||
@@ -68,3 +69,10 @@ export function toCircuitDeviceRowSnapshot(
|
|||||||
overriddenFields: row.overriddenFields,
|
overriddenFields: row.overriddenFields,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toCircuitDeviceRowInsertValues(row: CircuitDeviceRowSnapshot) {
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
manualQuantity: row.manualQuantity ?? row.quantity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface CircuitDeviceRowUpdateInput {
|
|||||||
roomNumberSnapshot?: string;
|
roomNumberSnapshot?: string;
|
||||||
roomNameSnapshot?: string;
|
roomNameSnapshot?: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
manualQuantity?: number;
|
||||||
powerPerUnit: number;
|
powerPerUnit: number;
|
||||||
simultaneityFactor: number;
|
simultaneityFactor: number;
|
||||||
cosPhi?: number;
|
cosPhi?: number;
|
||||||
@@ -34,6 +35,7 @@ export interface CircuitDeviceRowPatchInput {
|
|||||||
roomNumberSnapshot?: string | null;
|
roomNumberSnapshot?: string | null;
|
||||||
roomNameSnapshot?: string | null;
|
roomNameSnapshot?: string | null;
|
||||||
quantity?: number;
|
quantity?: number;
|
||||||
|
manualQuantity?: number;
|
||||||
powerPerUnit?: number;
|
powerPerUnit?: number;
|
||||||
simultaneityFactor?: number;
|
simultaneityFactor?: number;
|
||||||
cosPhi?: number | null;
|
cosPhi?: number | null;
|
||||||
@@ -61,6 +63,7 @@ export function toCircuitDeviceRowUpdateValues(input: CircuitDeviceRowUpdateInpu
|
|||||||
roomNumberSnapshot: input.roomNumberSnapshot ?? null,
|
roomNumberSnapshot: input.roomNumberSnapshot ?? null,
|
||||||
roomNameSnapshot: input.roomNameSnapshot ?? null,
|
roomNameSnapshot: input.roomNameSnapshot ?? null,
|
||||||
quantity: input.quantity,
|
quantity: input.quantity,
|
||||||
|
manualQuantity: input.manualQuantity ?? input.quantity,
|
||||||
powerPerUnit: input.powerPerUnit,
|
powerPerUnit: input.powerPerUnit,
|
||||||
simultaneityFactor: input.simultaneityFactor,
|
simultaneityFactor: input.simultaneityFactor,
|
||||||
cosPhi: input.cosPhi ?? null,
|
cosPhi: input.cosPhi ?? null,
|
||||||
@@ -90,6 +93,7 @@ export function toCircuitDeviceRowPatchValues(input: CircuitDeviceRowPatchInput)
|
|||||||
}
|
}
|
||||||
if (has("roomNameSnapshot")) values.roomNameSnapshot = input.roomNameSnapshot ?? null;
|
if (has("roomNameSnapshot")) values.roomNameSnapshot = input.roomNameSnapshot ?? null;
|
||||||
if (input.quantity !== undefined) values.quantity = input.quantity;
|
if (input.quantity !== undefined) values.quantity = input.quantity;
|
||||||
|
if (input.manualQuantity !== undefined) values.manualQuantity = input.manualQuantity;
|
||||||
if (input.powerPerUnit !== undefined) values.powerPerUnit = input.powerPerUnit;
|
if (input.powerPerUnit !== undefined) values.powerPerUnit = input.powerPerUnit;
|
||||||
if (input.simultaneityFactor !== undefined) {
|
if (input.simultaneityFactor !== undefined) {
|
||||||
values.simultaneityFactor = input.simultaneityFactor;
|
values.simultaneityFactor = input.simultaneityFactor;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export class CircuitDeviceRowRepository {
|
|||||||
roomNumberSnapshot: circuitDeviceRows.roomNumberSnapshot,
|
roomNumberSnapshot: circuitDeviceRows.roomNumberSnapshot,
|
||||||
roomNameSnapshot: circuitDeviceRows.roomNameSnapshot,
|
roomNameSnapshot: circuitDeviceRows.roomNameSnapshot,
|
||||||
quantity: circuitDeviceRows.quantity,
|
quantity: circuitDeviceRows.quantity,
|
||||||
|
manualQuantity: circuitDeviceRows.manualQuantity,
|
||||||
powerPerUnit: circuitDeviceRows.powerPerUnit,
|
powerPerUnit: circuitDeviceRows.powerPerUnit,
|
||||||
simultaneityFactor: circuitDeviceRows.simultaneityFactor,
|
simultaneityFactor: circuitDeviceRows.simultaneityFactor,
|
||||||
cosPhi: circuitDeviceRows.cosPhi,
|
cosPhi: circuitDeviceRows.cosPhi,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { circuitSections } from "../schema/circuit-sections.js";
|
|||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js";
|
import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js";
|
||||||
import { distributionBoardComponents } from "../schema/distribution-board-components.js";
|
import { distributionBoardComponents } from "../schema/distribution-board-components.js";
|
||||||
|
import { toCircuitDeviceRowInsertValues } from "./circuit-device-row-structure.persistence.js";
|
||||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
|
|
||||||
export class CircuitGroupSubtreeProjectCommandRepository
|
export class CircuitGroupSubtreeProjectCommandRepository
|
||||||
@@ -212,7 +213,10 @@ export class CircuitGroupSubtreeProjectCommandRepository
|
|||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
if (deviceRows.length > 0) {
|
if (deviceRows.length > 0) {
|
||||||
database.insert(circuitDeviceRows).values(deviceRows).run();
|
database
|
||||||
|
.insert(circuitDeviceRows)
|
||||||
|
.values(deviceRows.map(toCircuitDeviceRowInsertValues))
|
||||||
|
.run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { circuitSections } from "../schema/circuit-sections.js";
|
|||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
import {
|
import {
|
||||||
assertCircuitDeviceRowReferencesInProject,
|
assertCircuitDeviceRowReferencesInProject,
|
||||||
|
toCircuitDeviceRowInsertValues,
|
||||||
toCircuitDeviceRowSnapshot,
|
toCircuitDeviceRowSnapshot,
|
||||||
} from "./circuit-device-row-structure.persistence.js";
|
} from "./circuit-device-row-structure.persistence.js";
|
||||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
@@ -166,7 +167,10 @@ export class CircuitStructureProjectCommandRepository
|
|||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
if (snapshot.deviceRows.length > 0) {
|
if (snapshot.deviceRows.length > 0) {
|
||||||
database.insert(circuitDeviceRows).values(snapshot.deviceRows).run();
|
database
|
||||||
|
.insert(circuitDeviceRows)
|
||||||
|
.values(snapshot.deviceRows.map(toCircuitDeviceRowInsertValues))
|
||||||
|
.run();
|
||||||
}
|
}
|
||||||
if (snapshot.protectionDevice) {
|
if (snapshot.protectionDevice) {
|
||||||
database
|
database
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { floors } from "../schema/floors.js";
|
|||||||
import { projectDevices } from "../schema/project-devices.js";
|
import { projectDevices } from "../schema/project-devices.js";
|
||||||
import { projects } from "../schema/projects.js";
|
import { projects } from "../schema/projects.js";
|
||||||
import { rooms } from "../schema/rooms.js";
|
import { rooms } from "../schema/rooms.js";
|
||||||
|
import { toCircuitDeviceRowInsertValues } from "./circuit-device-row-structure.persistence.js";
|
||||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||||
|
|
||||||
export class DistributionBoardSubtreeProjectCommandRepository
|
export class DistributionBoardSubtreeProjectCommandRepository
|
||||||
@@ -253,7 +254,10 @@ export class DistributionBoardSubtreeProjectCommandRepository
|
|||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
if (deviceRows.length > 0) {
|
if (deviceRows.length > 0) {
|
||||||
database.insert(circuitDeviceRows).values(deviceRows).run();
|
database
|
||||||
|
.insert(circuitDeviceRows)
|
||||||
|
.values(deviceRows.map(toCircuitDeviceRowInsertValues))
|
||||||
|
.run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ export function readProjectStateSnapshot(
|
|||||||
roomNumberSnapshot: circuitDeviceRows.roomNumberSnapshot,
|
roomNumberSnapshot: circuitDeviceRows.roomNumberSnapshot,
|
||||||
roomNameSnapshot: circuitDeviceRows.roomNameSnapshot,
|
roomNameSnapshot: circuitDeviceRows.roomNameSnapshot,
|
||||||
quantity: circuitDeviceRows.quantity,
|
quantity: circuitDeviceRows.quantity,
|
||||||
|
manualQuantity: circuitDeviceRows.manualQuantity,
|
||||||
powerPerUnit: circuitDeviceRows.powerPerUnit,
|
powerPerUnit: circuitDeviceRows.powerPerUnit,
|
||||||
simultaneityFactor: circuitDeviceRows.simultaneityFactor,
|
simultaneityFactor: circuitDeviceRows.simultaneityFactor,
|
||||||
cosPhi: circuitDeviceRows.cosPhi,
|
cosPhi: circuitDeviceRows.cosPhi,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export const circuitDeviceRows = sqliteTable("circuit_device_rows", {
|
|||||||
roomNumberSnapshot: text("room_number_snapshot"),
|
roomNumberSnapshot: text("room_number_snapshot"),
|
||||||
roomNameSnapshot: text("room_name_snapshot"),
|
roomNameSnapshot: text("room_name_snapshot"),
|
||||||
quantity: integer("quantity").notNull(),
|
quantity: integer("quantity").notNull(),
|
||||||
|
manualQuantity: integer("manual_quantity").notNull().default(0),
|
||||||
powerPerUnit: real("power_per_unit").notNull(),
|
powerPerUnit: real("power_per_unit").notNull(),
|
||||||
simultaneityFactor: real("simultaneity_factor").notNull(),
|
simultaneityFactor: real("simultaneity_factor").notNull(),
|
||||||
cosPhi: real("cos_phi"),
|
cosPhi: real("cos_phi"),
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
export interface ExternalQuantityContribution {
|
||||||
|
effectiveQuantity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateCircuitDeviceRowQuantity(
|
||||||
|
manualQuantity: number,
|
||||||
|
externalObjects: ExternalQuantityContribution[]
|
||||||
|
) {
|
||||||
|
assertNonNegativeFinite(manualQuantity, "Manual quantity");
|
||||||
|
return externalObjects.reduce((quantity, object) => {
|
||||||
|
assertPositiveFinite(object.effectiveQuantity, "External effective quantity");
|
||||||
|
return quantity + object.effectiveQuantity;
|
||||||
|
}, manualQuantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertCircuitDeviceRowQuantity(input: {
|
||||||
|
quantity: number;
|
||||||
|
manualQuantity: number;
|
||||||
|
externalObjects: ExternalQuantityContribution[];
|
||||||
|
}) {
|
||||||
|
assertNonNegativeFinite(input.quantity, "Materialized quantity");
|
||||||
|
const expected = calculateCircuitDeviceRowQuantity(
|
||||||
|
input.manualQuantity,
|
||||||
|
input.externalObjects
|
||||||
|
);
|
||||||
|
if (input.quantity !== expected) {
|
||||||
|
throw new Error(
|
||||||
|
`Materialized quantity ${input.quantity} does not match manual and external quantity ${expected}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertNonNegativeFinite(value: number, label: string) {
|
||||||
|
if (!Number.isFinite(value) || value < 0) {
|
||||||
|
throw new Error(`${label} must be a non-negative finite number.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertPositiveFinite(value: number, label: string) {
|
||||||
|
if (!Number.isFinite(value) || value <= 0) {
|
||||||
|
throw new Error(`${label} must be a positive finite number.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ export interface CircuitDeviceRowUpdateValues {
|
|||||||
roomNumberSnapshot: string | null;
|
roomNumberSnapshot: string | null;
|
||||||
roomNameSnapshot: string | null;
|
roomNameSnapshot: string | null;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
manualQuantity: number;
|
||||||
powerPerUnit: number;
|
powerPerUnit: number;
|
||||||
simultaneityFactor: number;
|
simultaneityFactor: number;
|
||||||
cosPhi: number | null;
|
cosPhi: number | null;
|
||||||
@@ -51,7 +52,18 @@ export function createCircuitDeviceRowUpdateProjectCommand(
|
|||||||
rowId: string,
|
rowId: string,
|
||||||
patch: CircuitDeviceRowUpdatePatch
|
patch: CircuitDeviceRowUpdatePatch
|
||||||
): CircuitDeviceRowUpdateProjectCommand {
|
): CircuitDeviceRowUpdateProjectCommand {
|
||||||
const changes = Object.entries(patch).map(([field, value]) => ({
|
const normalizedPatch =
|
||||||
|
patch.quantity !== undefined && patch.manualQuantity === undefined
|
||||||
|
? { ...patch, manualQuantity: patch.quantity }
|
||||||
|
: patch;
|
||||||
|
if (
|
||||||
|
normalizedPatch.quantity !== undefined &&
|
||||||
|
normalizedPatch.manualQuantity !== undefined &&
|
||||||
|
normalizedPatch.manualQuantity > normalizedPatch.quantity
|
||||||
|
) {
|
||||||
|
throw new Error("manualQuantity must not exceed quantity.");
|
||||||
|
}
|
||||||
|
const changes = Object.entries(normalizedPatch).map(([field, value]) => ({
|
||||||
field: field as CircuitDeviceRowUpdateField,
|
field: field as CircuitDeviceRowUpdateField,
|
||||||
value,
|
value,
|
||||||
})) as CircuitDeviceRowUpdateFieldChange[];
|
})) as CircuitDeviceRowUpdateFieldChange[];
|
||||||
@@ -125,6 +137,7 @@ function assertCircuitDeviceRowUpdateFieldValue(
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
field === "quantity" ||
|
field === "quantity" ||
|
||||||
|
field === "manualQuantity" ||
|
||||||
field === "powerPerUnit" ||
|
field === "powerPerUnit" ||
|
||||||
field === "simultaneityFactor"
|
field === "simultaneityFactor"
|
||||||
) {
|
) {
|
||||||
@@ -164,6 +177,7 @@ function isCircuitDeviceRowUpdateField(
|
|||||||
"roomNumberSnapshot",
|
"roomNumberSnapshot",
|
||||||
"roomNameSnapshot",
|
"roomNameSnapshot",
|
||||||
"quantity",
|
"quantity",
|
||||||
|
"manualQuantity",
|
||||||
"powerPerUnit",
|
"powerPerUnit",
|
||||||
"simultaneityFactor",
|
"simultaneityFactor",
|
||||||
"cosPhi",
|
"cosPhi",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface CircuitDeviceRowSnapshot {
|
|||||||
roomNumberSnapshot: string | null;
|
roomNumberSnapshot: string | null;
|
||||||
roomNameSnapshot: string | null;
|
roomNameSnapshot: string | null;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
manualQuantity?: number;
|
||||||
powerPerUnit: number;
|
powerPerUnit: number;
|
||||||
simultaneityFactor: number;
|
simultaneityFactor: number;
|
||||||
cosPhi: number | null;
|
cosPhi: number | null;
|
||||||
@@ -57,10 +58,14 @@ export type CircuitDeviceRowStructureProjectCommand =
|
|||||||
export function createCircuitDeviceRowInsertProjectCommand(
|
export function createCircuitDeviceRowInsertProjectCommand(
|
||||||
row: CircuitDeviceRowSnapshot
|
row: CircuitDeviceRowSnapshot
|
||||||
): CircuitDeviceRowInsertProjectCommand {
|
): CircuitDeviceRowInsertProjectCommand {
|
||||||
|
const normalizedRow = {
|
||||||
|
...row,
|
||||||
|
manualQuantity: row.manualQuantity ?? row.quantity,
|
||||||
|
};
|
||||||
const command: CircuitDeviceRowInsertProjectCommand = {
|
const command: CircuitDeviceRowInsertProjectCommand = {
|
||||||
schemaVersion: circuitDeviceRowStructureCommandSchemaVersion,
|
schemaVersion: circuitDeviceRowStructureCommandSchemaVersion,
|
||||||
type: circuitDeviceRowInsertCommandType,
|
type: circuitDeviceRowInsertCommandType,
|
||||||
payload: { row },
|
payload: { row: normalizedRow },
|
||||||
};
|
};
|
||||||
assertCircuitDeviceRowInsertProjectCommand(command);
|
assertCircuitDeviceRowInsertProjectCommand(command);
|
||||||
return command;
|
return command;
|
||||||
@@ -117,7 +122,13 @@ export function assertCircuitDeviceRowInsertProjectCommand(
|
|||||||
] as const) {
|
] as const) {
|
||||||
assertNullableString(row[field], `row.${field}`);
|
assertNullableString(row[field], `row.${field}`);
|
||||||
}
|
}
|
||||||
assertNonNegativeNumber(row.quantity, "row.quantity");
|
const quantity = row.quantity;
|
||||||
|
assertNonNegativeNumber(quantity, "row.quantity");
|
||||||
|
const manualQuantity = row.manualQuantity ?? quantity;
|
||||||
|
assertNonNegativeNumber(manualQuantity, "row.manualQuantity");
|
||||||
|
if (manualQuantity > quantity) {
|
||||||
|
throw new Error("row.manualQuantity must not exceed row.quantity.");
|
||||||
|
}
|
||||||
assertNonNegativeNumber(row.powerPerUnit, "row.powerPerUnit");
|
assertNonNegativeNumber(row.powerPerUnit, "row.powerPerUnit");
|
||||||
assertNonNegativeNumber(
|
assertNonNegativeNumber(
|
||||||
row.simultaneityFactor,
|
row.simultaneityFactor,
|
||||||
@@ -172,7 +183,10 @@ function assertFiniteNumber(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertNonNegativeNumber(value: unknown, field: string) {
|
function assertNonNegativeNumber(
|
||||||
|
value: unknown,
|
||||||
|
field: string
|
||||||
|
): asserts value is number {
|
||||||
assertFiniteNumber(value, field);
|
assertFiniteNumber(value, field);
|
||||||
if (value < 0) {
|
if (value < 0) {
|
||||||
throw new Error(`${field} must not be negative.`);
|
throw new Error(`${field} must not be negative.`);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface CircuitDeviceRow {
|
|||||||
roomNumberSnapshot?: string;
|
roomNumberSnapshot?: string;
|
||||||
roomNameSnapshot?: string;
|
roomNameSnapshot?: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
manualQuantity: number;
|
||||||
powerPerUnit: number;
|
powerPerUnit: number;
|
||||||
simultaneityFactor: number;
|
simultaneityFactor: number;
|
||||||
cosPhi?: number;
|
cosPhi?: number;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface CircuitTreeDeviceRow {
|
|||||||
roomNumberSnapshot?: string;
|
roomNumberSnapshot?: string;
|
||||||
roomNameSnapshot?: string;
|
roomNameSnapshot?: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
manualQuantity: number;
|
||||||
powerPerUnit: number;
|
powerPerUnit: number;
|
||||||
simultaneityFactor: number;
|
simultaneityFactor: number;
|
||||||
cosPhi?: number;
|
cosPhi?: number;
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ import {
|
|||||||
type ExternalModelStateSnapshot,
|
type ExternalModelStateSnapshot,
|
||||||
} from "../../external-model/domain/external-model-contracts.js";
|
} from "../../external-model/domain/external-model-contracts.js";
|
||||||
|
|
||||||
export const projectStateSnapshotSchemaVersion = 4 as const;
|
export const projectStateSnapshotSchemaVersion = 5 as const;
|
||||||
const previousProjectStateSnapshotSchemaVersion = 3 as const;
|
const previousProjectStateSnapshotSchemaVersion = 4 as const;
|
||||||
|
const externalModelProjectStateSnapshotSchemaVersion = 3 as const;
|
||||||
const legacyProjectStateSnapshotSchemaVersion = 2 as const;
|
const legacyProjectStateSnapshotSchemaVersion = 2 as const;
|
||||||
const baselineProjectStateSnapshotSchemaVersion = 1 as const;
|
const baselineProjectStateSnapshotSchemaVersion = 1 as const;
|
||||||
|
|
||||||
@@ -103,8 +104,15 @@ const circuitSectionSchema = z
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const circuitDeviceRowSchema = z
|
const circuitDeviceRowSchema = z.preprocess(
|
||||||
.object({
|
(value) => {
|
||||||
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
||||||
|
const row = value as Record<string, unknown>;
|
||||||
|
return Object.prototype.hasOwnProperty.call(row, "manualQuantity")
|
||||||
|
? row
|
||||||
|
: { ...row, manualQuantity: row.quantity };
|
||||||
|
},
|
||||||
|
z.object({
|
||||||
id: idSchema,
|
id: idSchema,
|
||||||
circuitId: idSchema,
|
circuitId: idSchema,
|
||||||
linkedProjectDeviceId: idSchema.nullable(),
|
linkedProjectDeviceId: idSchema.nullable(),
|
||||||
@@ -120,13 +128,15 @@ const circuitDeviceRowSchema = z
|
|||||||
roomNumberSnapshot: nullableStringSchema,
|
roomNumberSnapshot: nullableStringSchema,
|
||||||
roomNameSnapshot: nullableStringSchema,
|
roomNameSnapshot: nullableStringSchema,
|
||||||
quantity: finiteNumberSchema.nonnegative(),
|
quantity: finiteNumberSchema.nonnegative(),
|
||||||
|
manualQuantity: finiteNumberSchema.nonnegative(),
|
||||||
powerPerUnit: finiteNumberSchema.nonnegative(),
|
powerPerUnit: finiteNumberSchema.nonnegative(),
|
||||||
simultaneityFactor: finiteNumberSchema.nonnegative(),
|
simultaneityFactor: finiteNumberSchema.nonnegative(),
|
||||||
cosPhi: finiteNumberSchema.positive().nullable(),
|
cosPhi: finiteNumberSchema.positive().nullable(),
|
||||||
remark: nullableStringSchema,
|
remark: nullableStringSchema,
|
||||||
overriddenFields: nullableStringSchema,
|
overriddenFields: nullableStringSchema,
|
||||||
})
|
})
|
||||||
.strict();
|
.strict()
|
||||||
|
);
|
||||||
|
|
||||||
const circuitSchema = z
|
const circuitSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -453,6 +463,17 @@ const previousProjectStateSnapshotSchema = z
|
|||||||
project: projectSchema,
|
project: projectSchema,
|
||||||
distributionBoards: z.array(distributionBoardSchema),
|
distributionBoards: z.array(distributionBoardSchema),
|
||||||
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
||||||
|
externalModel: externalModelStateSchema,
|
||||||
|
...projectStateSnapshotContents,
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
const externalModelProjectStateSnapshotSchema = z
|
||||||
|
.object({
|
||||||
|
schemaVersion: z.literal(externalModelProjectStateSnapshotSchemaVersion),
|
||||||
|
project: projectSchema,
|
||||||
|
distributionBoards: z.array(distributionBoardSchema),
|
||||||
|
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
||||||
...projectStateSnapshotContents,
|
...projectStateSnapshotContents,
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
@@ -502,6 +523,13 @@ function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
|
|||||||
const schemaVersion = (value as { schemaVersion: number }).schemaVersion;
|
const schemaVersion = (value as { schemaVersion: number }).schemaVersion;
|
||||||
if (schemaVersion === previousProjectStateSnapshotSchemaVersion) {
|
if (schemaVersion === previousProjectStateSnapshotSchemaVersion) {
|
||||||
const previous = previousProjectStateSnapshotSchema.parse(value);
|
const previous = previousProjectStateSnapshotSchema.parse(value);
|
||||||
|
return {
|
||||||
|
...previous,
|
||||||
|
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (schemaVersion === externalModelProjectStateSnapshotSchemaVersion) {
|
||||||
|
const previous = externalModelProjectStateSnapshotSchema.parse(value);
|
||||||
return {
|
return {
|
||||||
...previous,
|
...previous,
|
||||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||||
@@ -765,6 +793,11 @@ function assertProjectStateSnapshotRelations(
|
|||||||
if (row.roomId !== null) {
|
if (row.roomId !== null) {
|
||||||
assertReference(roomIds, row.roomId, "device row room");
|
assertReference(roomIds, row.roomId, "device row room");
|
||||||
}
|
}
|
||||||
|
if (row.manualQuantity > row.quantity) {
|
||||||
|
throw new Error(
|
||||||
|
"Snapshot device row manual quantity must not exceed total quantity."
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -350,6 +350,7 @@ export interface CircuitTreeDeviceRowDto {
|
|||||||
roomNumberSnapshot?: string;
|
roomNumberSnapshot?: string;
|
||||||
roomNameSnapshot?: string;
|
roomNameSnapshot?: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
manualQuantity: number;
|
||||||
powerPerUnit: number;
|
powerPerUnit: number;
|
||||||
simultaneityFactor: number;
|
simultaneityFactor: number;
|
||||||
cosPhi?: number;
|
cosPhi?: number;
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ export async function getCircuitTree(req: Request, res: Response) {
|
|||||||
roomNumberSnapshot: row.roomNumberSnapshot ?? undefined,
|
roomNumberSnapshot: row.roomNumberSnapshot ?? undefined,
|
||||||
roomNameSnapshot: row.roomNameSnapshot ?? undefined,
|
roomNameSnapshot: row.roomNameSnapshot ?? undefined,
|
||||||
quantity: row.quantity,
|
quantity: row.quantity,
|
||||||
|
manualQuantity: row.manualQuantity,
|
||||||
powerPerUnit: row.powerPerUnit,
|
powerPerUnit: row.powerPerUnit,
|
||||||
simultaneityFactor: row.simultaneityFactor,
|
simultaneityFactor: row.simultaneityFactor,
|
||||||
cosPhi: row.cosPhi ?? undefined,
|
cosPhi: row.cosPhi ?? undefined,
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ function createTestDatabase(): DatabaseContext {
|
|||||||
name: "Leuchte",
|
name: "Leuchte",
|
||||||
displayName: "Leuchte lokal",
|
displayName: "Leuchte lokal",
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
|
manualQuantity: 1,
|
||||||
powerPerUnit: 0.1,
|
powerPerUnit: 0.1,
|
||||||
simultaneityFactor: 1,
|
simultaneityFactor: 1,
|
||||||
},
|
},
|
||||||
@@ -138,6 +139,7 @@ function createTestDatabase(): DatabaseContext {
|
|||||||
name: "Fremdzeile",
|
name: "Fremdzeile",
|
||||||
displayName: "Fremdzeile",
|
displayName: "Fremdzeile",
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
|
manualQuantity: 1,
|
||||||
powerPerUnit: 0.2,
|
powerPerUnit: 0.2,
|
||||||
simultaneityFactor: 1,
|
simultaneityFactor: 1,
|
||||||
},
|
},
|
||||||
@@ -175,6 +177,7 @@ describe("circuit device-row project-command repository", () => {
|
|||||||
|
|
||||||
const row = getRow(context);
|
const row = getRow(context);
|
||||||
assert.equal(row.quantity, 2);
|
assert.equal(row.quantity, 2);
|
||||||
|
assert.equal(row.manualQuantity, 2);
|
||||||
assert.equal(row.roomId, "room-1");
|
assert.equal(row.roomId, "room-1");
|
||||||
assert.equal(row.remark, null);
|
assert.equal(row.remark, null);
|
||||||
assert.equal(row.overriddenFields, "[\"quantity\"]");
|
assert.equal(row.overriddenFields, "[\"quantity\"]");
|
||||||
@@ -185,6 +188,7 @@ describe("circuit device-row project-command repository", () => {
|
|||||||
{ field: "quantity", value: 1 },
|
{ field: "quantity", value: 1 },
|
||||||
{ field: "roomId", value: null },
|
{ field: "roomId", value: null },
|
||||||
{ field: "remark", value: null },
|
{ field: "remark", value: null },
|
||||||
|
{ field: "manualQuantity", value: 1 },
|
||||||
{ field: "overriddenFields", value: null },
|
{ field: "overriddenFields", value: null },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -203,6 +207,7 @@ describe("circuit device-row project-command repository", () => {
|
|||||||
{ field: "quantity", value: 2 },
|
{ field: "quantity", value: 2 },
|
||||||
{ field: "roomId", value: "room-1" },
|
{ field: "roomId", value: "room-1" },
|
||||||
{ field: "remark", value: null },
|
{ field: "remark", value: null },
|
||||||
|
{ field: "manualQuantity", value: 2 },
|
||||||
{ field: "overriddenFields", value: "[\"quantity\"]" },
|
{ field: "overriddenFields", value: "[\"quantity\"]" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -234,6 +239,7 @@ describe("circuit device-row project-command repository", () => {
|
|||||||
|
|
||||||
const row = getRow(context);
|
const row = getRow(context);
|
||||||
assert.equal(row.quantity, 1);
|
assert.equal(row.quantity, 1);
|
||||||
|
assert.equal(row.manualQuantity, 1);
|
||||||
assert.equal(row.overriddenFields, null);
|
assert.equal(row.overriddenFields, null);
|
||||||
assert.equal(undone.revision.revisionNumber, 2);
|
assert.equal(undone.revision.revisionNumber, 2);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
@@ -377,4 +383,22 @@ describe("circuit device-row project-command repository", () => {
|
|||||||
context.close();
|
context.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects a manual share above the materialized total", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const store = new CircuitDeviceRowProjectCommandRepository(context.db);
|
||||||
|
assert.throws(() => store.executeUpdate({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createCircuitDeviceRowUpdateProjectCommand("row-1", {
|
||||||
|
manualQuantity: 2,
|
||||||
|
}),
|
||||||
|
}), /manual quantity/);
|
||||||
|
assert.equal(getRow(context).manualQuantity, 1);
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
import {
|
||||||
|
assertCircuitDeviceRowQuantity,
|
||||||
|
calculateCircuitDeviceRowQuantity,
|
||||||
|
} from "../src/domain/calculations/circuit-device-row-quantity.js";
|
||||||
|
|
||||||
|
describe("circuit device-row quantity", () => {
|
||||||
|
it("adds the manual share and every indivisible external quantity", () => {
|
||||||
|
assert.equal(
|
||||||
|
calculateCircuitDeviceRowQuantity(2, [
|
||||||
|
{ effectiveQuantity: 2 },
|
||||||
|
{ effectiveQuantity: 1 },
|
||||||
|
]),
|
||||||
|
5
|
||||||
|
);
|
||||||
|
assert.doesNotThrow(() => assertCircuitDeviceRowQuantity({
|
||||||
|
quantity: 5,
|
||||||
|
manualQuantity: 2,
|
||||||
|
externalObjects: [{ effectiveQuantity: 2 }, { effectiveQuantity: 1 }],
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects invalid shares and a stale materialized total", () => {
|
||||||
|
assert.throws(
|
||||||
|
() => calculateCircuitDeviceRowQuantity(-1, []),
|
||||||
|
/Manual quantity/
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => calculateCircuitDeviceRowQuantity(0, [{ effectiveQuantity: 0 }]),
|
||||||
|
/External effective quantity/
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => assertCircuitDeviceRowQuantity({
|
||||||
|
quantity: 2,
|
||||||
|
manualQuantity: 1,
|
||||||
|
externalObjects: [{ effectiveQuantity: 2 }],
|
||||||
|
}),
|
||||||
|
/does not match/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves existing row quantities in migration 0005", () => {
|
||||||
|
const database = new Database(":memory:");
|
||||||
|
try {
|
||||||
|
database.exec("CREATE TABLE circuit_device_rows (id text PRIMARY KEY, quantity integer NOT NULL)");
|
||||||
|
database.exec("INSERT INTO circuit_device_rows (id, quantity) VALUES ('row-1', 7)");
|
||||||
|
const migration = fs
|
||||||
|
.readFileSync("src/db/migrations/0005_stale_gorilla_man.sql", "utf8")
|
||||||
|
.replaceAll("--> statement-breakpoint", "");
|
||||||
|
database.exec(migration);
|
||||||
|
assert.deepEqual(
|
||||||
|
database.prepare("SELECT quantity, manual_quantity FROM circuit_device_rows").get(),
|
||||||
|
{ quantity: 7, manual_quantity: 7 }
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
database.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -21,6 +21,7 @@ import { projectDevices } from "../src/db/schema/project-devices.js";
|
|||||||
import { projects } from "../src/db/schema/projects.js";
|
import { projects } from "../src/db/schema/projects.js";
|
||||||
import { rooms } from "../src/db/schema/rooms.js";
|
import { rooms } from "../src/db/schema/rooms.js";
|
||||||
import { createProjectStateRestoreCommand } from "../src/domain/models/project-state-restore-command.model.js";
|
import { createProjectStateRestoreCommand } from "../src/domain/models/project-state-restore-command.model.js";
|
||||||
|
import { projectStateSnapshotSchemaVersion } from "../src/domain/models/project-state-snapshot.model.js";
|
||||||
import { createExternalCsvPreview } from "../src/external-model/application/external-csv-preview.js";
|
import { createExternalCsvPreview } from "../src/external-model/application/external-csv-preview.js";
|
||||||
import { parseExternalCsv } from "../src/external-model/csv/external-csv-transport.js";
|
import { parseExternalCsv } from "../src/external-model/csv/external-csv-transport.js";
|
||||||
import {
|
import {
|
||||||
@@ -211,7 +212,7 @@ describe("external model snapshot and transfer", () => {
|
|||||||
insertLinkedExternalFixture(context.db);
|
insertLinkedExternalFixture(context.db);
|
||||||
const populated = readProjectStateSnapshot(context.db, "project-1");
|
const populated = readProjectStateSnapshot(context.db, "project-1");
|
||||||
assert.ok(populated);
|
assert.ok(populated);
|
||||||
assert.equal(populated.state.schemaVersion, 4);
|
assert.equal(populated.state.schemaVersion, projectStateSnapshotSchemaVersion);
|
||||||
assert.equal(populated.state.externalModel.objects[0].circuitDeviceRowId, "row-1");
|
assert.equal(populated.state.externalModel.objects[0].circuitDeviceRowId, "row-1");
|
||||||
|
|
||||||
context.db.delete(externalModelSources).run();
|
context.db.delete(externalModelSources).run();
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ describe("circuit device-row update project commands", () => {
|
|||||||
{ field: "roomId", value: null },
|
{ field: "roomId", value: null },
|
||||||
{ field: "quantity", value: 2 },
|
{ field: "quantity", value: 2 },
|
||||||
{ field: "cosPhi", value: 0.9 },
|
{ field: "cosPhi", value: 0.9 },
|
||||||
|
{ field: "manualQuantity", value: 2 },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -187,6 +188,13 @@ describe("circuit device-row update project commands", () => {
|
|||||||
() => createCircuitDeviceRowUpdateProjectCommand("row-1", {}),
|
() => createCircuitDeviceRowUpdateProjectCommand("row-1", {}),
|
||||||
/at least one change/
|
/at least one change/
|
||||||
);
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => createCircuitDeviceRowUpdateProjectCommand("row-1", {
|
||||||
|
quantity: 1,
|
||||||
|
manualQuantity: 2,
|
||||||
|
}),
|
||||||
|
/must not exceed/
|
||||||
|
);
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() =>
|
() =>
|
||||||
createCircuitDeviceRowUpdateProjectCommand("row-1", {
|
createCircuitDeviceRowUpdateProjectCommand("row-1", {
|
||||||
@@ -470,7 +478,7 @@ describe("circuit device-row structure project commands", () => {
|
|||||||
row.circuitId
|
row.circuitId
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.deepEqual(insert.payload.row, row);
|
assert.deepEqual(insert.payload.row, { ...row, manualQuantity: 1 });
|
||||||
assert.deepEqual(remove.payload, {
|
assert.deepEqual(remove.payload, {
|
||||||
rowId: "row-1",
|
rowId: "row-1",
|
||||||
expectedCircuitId: "circuit-1",
|
expectedCircuitId: "circuit-1",
|
||||||
|
|||||||
@@ -199,6 +199,39 @@ describe("project state snapshot model", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("upgrades snapshot version 4 with the existing quantity as manual share", () => {
|
||||||
|
const previous = protectedSnapshot();
|
||||||
|
previous.schemaVersion = 4 as typeof previous.schemaVersion;
|
||||||
|
previous.circuits[0]!.isReserve = false;
|
||||||
|
previous.circuits[0]!.deviceRows = [{
|
||||||
|
id: "row-1",
|
||||||
|
circuitId: "circuit-1",
|
||||||
|
linkedProjectDeviceId: null,
|
||||||
|
sortOrder: 10,
|
||||||
|
name: "Leuchte",
|
||||||
|
displayName: "Leuchte",
|
||||||
|
phaseType: "single_phase",
|
||||||
|
connectionKind: null,
|
||||||
|
costGroup: null,
|
||||||
|
category: "lighting",
|
||||||
|
level: null,
|
||||||
|
roomId: null,
|
||||||
|
roomNumberSnapshot: null,
|
||||||
|
roomNameSnapshot: null,
|
||||||
|
quantity: 3,
|
||||||
|
powerPerUnit: 0.1,
|
||||||
|
simultaneityFactor: 1,
|
||||||
|
cosPhi: 1,
|
||||||
|
remark: null,
|
||||||
|
overriddenFields: null,
|
||||||
|
}];
|
||||||
|
|
||||||
|
const upgraded = parseProjectStateSnapshot(previous);
|
||||||
|
|
||||||
|
assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion);
|
||||||
|
assert.equal(upgraded.circuits[0]?.deviceRows[0]?.manualQuantity, 3);
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects malformed baseline and unknown snapshot shapes", () => {
|
it("rejects malformed baseline and unknown snapshot shapes", () => {
|
||||||
const outdated = structuredClone(minimalSnapshot());
|
const outdated = structuredClone(minimalSnapshot());
|
||||||
delete (outdated.project as Record<string, unknown>).description;
|
delete (outdated.project as Record<string, unknown>).description;
|
||||||
|
|||||||
Reference in New Issue
Block a user