From 704f4df2a8b9e5b1e0685c2a61aeb361ff8940da Mon Sep 17 00:00:00 2001 From: Grovy311 Date: Thu, 30 Jul 2026 17:40:11 +0200 Subject: [PATCH 01/59] Apply Petrol/Volt/Kupfer design system and project sidebar Bring the Econsult-family design tokens (see design-system-econsult.md) into leistungsbilanz-ts and add a persistent sidebar for the projects area, mirroring elt-planung-suite / Einsatz-Orga. The sidebar lists all projects and, inside a project, jumps to its sections; the tree editor stays sidebar-free since it needs the full width. The projects overview page is decluttered to a single stacked column (create/import/global devices) now that the project list lives in the sidebar. --- src/app/globals.css | 298 +++++++++++++++++++++++++- src/app/layout.tsx | 5 +- src/app/projects/[projectId]/page.tsx | 34 ++- src/app/projects/page.tsx | 215 +++++++++---------- src/frontend/components/AppShell.tsx | 22 ++ src/frontend/components/Sidebar.tsx | 121 +++++++++++ 6 files changed, 568 insertions(+), 127 deletions(-) create mode 100644 src/frontend/components/AppShell.tsx create mode 100644 src/frontend/components/Sidebar.tsx diff --git a/src/app/globals.css b/src/app/globals.css index 351a429..a6c31c4 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,9 +1,303 @@ +/* Design-System "Petrol · Volt · Kupfer" v2.1 (H:\design-system-econsult.md), + projektübergreifendes Update für die Econsult-Familie (siehe elt-planung-suite). + Bootstrap bleibt Basis für Formulare/Tabellen/Modals; wir tönen die + vorhandenen Bootstrap-Komponenten über deren eigene --bs-btn-*-Variablen + um (globals.css lädt nach bootstrap.min.css, gleiche Spezifität gewinnt + per Ladereihenfolge) statt Bootstrap zu ersetzen. */ +:root { + --color-primary: #1c3f52; /* Petrol */ + --color-primary-dark: #0f2733; /* Petrol Dark */ + --color-primary-deep: #0a1920; /* Petrol Deep */ + --color-accent: #4e7e93; /* Petrol Light */ + --color-accent-pale: #9dbac9; /* Petrol Pale */ + --color-signal: #3fa66b; /* Volt - generelle aktiv/positiv-Farbe */ + --color-signal-dark: #237a47; + --color-attention: #c97a3b; /* Kupfer - bewusst selten, "braucht Aufmerksamkeit" */ + --color-attention-dark: #9c5a26; + --color-ink: #182229; /* Graphite */ + --color-ink-soft: #59666d; /* Slate */ + --color-bg: #ffffff; + --color-surface: #f1f2ee; /* Fog */ + --color-border: #dee2dc; + --color-danger: #b04035; + --color-danger-bg: #f7e6e3; + --color-warn: #a9791e; + + --font-head: system-ui, "Segoe UI", "Helvetica Neue", sans-serif; + --font-body: system-ui, "Segoe UI", "Helvetica Neue", sans-serif; + + --space-xs: 4px; + --space-sm: 8px; + --space-md: 16px; + --space-lg: 24px; + --space-xl: 40px; + + --radius: 12px; +} + +html { + scroll-behavior: smooth; +} + body { - background-color: #f5f7fb; + background-color: var(--color-surface); + color: var(--color-ink); +} + +a { + color: var(--color-primary); +} + +/* ── Bootstrap-Einfärbung auf Petrol/Volt/Kupfer ────────────────────────── */ + +.btn-primary { + --bs-btn-bg: var(--color-primary); + --bs-btn-border-color: var(--color-primary); + --bs-btn-hover-bg: var(--color-primary-dark); + --bs-btn-hover-border-color: var(--color-primary-dark); + --bs-btn-active-bg: var(--color-primary-deep); + --bs-btn-active-border-color: var(--color-primary-deep); + --bs-btn-disabled-bg: var(--color-primary); + --bs-btn-disabled-border-color: var(--color-primary); +} + +.btn-outline-primary { + --bs-btn-color: var(--color-primary); + --bs-btn-border-color: var(--color-primary); + --bs-btn-hover-bg: var(--color-primary); + --bs-btn-hover-border-color: var(--color-primary); + --bs-btn-active-bg: var(--color-primary-dark); + --bs-btn-active-border-color: var(--color-primary-dark); +} + +.btn-success, +.btn-outline-success { + --bs-btn-bg: var(--color-signal); + --bs-btn-border-color: var(--color-signal); + --bs-btn-color: #fff; + --bs-btn-hover-bg: var(--color-signal-dark); + --bs-btn-hover-border-color: var(--color-signal-dark); + --bs-btn-active-bg: var(--color-signal-dark); + --bs-btn-active-border-color: var(--color-signal-dark); +} + +.btn-outline-success { + --bs-btn-bg: transparent; + --bs-btn-color: var(--color-signal-dark); + --bs-btn-hover-color: #fff; +} + +.form-control:focus, +.form-select:focus { + border-color: var(--color-accent); + box-shadow: 0 0 0 0.25rem rgba(28, 63, 82, 0.15); +} + +.form-check-input:checked { + background-color: var(--color-primary); + border-color: var(--color-primary); +} + +.text-bg-secondary { + background-color: var(--color-surface) !important; + color: var(--color-ink-soft) !important; +} + +.alert-success { + --bs-alert-color: var(--color-signal-dark); + --bs-alert-bg: #eaf6f0; + --bs-alert-border-color: #b6e2ce; +} + +/* ── App-Shell / Seitenleiste (nur auf der Projektseite) ────────────────── */ + +.app-shell { + display: grid; + grid-template-columns: 260px 1fr; + min-height: 100vh; +} + +.sidebar { + background: linear-gradient( + 160deg, + var(--color-primary-deep) 0%, + var(--color-primary-dark) 45%, + var(--color-primary) 100% + ); + color: #fff; + display: flex; + flex-direction: column; + position: sticky; + top: 0; + height: 100vh; + overflow-y: auto; +} + +.sidebar-brand { + padding: 22px 22px 18px; + border-bottom: 1px solid rgba(255, 255, 255, 0.12); +} + +.sidebar-wordmark { + font-size: 1.2rem; + font-weight: 700; + color: #fff; +} + +.sidebar-bar { + height: 3px; + background: linear-gradient(90deg, var(--color-signal), var(--color-attention)); + margin-top: 9px; + width: 42px; + border-radius: 2px; +} + +.sidebar-tag { + font-size: 0.72rem; + letter-spacing: 0.13em; + text-transform: uppercase; + color: var(--color-accent-pale); + margin-top: 10px; +} + +.sidebar-nav { + display: flex; + flex-direction: column; + gap: 2px; + padding: 12px; +} + +.sidebar-section-label { + font-size: 0.66rem; + letter-spacing: 0.1em; + text-transform: uppercase; + color: rgba(199, 214, 224, 0.7); + padding: 16px 14px 4px; +} + +.sidebar-link { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + border-radius: 8px; + color: #c7d6e0; + text-decoration: none; + font-size: 0.92rem; + font-weight: 500; + border: 0; + background: transparent; + width: 100%; + text-align: left; + cursor: pointer; +} + +.sidebar-link:hover { + background: rgba(255, 255, 255, 0.08); + color: #fff; + text-decoration: none; +} + +.sidebar-link.active { + background: rgba(255, 255, 255, 0.14); + color: #fff; + box-shadow: inset 3px 0 0 var(--color-signal); +} + +.sidebar-icon { + width: 1.1em; + display: inline-flex; + justify-content: center; +} + +.app-shell-content { + width: 100%; + min-width: 0; +} + +.page-header { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: var(--space-md); + margin-bottom: var(--space-lg); + flex-wrap: wrap; +} + +.page-header h1 { + font-size: 1.5rem; + font-weight: 700; + color: var(--color-primary); +} + +.kicker { + font-size: 0.72rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--color-accent); + margin-bottom: 2px; + font-weight: 600; +} + +/* ── KPI-Kacheln ─────────────────────────────────────────────────────── */ + +.kpi-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: var(--space-md); + margin-bottom: var(--space-lg); +} + +.kpi { + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: 9px; + padding: 13px 15px; + display: block; + color: inherit; + text-decoration: none; + transition: box-shadow 0.15s, transform 0.15s; +} + +a.kpi:hover { + color: inherit; + text-decoration: none; + box-shadow: 0 3px 8px rgba(15, 39, 51, 0.07), 0 16px 32px -14px rgba(15, 39, 51, 0.28); + transform: translateY(-1px); +} + +.kpi-label { + font-size: 0.66rem; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--color-ink-soft); + margin-bottom: var(--space-xs); + font-weight: 600; + display: flex; + align-items: center; + gap: 6px; +} + +.kpi-value { + font-size: 1.2rem; + font-weight: 700; + line-height: 1.1; + color: var(--color-primary); } .card { - border-radius: 0.5rem; + border-radius: var(--radius); + border-top: 3px solid var(--color-primary); + box-shadow: + 0 1px 2px rgba(15, 39, 51, 0.05), + 0 10px 24px -14px rgba(15, 39, 51, 0.22); +} + +.card-header { + font-weight: 700; + color: var(--color-primary); + background: var(--color-bg); + border-bottom: 1px solid var(--color-border); } .table td input.form-control-sm, diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a67de86..01df63e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import "bootstrap/dist/css/bootstrap.min.css"; import "./globals.css"; +import { AppShell } from "../frontend/components/AppShell"; export const metadata: Metadata = { title: "Leistungsbilanz", @@ -10,7 +11,9 @@ export const metadata: Metadata = { export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { return ( - {children} + + {children} + ); } diff --git a/src/app/projects/[projectId]/page.tsx b/src/app/projects/[projectId]/page.tsx index b574312..f7945df 100644 --- a/src/app/projects/[projectId]/page.tsx +++ b/src/app/projects/[projectId]/page.tsx @@ -661,9 +661,10 @@ export default function ProjectDetailPage() { return (
-
+
-

{project?.name ?? "Projekt"}

+
Projekt
+

{project?.name ?? "Projekt"}

{project?.internalProjectNumber ? `Projektnummer ${project.internalProjectNumber} · ` @@ -688,9 +689,28 @@ export default function ProjectDetailPage() { {error ?

{error}
: null} + +
{project ? ( -
+
window.location.reload()} @@ -699,7 +719,7 @@ export default function ProjectDetailPage() {
) : null} -
+
@@ -792,7 +812,7 @@ export default function ProjectDetailPage() {
-
+
@@ -820,7 +840,7 @@ export default function ProjectDetailPage() {
-
+
@@ -866,7 +886,7 @@ export default function ProjectDetailPage() {
-
+
Projektgeräte diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx index 14a459d..93a2d14 100644 --- a/src/app/projects/page.tsx +++ b/src/app/projects/page.tsx @@ -8,15 +8,15 @@ import { deleteGlobalDevice, importProjectTransferAsNew, listGlobalDevices, - listProjects, updateGlobalDevice, } from "../../frontend/utils/api"; import type { CreateGlobalDeviceInput, GlobalDeviceDto, - ProjectDto, } from "../../frontend/types"; +const PROJECTS_CHANGED_EVENT = "leistungsbilanz:projects-changed"; + const emptyGlobalDevice: CreateGlobalDeviceInput = { name: "", displayName: "", @@ -38,7 +38,6 @@ function toOptionalNumber(value: string) { } export default function ProjectsPage() { - const [projects, setProjects] = useState([]); const [globalDevices, setGlobalDevices] = useState([]); const [projectName, setProjectName] = useState(""); const [globalDeviceForm, setGlobalDeviceForm] = useState>({ @@ -64,12 +63,14 @@ export default function ProjectsPage() { projectId: string; name: string; } | null>(null); + const [createdProject, setCreatedProject] = useState<{ + id: string; + name: string; + } | null>(null); async function loadData() { setError(null); - const [loadedProjects, loadedGlobalDevices] = await Promise.all([listProjects(), listGlobalDevices()]); - setProjects(loadedProjects); - setGlobalDevices(loadedGlobalDevices); + setGlobalDevices(await listGlobalDevices()); } useEffect(() => { @@ -85,10 +86,12 @@ export default function ProjectsPage() { } setIsSaving(true); setError(null); + setCreatedProject(null); try { const created = await createProject(projectName.trim()); - setProjects((current) => [...current, created]); + setCreatedProject({ id: created.id, name: created.name }); setProjectName(""); + window.dispatchEvent(new Event(PROJECTS_CHANGED_EVENT)); } catch (err) { setError(err instanceof Error ? err.message : "Projekt konnte nicht erstellt werden."); } finally { @@ -123,11 +126,11 @@ export default function ProjectsPage() { setImportedProject(null); try { const imported = await importProjectTransferAsNew(projectImport); - setProjects(await listProjects()); setImportedProject(imported); setProjectImport(null); setProjectImportFilename(""); setProjectImportInputKey((current) => current + 1); + window.dispatchEvent(new Event(PROJECTS_CHANGED_EVENT)); } catch (err) { setError( err instanceof Error @@ -222,130 +225,107 @@ export default function ProjectsPage() { return (
-
+
-

Projekte

+
Leistungsbilanz
+

Projekte

Projektübersicht und globale Geräteverwaltung

{error ?
{error}
: null} -
-
-
-
Neues Projekt
-
-
-
- - setProjectName(event.target.value)} - placeholder="z. B. Neubau Schule" - /> -
- -
-
-
-
-
Projekt importieren
-
-

- Eine exportierte JSON-Projektdatei wird als eigenständige Kopie - mit neuen internen IDs angelegt. -

- - - void handleProjectImportFile(event.target.files?.[0]) - } - type="file" - /> - {projectImportFilename ? ( -
- {projectImportFilename} ist bereit. -
- ) : null} - {projectImportFileError ? ( -
- {projectImportFileError} -
- ) : null} - - {importedProject ? ( -
-
- „{importedProject.name}“ wurde angelegt. -
- - Importiertes Projekt öffnen - + + {createdProject ? ( +
+
+ „{createdProject.name}“ wurde angelegt.
- ) : null} -
+ + Projekt öffnen + +
+ ) : null}
-
-
-
Alle Projekte
-
- - - - - - - - - {projects.map((project) => ( - - - - - ))} - {!projects.length ? ( - - - - ) : null} - -
ProjektAktionen
{project.name} - - Projekt öffnen - -
- Noch keine Projekte vorhanden. -
-
+
+
Projekt importieren
+
+

+ Eine exportierte JSON-Projektdatei wird als eigenständige Kopie + mit neuen internen IDs angelegt. +

+ + + void handleProjectImportFile(event.target.files?.[0]) + } + type="file" + /> + {projectImportFilename ? ( +
+ {projectImportFilename} ist bereit. +
+ ) : null} + {projectImportFileError ? ( +
+ {projectImportFileError} +
+ ) : null} + + {importedProject ? ( +
+
+ „{importedProject.name}“ wurde angelegt. +
+ + Importiertes Projekt öffnen + +
+ ) : null}
-
-
-
Globale Geräte / Verbraucher
+
+
Globale Geräte / Verbraucher
@@ -524,6 +504,7 @@ export default function ProjectsPage() {
+
); } diff --git a/src/frontend/components/AppShell.tsx b/src/frontend/components/AppShell.tsx new file mode 100644 index 0000000..ba08175 --- /dev/null +++ b/src/frontend/components/AppShell.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import { Sidebar } from "./Sidebar"; + +const PROJECT_AREA_PATTERN = /^\/projects(\/[^/]+)?$/; + +export function AppShell({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + const showSidebar = PROJECT_AREA_PATTERN.test(pathname ?? ""); + + if (!showSidebar) { + return <>{children}; + } + + return ( +
+ +
{children}
+
+ ); +} diff --git a/src/frontend/components/Sidebar.tsx b/src/frontend/components/Sidebar.tsx new file mode 100644 index 0000000..f1c0dbc --- /dev/null +++ b/src/frontend/components/Sidebar.tsx @@ -0,0 +1,121 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useEffect, useState } from "react"; +import { listProjects } from "../utils/api"; +import type { ProjectDto } from "../types"; + +const PROJECT_SECTIONS = [ + { id: "verlauf", label: "Verlauf", icon: "↺" }, + { id: "verteilungen", label: "Verteilungen", icon: "⌁" }, + { id: "etagen", label: "Etagen", icon: "▥" }, + { id: "raeume", label: "Räume", icon: "⌂" }, + { id: "projektgeraete", label: "Projektgeräte", icon: "⚙" }, +] as const; + +export function Sidebar() { + const pathname = usePathname(); + const projectMatch = pathname?.match(/^\/projects\/([^/]+)$/); + const activeProjectId = projectMatch ? projectMatch[1] : null; + + const [projects, setProjects] = useState([]); + const [activeSection, setActiveSection] = useState(null); + + useEffect(() => { + function refreshProjects() { + listProjects() + .then(setProjects) + .catch(() => setProjects([])); + } + + refreshProjects(); + window.addEventListener("leistungsbilanz:projects-changed", refreshProjects); + return () => + window.removeEventListener("leistungsbilanz:projects-changed", refreshProjects); + }, []); + + useEffect(() => { + if (!activeProjectId) { + setActiveSection(null); + return; + } + + const elements = PROJECT_SECTIONS.map((section) => + document.getElementById(section.id) + ).filter((element): element is HTMLElement => element !== null); + + if (!elements.length) { + return; + } + + const observer = new IntersectionObserver( + (entries) => { + const visible = entries + .filter((entry) => entry.isIntersecting) + .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top); + if (visible[0]) { + setActiveSection(visible[0].target.id); + } + }, + { rootMargin: "-20% 0px -70% 0px", threshold: 0 } + ); + + elements.forEach((element) => observer.observe(element)); + return () => observer.disconnect(); + }, [activeProjectId]); + + return ( + + ); +} From 31589875b592e8fe4823a87758e6da5e06854496 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Fri, 31 Jul 2026 07:22:26 +0200 Subject: [PATCH 02/59] Render protected circuit structure --- docs/current-architecture.md | 12 ++- docs/spec/08-current-product-backlog.md | 2 +- ...-board-components-and-protection-groups.md | 11 ++- src/app/globals.css | 34 +++++++++ .../components/circuit-tree-editor.tsx | 62 +++++++++++++-- src/frontend/utils/circuit-grid-insertion.ts | 10 +++ src/frontend/utils/circuit-grid-model.ts | 5 +- src/frontend/utils/circuit-grid-projection.ts | 76 +++++++++++++++---- .../utils/circuit-structure-projection.ts | 2 +- tests/circuit-grid-insertion.test.ts | 8 ++ tests/circuit-grid-projection.test.ts | 61 +++++++++++++++ 11 files changed, 254 insertions(+), 29 deletions(-) diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 0534219..f3956cd 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -355,6 +355,11 @@ rein und deterministisch in die drei Zonen Kopf, vollständige Gruppen und Fuß. Innerhalb einer Gruppe folgen auf die Überschrift optionale Schutzkomponenten, vollständige Stromkreisblöcke und der freie Einfügeplatz. Leere Gruppen bleiben sichtbar; Komponenten werden stabil nach `sortOrder` und ID sortiert. +`circuit-grid-projection.ts` normalisiert diese Struktur zusammen mit den +bisherigen kompakten und mehrzeiligen Stromkreisblöcken für den Editor. Kopf-, +Gruppen- und Fußkomponenten erscheinen dort zunächst als schreibgeschützte +Zeilen über die volle Tabellenbreite und nehmen nicht an Zellauswahl, +Tastatur-Einfügen oder Löschen teil. `distribution-board.update` versioniert Etage, Netzart und den verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und @@ -426,9 +431,10 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät. stromkreislistenweite BMK-Eindeutigkeit über Stromkreise und Verteilerkomponenten. Snapshot- und Transfer-Integration verwenden Snapshot-Schema 7. Persistente Insert/Delete/Update-Commands für - veränderliche Verteilerkomponenten sowie CRUD-Commands für leere Gruppen - sowie vollständige Gruppensortierung sind integriert; befüllte Unterbäume - und UI folgen in abgegrenzten Arbeitspaketen. + veränderliche Verteilerkomponenten, Gruppen einschließlich befüllter + Unterbäume sowie vollständige Gruppensortierung sind integriert. Der Editor + zeigt die geschützte Struktur bereits schreibgeschützt an; dedizierte + Bearbeitungsfunktionen folgen in einem abgegrenzten Arbeitspaket. PostgreSQL ist bewusst nicht implementiert. Die Domainregeln und Transaktionsgrenzen sollen portabel bleiben; Schema und Betriebsmodell benötigen diff --git a/docs/spec/08-current-product-backlog.md b/docs/spec/08-current-product-backlog.md index 4614858..33751af 100644 --- a/docs/spec/08-current-product-backlog.md +++ b/docs/spec/08-current-product-backlog.md @@ -33,7 +33,7 @@ requirements and intended sequencing, not proof of implementation. - [x] Phase D3b: persistent populated-group delete/restore command. - [x] Phase E1: tree read model for components, groups and protection devices. - [x] Phase E2a: pure header/group/footer structure projection. -- [ ] Phase E2b: render the structure projection in the editor grid. +- [x] Phase E2b: render the structure projection in the editor grid. - [ ] Phase E: editor projection and editing. - [ ] Phase F: documentation and full GUI verification. - [ ] Keep full electrical sizing and cable-dimensioning rules separate until diff --git a/docs/spec/09-distribution-board-components-and-protection-groups.md b/docs/spec/09-distribution-board-components-and-protection-groups.md index 5bdbd87..f1a8772 100644 --- a/docs/spec/09-distribution-board-components-and-protection-groups.md +++ b/docs/spec/09-distribution-board-components-and-protection-groups.md @@ -739,8 +739,9 @@ Acceptance: ### E. Editor Projection and Editing -Status: In progress. The complete tree read model E1 and pure structural -projection E2a are complete. Grid rendering and editing remain pending. +Status: In progress. The complete tree read model E1, pure structural +projection E2a and read-only grid rendering E2b are complete. Editing remains +pending. - render fixed header components - render group components and circuit blocks @@ -749,7 +750,7 @@ projection E2a are complete. Grid rendering and editing remain pending. - add type-dependent protection editing - keep spreadsheet selection, keyboard and drag behavior coherent -Implemented in E1/E2a: +Implemented in E1/E2a/E2b: - the tree response exposes fixed header, group and auxiliary footer components plus both one-to-one protection layers @@ -759,6 +760,10 @@ Implemented in E1/E2a: circuit blocks and free placeholder together - empty groups remain visible and circuit device rows remain owned by their circuit block +- the editor renders fixed header, group protection and auxiliary footer + components as read-only full-width rows +- structural component rows are excluded from spreadsheet cell navigation, + insertion and deletion commands until their dedicated editing controls exist Acceptance: diff --git a/src/app/globals.css b/src/app/globals.css index 351a429..9fe85c5 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -657,6 +657,40 @@ body { font-weight: 600; } +.tree-grid .structure-component-row td { + padding: 0.38rem 0.6rem; + border-bottom-color: #d8dee9; +} + +.tree-grid .structure-component-row.headerComponent td { + background: #e2e8f0; +} + +.tree-grid .structure-component-row.groupComponent td { + background: #f8fafc; +} + +.tree-grid .structure-component-row.footerComponent td { + background: #f1f5f9; +} + +.tree-grid .structure-component-content { + display: flex; + align-items: baseline; + gap: 0.75rem; + min-width: 0; +} + +.tree-grid .structure-component-content strong { + flex: 0 0 auto; + font-variant-numeric: tabular-nums; +} + +.tree-grid .structure-component-protection { + color: #475569; + font-size: 0.78rem; +} + .tree-grid .section-drop-cell { padding: 0.45rem 0.6rem; } diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx index 469e4b2..2ed122a 100644 --- a/src/frontend/components/circuit-tree-editor.tsx +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -11,6 +11,7 @@ import { } from "../../domain/services/project-voltage.service"; import { getInsertionSortOrder, + isGridInsertionRowType, resolveGridInsertionIntent, } from "../utils/circuit-grid-insertion"; import { @@ -53,7 +54,7 @@ import type { RowType, } from "../utils/circuit-grid-model"; import { - buildVisibleGridRows, + buildVisibleGridRowsWithStructure, filterAndSortCircuitSections, getDistinctFilterValues, } from "../utils/circuit-grid-projection"; @@ -386,8 +387,11 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str // visibleRows is the single normalized grid used by render + navigation + editability. // This avoids selected/rendered/editable state drifting apart after filters/sorts/reloads. const visibleRows = useMemo(() => { - return buildVisibleGridRows(filteredSortedSections); - }, [filteredSortedSections]); + return buildVisibleGridRowsWithStructure(filteredSortedSections, { + headerComponents: data?.headerComponents ?? [], + footerComponents: data?.footerComponents ?? [], + }); + }, [data, filteredSortedSections]); function clearColumnFilter(key: CellKey) { setColumnFilters((current) => { @@ -1660,7 +1664,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const row = selectedCell ? findRow(selectedCell.rowKey) : null; const cell = row?.cells.find((entry) => entry.cellKey === selectedCell?.cellKey); const selection = - row && cell && row.rowType !== "section" + row && cell && isGridInsertionRowType(row.rowType) ? { rowType: row.rowType, cellKind: cell.kind, @@ -2351,7 +2355,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const row = selectedCell ? findRow(selectedCell.rowKey) : null; const cell = row?.cells.find((entry) => entry.cellKey === selectedCell?.cellKey); const intent = resolveGridDeleteIntent( - row && cell && row.rowType !== "section" + row && cell && isGridInsertionRowType(row.rowType) ? { rowType: row.rowType, cellKind: cell.kind, @@ -2986,6 +2990,54 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str {visibleRows.map((row) => { + if ( + row.rowType === "headerComponent" || + row.rowType === "groupComponent" || + row.rowType === "footerComponent" + ) { + const component = row.component!; + const protection = component.protectionDevice; + const protectionSummary = protection + ? [ + protection.type, + `${formatValue( + protection.ratedCurrentA, + "protectionRatedCurrent" + )} A`, + protection.fuseUtilizationCategory, + protection.tripCharacteristic, + protection.rcdType + ? `Typ ${protection.rcdType}` + : undefined, + protection.ratedResidualCurrentMa !== undefined + ? `${formatValue( + protection.ratedResidualCurrentMa, + "protectionRatedCurrent" + )} mA` + : undefined, + ] + .filter(Boolean) + .join(" · ") + : null; + return ( + + +
+ {component.equipmentIdentifier} + {component.name} + {protectionSummary ? ( + + {protectionSummary} + + ) : null} +
+ + + ); + } if (row.rowType === "section") { const section = data.sections.find((entry) => entry.id === row.sectionId)!; return ( diff --git a/src/frontend/utils/circuit-grid-insertion.ts b/src/frontend/utils/circuit-grid-insertion.ts index ccb73ad..b207193 100644 --- a/src/frontend/utils/circuit-grid-insertion.ts +++ b/src/frontend/utils/circuit-grid-insertion.ts @@ -7,6 +7,16 @@ export type GridInsertionRowType = export type GridInsertionCellKind = "circuitField" | "deviceField" | "readonly" | "computed"; +export function isGridInsertionRowType(rowType: string): rowType is GridInsertionRowType { + return ( + rowType === "circuitCompact" || + rowType === "circuitSummary" || + rowType === "deviceRow" || + rowType === "reserveCircuit" || + rowType === "placeholder" + ); +} + export interface GridInsertionSelection { rowType: GridInsertionRowType; cellKind: GridInsertionCellKind; diff --git a/src/frontend/utils/circuit-grid-model.ts b/src/frontend/utils/circuit-grid-model.ts index b922eb2..e96abef 100644 --- a/src/frontend/utils/circuit-grid-model.ts +++ b/src/frontend/utils/circuit-grid-model.ts @@ -37,12 +37,15 @@ export type CellKey = | "isReserve"; export type RowType = + | "headerComponent" | "section" + | "groupComponent" | "circuitCompact" | "circuitSummary" | "deviceRow" | "reserveCircuit" - | "placeholder"; + | "placeholder" + | "footerComponent"; export type CellKind = "circuitField" | "deviceField" | "computed" | "readonly"; export type GridValue = string | number | boolean | undefined; diff --git a/src/frontend/utils/circuit-grid-projection.ts b/src/frontend/utils/circuit-grid-projection.ts index 4eca978..ec28259 100644 --- a/src/frontend/utils/circuit-grid-projection.ts +++ b/src/frontend/utils/circuit-grid-projection.ts @@ -1,8 +1,10 @@ import type { CircuitTreeCircuitDto, + CircuitTreeComponentDto, CircuitTreeDeviceRowDto, CircuitTreeSectionDto, } from "../types.js"; +import { buildCircuitStructureProjection } from "./circuit-structure-projection"; import { allColumns, circuitOnlyColumns, @@ -42,6 +44,7 @@ export interface VisibleGridRow { sectionId: string; circuit?: CircuitTreeCircuitDto; device?: CircuitTreeDeviceRowDto; + component?: CircuitTreeComponentDto; cells: VisibleGridCell[]; } @@ -160,21 +163,64 @@ function makeVisibleGridRow( } export function buildVisibleGridRows(sections: readonly CircuitTreeSectionDto[]): VisibleGridRow[] { - const rows: VisibleGridRow[] = []; - for (const section of sections) { - rows.push({ - rowKey: `section:${section.id}`, - rowType: "section", - sectionId: section.id, - cells: allColumns.map((column) => ({ - cellKey: column.key, - editable: false, - kind: "readonly", - value: undefined, - })), - }); + return buildVisibleGridRowsWithStructure(sections, { + headerComponents: [], + footerComponents: [], + }); +} - for (const circuit of section.circuits) { +export function buildVisibleGridRowsWithStructure( + sections: readonly CircuitTreeSectionDto[], + structure: { + headerComponents: readonly CircuitTreeComponentDto[]; + footerComponents: readonly CircuitTreeComponentDto[]; + } +): VisibleGridRow[] { + const rows: VisibleGridRow[] = []; + const projected = buildCircuitStructureProjection({ + headerComponents: [...structure.headerComponents], + sections: [...sections], + footerComponents: [...structure.footerComponents], + }); + for (const row of projected) { + if ("component" in row) { + const component = row.component; + rows.push({ + rowKey: row.rowKey, + rowType: row.rowType, + sectionId: component.sectionId ?? `__${row.zone}__`, + component, + cells: allColumns.map((column) => ({ + cellKey: column.key, + editable: false, + kind: "readonly", + value: + column.key === "equipmentIdentifier" + ? component.equipmentIdentifier + : column.key === "displayName" + ? component.name + : undefined, + })), + }); + continue; + } + const section = row.section; + if (row.rowType === "groupHeader") { + rows.push({ + rowKey: `section:${section.id}`, + rowType: "section", + sectionId: section.id, + cells: allColumns.map((column) => ({ + cellKey: column.key, + editable: false, + kind: "readonly", + value: undefined, + })), + }); + continue; + } + if (row.rowType === "circuitBlock") { + const circuit = row.circuit; if (circuit.deviceRows.length === 0) { rows.push(makeVisibleGridRow("reserveCircuit", section.id, circuit)); } else if (circuit.deviceRows.length === 1) { @@ -185,8 +231,8 @@ export function buildVisibleGridRows(sections: readonly CircuitTreeSectionDto[]) rows.push(makeVisibleGridRow("deviceRow", section.id, circuit, device)); } } + continue; } - rows.push(makeVisibleGridRow("placeholder", section.id)); } return rows; diff --git a/src/frontend/utils/circuit-structure-projection.ts b/src/frontend/utils/circuit-structure-projection.ts index 9c2f15d..a667541 100644 --- a/src/frontend/utils/circuit-structure-projection.ts +++ b/src/frontend/utils/circuit-structure-projection.ts @@ -55,7 +55,7 @@ export function buildCircuitStructureProjection( zone: "group", section, }); - for (const component of ordered(section.components)) { + for (const component of ordered(section.components ?? [])) { rows.push({ rowKey: `group-component:${component.id}`, rowType: "groupComponent", diff --git a/tests/circuit-grid-insertion.test.ts b/tests/circuit-grid-insertion.test.ts index d4cf108..c2260f8 100644 --- a/tests/circuit-grid-insertion.test.ts +++ b/tests/circuit-grid-insertion.test.ts @@ -2,10 +2,18 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { getInsertionSortOrder, + isGridInsertionRowType, resolveGridInsertionIntent, } from "../src/frontend/utils/circuit-grid-insertion.js"; describe("circuit grid insertion", () => { + it("excludes structural rows from insertion and deletion commands", () => { + assert.equal(isGridInsertionRowType("headerComponent"), false); + assert.equal(isGridInsertionRowType("groupComponent"), false); + assert.equal(isGridInsertionRowType("footerComponent"), false); + assert.equal(isGridInsertionRowType("circuitCompact"), true); + }); + it("inserts a circuit after circuit-level selections", () => { assert.deepEqual( resolveGridInsertionIntent( diff --git a/tests/circuit-grid-projection.test.ts b/tests/circuit-grid-projection.test.ts index ad241e0..c5fdfdb 100644 --- a/tests/circuit-grid-projection.test.ts +++ b/tests/circuit-grid-projection.test.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { buildVisibleGridRows, + buildVisibleGridRowsWithStructure, filterAndSortCircuitSections, getDistinctFilterValues, } from "../src/frontend/utils/circuit-grid-projection.js"; @@ -176,6 +177,66 @@ describe("circuit grid projection", () => { ); }); + it("normalizes structural component rows around complete circuit groups", () => { + const component = { + circuitListId: "list-1", + equipmentIdentifier: "-Q0", + name: "Hauptschalter", + role: "main_switch" as const, + placement: "header" as const, + sortOrder: 10, + }; + const groupedSections = sections.map((section) => ({ + ...section, + components: [ + { + ...component, + id: "group-rcd", + sectionId: section.id, + equipmentIdentifier: "-1Q1.0", + name: "Gruppen-FI", + role: "group_residual_current_protection" as const, + placement: "group" as const, + }, + ], + })); + const rows = buildVisibleGridRowsWithStructure(groupedSections, { + headerComponents: [{ ...component, id: "main" }], + footerComponents: [ + { + ...component, + id: "actor", + equipmentIdentifier: "-K1", + name: "KNX Aktor", + role: "auxiliary", + placement: "footer", + }, + ], + }); + + assert.deepEqual( + rows.map((row) => row.rowType), + [ + "headerComponent", + "section", + "groupComponent", + "circuitCompact", + "circuitSummary", + "deviceRow", + "deviceRow", + "reserveCircuit", + "placeholder", + "footerComponent", + ] + ); + assert.equal( + rows.find((row) => row.rowType === "groupComponent")?.cells.some( + (cell) => cell.editable + ), + false + ); + }); + it("collects filter options from both circuit and device values", () => { const values = getDistinctFilterValues(sections, [ { key: "displayName", label: "Display name" }, From 756e307bd85a9b9c1af002369db23b516dc30b2f Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Fri, 31 Jul 2026 07:29:25 +0200 Subject: [PATCH 03/59] Manage distribution board components --- docs/current-architecture.md | 16 +- docs/spec/08-current-product-backlog.md | 2 + ...-board-components-and-protection-groups.md | 12 +- src/app/globals.css | 17 + .../components/circuit-tree-editor.tsx | 233 +++++++++++++ .../distribution-board-component-modal.tsx | 316 ++++++++++++++++++ src/frontend/utils/api.ts | 55 +++ .../distribution-board-component-editing.ts | 139 ++++++++ tests/distribution-board-component.test.ts | 95 ++++++ 9 files changed, 877 insertions(+), 8 deletions(-) create mode 100644 src/frontend/components/distribution-board-component-modal.tsx create mode 100644 src/frontend/utils/distribution-board-component-editing.ts diff --git a/docs/current-architecture.md b/docs/current-architecture.md index f3956cd..ed66858 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -357,9 +357,14 @@ vollständige Stromkreisblöcke und der freie Einfügeplatz. Leere Gruppen bleib sichtbar; Komponenten werden stabil nach `sortOrder` und ID sortiert. `circuit-grid-projection.ts` normalisiert diese Struktur zusammen mit den bisherigen kompakten und mehrzeiligen Stromkreisblöcken für den Editor. Kopf-, -Gruppen- und Fußkomponenten erscheinen dort zunächst als schreibgeschützte -Zeilen über die volle Tabellenbreite und nehmen nicht an Zellauswahl, -Tastatur-Einfügen oder Löschen teil. +Gruppen- und Fußkomponenten erscheinen als Zeilen über die volle Tabellenbreite +und nehmen nicht an Zellauswahl oder Tastatur-Einfügen teil. Hauptschalter und +Überspannungsableiter bleiben feste, schreibgeschützte Kopfkomponenten. +Optionale Gruppenvorsicherungen, Gruppen-FIs und zusätzliche Verteilergeräte +werden über beschriftete Modale angelegt, bearbeitet und nach Bestätigung +entfernt. Der Frontend-API-Adapter serialisiert dafür die vorhandenen +`distribution-board-component.*`-Commands; die verbindliche +geräteabhängige Validierung und atomare Revision bleiben serverseitig. `distribution-board.update` versioniert Etage, Netzart und den verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und @@ -433,8 +438,9 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät. Snapshot-Schema 7. Persistente Insert/Delete/Update-Commands für veränderliche Verteilerkomponenten, Gruppen einschließlich befüllter Unterbäume sowie vollständige Gruppensortierung sind integriert. Der Editor - zeigt die geschützte Struktur bereits schreibgeschützt an; dedizierte - Bearbeitungsfunktionen folgen in einem abgegrenzten Arbeitspaket. + zeigt die geschützte Struktur an und bearbeitet veränderliche Gruppen- und + Fußkomponenten über dedizierte Command-Modale. Gruppenverwaltung und + Stromkreisschutz folgen in weiteren abgegrenzten Arbeitspaketen. PostgreSQL ist bewusst nicht implementiert. Die Domainregeln und Transaktionsgrenzen sollen portabel bleiben; Schema und Betriebsmodell benötigen diff --git a/docs/spec/08-current-product-backlog.md b/docs/spec/08-current-product-backlog.md index 33751af..6880875 100644 --- a/docs/spec/08-current-product-backlog.md +++ b/docs/spec/08-current-product-backlog.md @@ -34,6 +34,8 @@ requirements and intended sequencing, not proof of implementation. - [x] Phase E1: tree read model for components, groups and protection devices. - [x] Phase E2a: pure header/group/footer structure projection. - [x] Phase E2b: render the structure projection in the editor grid. +- [x] Phase E3a: create, edit and delete mutable group and footer components. +- [ ] Phase E3b: group management and circuit-protection editing. - [ ] Phase E: editor projection and editing. - [ ] Phase F: documentation and full GUI verification. - [ ] Keep full electrical sizing and cable-dimensioning rules separate until diff --git a/docs/spec/09-distribution-board-components-and-protection-groups.md b/docs/spec/09-distribution-board-components-and-protection-groups.md index f1a8772..26bc6df 100644 --- a/docs/spec/09-distribution-board-components-and-protection-groups.md +++ b/docs/spec/09-distribution-board-components-and-protection-groups.md @@ -740,8 +740,8 @@ Acceptance: ### E. Editor Projection and Editing Status: In progress. The complete tree read model E1, pure structural -projection E2a and read-only grid rendering E2b are complete. Editing remains -pending. +projection E2a, grid rendering E2b and mutable component editing E3a are +complete. Group management and circuit-protection editing remain pending. - render fixed header components - render group components and circuit blocks @@ -750,7 +750,7 @@ pending. - add type-dependent protection editing - keep spreadsheet selection, keyboard and drag behavior coherent -Implemented in E1/E2a/E2b: +Implemented in E1/E2a/E2b/E3a: - the tree response exposes fixed header, group and auxiliary footer components plus both one-to-one protection layers @@ -764,6 +764,12 @@ Implemented in E1/E2a/E2b: components as read-only full-width rows - structural component rows are excluded from spreadsheet cell navigation, insertion and deletion commands until their dedicated editing controls exist +- group headings offer at most one optional upstream protection device and one + optional group RCD; the agreed group-RCD default is applied only when it is + created +- mutable group protection and auxiliary footer components use labeled modals, + type-dependent protection fields and persistent project commands +- fixed main-switch and surge-protection header components remain read-only Acceptance: diff --git a/src/app/globals.css b/src/app/globals.css index 9fe85c5..e66489a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -691,6 +691,23 @@ body { font-size: 0.78rem; } +.tree-grid .structure-component-actions { + display: inline-flex; + align-items: center; + gap: 0.35rem; + margin-left: auto; +} + +.tree-grid .structure-component-actions button { + font-size: 0.75rem; + padding: 0.18rem 0.45rem; +} + +.tree-grid .structure-component-fixed { + color: #64748b; + font-size: 0.75rem; +} + .tree-grid .section-drop-cell { padding: 0.45rem 0.6rem; } diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx index 2ed122a..b3ae1fc 100644 --- a/src/frontend/components/circuit-tree-editor.tsx +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -47,6 +47,15 @@ import { import { buildCircuitSectionRenumberAssignments, } from "../utils/circuit-section-renumber-command"; +import { + buildDistributionBoardComponentSnapshot, + getNextComponentSortOrder, + getSuggestedGroupComponentIdentifier, + toDistributionBoardComponentSnapshot, + updateDistributionBoardComponentSnapshot, + type DistributionBoardComponentEditorValues, + type MutableDistributionBoardComponentRole, +} from "../utils/distribution-board-component-editing"; import { loadCircuitEditorSnapshot } from "../utils/circuit-editor-history"; import type { CellKey, @@ -62,11 +71,13 @@ import type { VisibleGridRow } from "../utils/circuit-grid-projection"; import { deleteCircuitCommand, deleteCircuitDeviceRowCommand, + deleteDistributionBoardComponentCommand, getCircuitTree, getNextCircuitIdentifier, getProjectHistory, insertCircuitCommand, insertCircuitDeviceRowCommand, + insertDistributionBoardComponentCommand, listProjectDevices, moveCircuitDeviceRowsCommand, moveCircuitDeviceRowsToNewCircuitCommand, @@ -76,10 +87,12 @@ import { redoProjectCommand, updateCircuitById, updateCircuitDeviceRowById, + updateDistributionBoardComponentCommand, undoProjectCommand, } from "../utils/api"; import type { CircuitTreeCircuitDto, + CircuitTreeComponentDto, CircuitTreeResponseDto, CreateCircuitDeviceRowInputDto, CreateCircuitInputDto, @@ -93,6 +106,7 @@ import type { import type { CircuitSnapshot, } from "../../domain/models/circuit-structure-project-command.model"; +import { DistributionBoardComponentModal } from "./distribution-board-component-modal"; type SaveDirection = "stay" | "next" | "prev"; type StartEditMode = "selectExisting" | "replaceWithTypedChar"; @@ -161,6 +175,18 @@ function normalizeUiError(err: unknown): string { return message; } +type StructureComponentEditorIntent = + | { + kind: "create"; + role: MutableDistributionBoardComponentRole; + sectionId?: string; + initialEquipmentIdentifier?: string; + } + | { + kind: "edit"; + component: CircuitTreeComponentDto; + }; + function getFullColumnLabel(column: ColumnDef): string { return column.fullLabel ?? column.label; } @@ -201,6 +227,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const [editingCell, setEditingCell] = useState(null); const [activeSectionId, setActiveSectionId] = useState(null); const [isSaving, setIsSaving] = useState(false); + const [componentEditorIntent, setComponentEditorIntent] = + useState(null); const [projectDevices, setProjectDevices] = useState([]); const [isProjectDeviceDrawerOpen, setIsProjectDeviceDrawerOpen] = useState(false); @@ -1031,6 +1059,85 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str await applyHistory("undo"); } + async function handleSaveDistributionBoardComponent( + values: DistributionBoardComponentEditorValues + ) { + const intent = componentEditorIntent; + if (!intent || !data) { + return; + } + await runCommand({ + label: + intent.kind === "edit" + ? "Verteilerkomponente bearbeiten" + : "Verteilerkomponente hinzufügen", + redo: async () => { + const result = + intent.kind === "edit" + ? await (() => { + const expected = toDistributionBoardComponentSnapshot( + intent.component + ); + return updateDistributionBoardComponentCommand( + projectId, + getExpectedProjectRevision(), + expected, + updateDistributionBoardComponentSnapshot(expected, values) + ); + })() + : await (() => { + const components = + intent.role === "auxiliary" + ? data.footerComponents + : data.sections.find( + (section) => section.id === intent.sectionId + )?.components ?? []; + const snapshot = buildDistributionBoardComponentSnapshot({ + id: crypto.randomUUID(), + circuitListId, + role: intent.role, + sectionId: intent.sectionId, + sortOrder: getNextComponentSortOrder(components), + values, + }); + return insertDistributionBoardComponentCommand( + projectId, + getExpectedProjectRevision(), + snapshot + ); + })(); + applyProjectCommandResult(result); + setComponentEditorIntent(null); + return null; + }, + }); + } + + async function handleDeleteDistributionBoardComponent( + component: CircuitTreeComponentDto + ) { + if ( + !confirm( + `Verteilerkomponente „${component.equipmentIdentifier} ${component.name}“ entfernen?` + ) + ) { + return; + } + await runCommand({ + label: "Verteilerkomponente entfernen", + redo: async () => { + const result = await deleteDistributionBoardComponentCommand( + projectId, + getExpectedProjectRevision(), + toDistributionBoardComponentSnapshot(component) + ); + applyProjectCommandResult(result); + setComponentEditorIntent(null); + return null; + }, + }); + } + async function handleRedo() { if (historyBusy || isSaving || !historyState || historyState.redoDepth === 0) { return; @@ -2605,6 +2712,29 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str return (
+ {componentEditorIntent ? ( + setComponentEditorIntent(null)} + onSave={handleSaveDistributionBoardComponent} + role={ + componentEditorIntent.kind === "edit" + ? (componentEditorIntent.component + .role as MutableDistributionBoardComponentRole) + : componentEditorIntent.role + } + /> + ) : null}
+ + + ) : ( + + Feste Verteilerkomponente + + )} +
@@ -3040,6 +3219,14 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str } if (row.rowType === "section") { const section = data.sections.find((entry) => entry.id === row.sectionId)!; + const hasUpstreamProtection = section.components.some( + (component) => + component.role === "group_upstream_protection" + ); + const hasGroupRcd = section.components.some( + (component) => + component.role === "group_residual_current_protection" + ); return (
+ + diff --git a/src/frontend/components/distribution-board-component-modal.tsx b/src/frontend/components/distribution-board-component-modal.tsx new file mode 100644 index 0000000..332d800 --- /dev/null +++ b/src/frontend/components/distribution-board-component-modal.tsx @@ -0,0 +1,316 @@ +"use client"; + +import { type FormEvent, useState } from "react"; +import { + allowedRatedCurrentsAByProtectionDeviceType, + breakerTripCharacteristics, + fuseProtectionDeviceTypes, + fuseUtilizationCategories, + protectionDeviceTypeLabels, + ratedResidualCurrentsMa, + rcdTypes, + type ProtectionDeviceType, +} from "../../shared/constants/protection-device"; +import type { CircuitTreeComponentDto } from "../types"; +import type { + DistributionBoardComponentEditorValues, + MutableDistributionBoardComponentRole, +} from "../utils/distribution-board-component-editing"; +import { FormModal } from "./form-modal"; + +interface DistributionBoardComponentModalProps { + initialComponent?: CircuitTreeComponentDto; + initialEquipmentIdentifier?: string; + isSaving: boolean; + onClose: () => void; + onSave: (values: DistributionBoardComponentEditorValues) => Promise; + role: MutableDistributionBoardComponentRole; +} + +const upstreamProtectionTypes: readonly ProtectionDeviceType[] = [ + ...fuseProtectionDeviceTypes, + "LS", +]; + +export function DistributionBoardComponentModal({ + initialComponent, + initialEquipmentIdentifier = "", + isSaving, + onClose, + onSave, + role, +}: DistributionBoardComponentModalProps) { + const [equipmentIdentifier, setEquipmentIdentifier] = useState( + initialComponent?.equipmentIdentifier ?? initialEquipmentIdentifier + ); + const [name, setName] = useState( + initialComponent?.name ?? defaultName(role) + ); + const initialProtection = initialComponent?.protectionDevice; + const [protectionType, setProtectionType] = useState< + ProtectionDeviceType | "" + >( + initialProtection?.type ?? + (role === "group_residual_current_protection" ? "FI" : "") + ); + const [ratedCurrentA, setRatedCurrentA] = useState( + initialProtection?.ratedCurrentA ?? + (role === "group_residual_current_protection" ? 40 : 0) + ); + const [fuseUtilizationCategory, setFuseUtilizationCategory] = useState( + initialProtection?.fuseUtilizationCategory ?? "gG" + ); + const [tripCharacteristic, setTripCharacteristic] = useState( + initialProtection?.tripCharacteristic ?? "B" + ); + const [rcdType, setRcdType] = useState( + initialProtection?.rcdType ?? "A" + ); + const [ratedResidualCurrentMa, setRatedResidualCurrentMa] = useState( + initialProtection?.ratedResidualCurrentMa ?? 30 + ); + + const isAuxiliary = role === "auxiliary"; + const availableProtectionTypes = + role === "group_residual_current_protection" + ? (["FI"] as const) + : upstreamProtectionTypes; + const usesFuseCategory = + protectionType !== "" && + (fuseProtectionDeviceTypes as readonly string[]).includes(protectionType); + const usesTripCharacteristic = protectionType === "LS"; + const usesResidualCurrent = protectionType === "FI"; + const allowedRatedCurrents = + protectionType === "" + ? [] + : allowedRatedCurrentsAByProtectionDeviceType[protectionType]; + + function handleProtectionTypeChange(type: ProtectionDeviceType) { + setProtectionType(type); + setRatedCurrentA(allowedRatedCurrentsAByProtectionDeviceType[type][0]); + setFuseUtilizationCategory("gG"); + setTripCharacteristic("B"); + setRcdType("A"); + setRatedResidualCurrentMa(30); + } + + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + await onSave({ + equipmentIdentifier, + name, + ...(!isAuxiliary && protectionType + ? { + protectionDevice: { + type: protectionType, + ratedCurrentA, + ...(usesFuseCategory + ? { fuseUtilizationCategory } + : {}), + ...(usesTripCharacteristic + ? { tripCharacteristic } + : {}), + ...(usesResidualCurrent + ? { rcdType, ratedResidualCurrentMa } + : {}), + }, + } + : {}), + }); + } + + const isValid = + equipmentIdentifier.trim().length > 0 && + name.trim().length > 0 && + (isAuxiliary || + (protectionType !== "" && + (allowedRatedCurrents as readonly number[]).includes(ratedCurrentA))); + + return ( + +
+
+ + setEquipmentIdentifier(event.target.value)} + required + value={equipmentIdentifier} + /> +
+
+ + setName(event.target.value)} + required + value={name} + /> +
+ {!isAuxiliary ? ( + <> +
+ + +
+
+ + +
+ {usesFuseCategory ? ( +
+ + +
+ ) : null} + {usesTripCharacteristic ? ( +
+ + +
+ ) : null} + {usesResidualCurrent ? ( + <> +
+ + +
+
+ + +
+ + ) : null} + + ) : null} +
+
+ ); +} + +function defaultName(role: MutableDistributionBoardComponentRole): string { + if (role === "group_upstream_protection") { + return "Gruppenvorsicherung"; + } + if (role === "group_residual_current_protection") { + return "Gruppen-FI"; + } + return "Verteilergerät"; +} diff --git a/src/frontend/utils/api.ts b/src/frontend/utils/api.ts index 7759959..0ad5cf0 100644 --- a/src/frontend/utils/api.ts +++ b/src/frontend/utils/api.ts @@ -52,6 +52,9 @@ import type { import type { CircuitSectionRenumberAssignment, } from "../../domain/models/circuit-section-renumber-project-command.model"; +import type { + DistributionBoardComponentSnapshot, +} from "../../domain/models/distribution-board-component-structure-project-command.model"; async function request(url: string, init?: RequestInit): Promise { const response = await fetch(url, { @@ -320,6 +323,58 @@ export function getCircuitTree(projectId: string, circuitListId: string) { return request(`/api/projects/${projectId}/circuit-lists/${circuitListId}/tree`); } +export function insertDistributionBoardComponentCommand( + projectId: string, + expectedRevision: number, + snapshot: DistributionBoardComponentSnapshot +) { + return executeProjectCommand( + projectId, + expectedRevision, + { + schemaVersion: 1, + type: "distribution-board-component.insert", + payload: { snapshot }, + }, + "Verteilerkomponente hinzufügen" + ); +} + +export function updateDistributionBoardComponentCommand( + projectId: string, + expectedRevision: number, + expected: DistributionBoardComponentSnapshot, + target: DistributionBoardComponentSnapshot +) { + return executeProjectCommand( + projectId, + expectedRevision, + { + schemaVersion: 1, + type: "distribution-board-component.update", + payload: { expected, target }, + }, + "Verteilerkomponente bearbeiten" + ); +} + +export function deleteDistributionBoardComponentCommand( + projectId: string, + expectedRevision: number, + snapshot: DistributionBoardComponentSnapshot +) { + return executeProjectCommand( + projectId, + expectedRevision, + { + schemaVersion: 1, + type: "distribution-board-component.delete", + payload: { snapshot }, + }, + "Verteilerkomponente entfernen" + ); +} + export function updateCircuitById( projectId: string, expectedRevision: number, diff --git a/src/frontend/utils/distribution-board-component-editing.ts b/src/frontend/utils/distribution-board-component-editing.ts new file mode 100644 index 0000000..c6bb879 --- /dev/null +++ b/src/frontend/utils/distribution-board-component-editing.ts @@ -0,0 +1,139 @@ +import type { + DistributionBoardComponentSnapshot, +} from "../../domain/models/distribution-board-component-structure-project-command.model"; +import type { + CircuitTreeComponentDto, + CircuitTreeProtectionDeviceDto, + CircuitTreeSectionDto, +} from "../types"; +import { circuitGroupCategoryNumbers } from "../../shared/constants/circuit-group"; + +export type MutableDistributionBoardComponentRole = + | "group_upstream_protection" + | "group_residual_current_protection" + | "auxiliary"; + +export interface DistributionBoardComponentEditorValues { + equipmentIdentifier: string; + name: string; + protectionDevice?: CircuitTreeProtectionDeviceDto; +} + +export function toDistributionBoardComponentSnapshot( + component: CircuitTreeComponentDto +): DistributionBoardComponentSnapshot { + if ( + component.role !== "group_upstream_protection" && + component.role !== "group_residual_current_protection" && + component.role !== "auxiliary" + ) { + throw new Error("Feste Verteilerkomponenten können hier nicht bearbeitet werden."); + } + return { + component: { + id: component.id, + circuitListId: component.circuitListId, + sectionId: component.sectionId ?? null, + equipmentIdentifier: component.equipmentIdentifier, + name: component.name, + role: component.role, + placement: component.placement, + sortOrder: component.sortOrder, + }, + protectionDevice: component.protectionDevice + ? { + componentId: component.id, + type: component.protectionDevice.type, + ratedCurrentA: component.protectionDevice.ratedCurrentA, + fuseUtilizationCategory: + component.protectionDevice.fuseUtilizationCategory ?? null, + tripCharacteristic: + component.protectionDevice.tripCharacteristic ?? null, + rcdType: component.protectionDevice.rcdType ?? null, + ratedResidualCurrentMa: + component.protectionDevice.ratedResidualCurrentMa ?? null, + } + : null, + }; +} + +export function buildDistributionBoardComponentSnapshot(input: { + id: string; + circuitListId: string; + role: MutableDistributionBoardComponentRole; + sectionId?: string; + sortOrder: number; + values: DistributionBoardComponentEditorValues; +}): DistributionBoardComponentSnapshot { + const groupComponent = input.role !== "auxiliary"; + if (groupComponent && !input.sectionId) { + throw new Error("Für den Gruppenschutz fehlt die Stromkreisgruppe."); + } + if (groupComponent && !input.values.protectionDevice) { + throw new Error("Für den Gruppenschutz fehlt die Schutzgerätekonfiguration."); + } + return { + component: { + id: input.id, + circuitListId: input.circuitListId, + sectionId: groupComponent ? input.sectionId! : null, + equipmentIdentifier: input.values.equipmentIdentifier.trim(), + name: input.values.name.trim(), + role: input.role, + placement: groupComponent ? "group" : "footer", + sortOrder: input.sortOrder, + }, + protectionDevice: + groupComponent && input.values.protectionDevice + ? { + componentId: input.id, + type: input.values.protectionDevice.type, + ratedCurrentA: input.values.protectionDevice.ratedCurrentA, + fuseUtilizationCategory: + input.values.protectionDevice.fuseUtilizationCategory ?? null, + tripCharacteristic: + input.values.protectionDevice.tripCharacteristic ?? null, + rcdType: input.values.protectionDevice.rcdType ?? null, + ratedResidualCurrentMa: + input.values.protectionDevice.ratedResidualCurrentMa ?? null, + } + : null, + }; +} + +export function updateDistributionBoardComponentSnapshot( + expected: DistributionBoardComponentSnapshot, + values: DistributionBoardComponentEditorValues +): DistributionBoardComponentSnapshot { + return buildDistributionBoardComponentSnapshot({ + id: expected.component.id, + circuitListId: expected.component.circuitListId, + role: expected.component.role, + sectionId: expected.component.sectionId ?? undefined, + sortOrder: expected.component.sortOrder, + values, + }); +} + +export function getSuggestedGroupComponentIdentifier( + section: CircuitTreeSectionDto, + role: + | "group_upstream_protection" + | "group_residual_current_protection" +): string { + if (!section.category || !section.groupNumber) { + return ""; + } + const categoryNumber = circuitGroupCategoryNumbers[section.category]; + const deviceLetter = + role === "group_upstream_protection" ? "F" : "Q"; + return `-${categoryNumber}${deviceLetter}${section.groupNumber}.0`; +} + +export function getNextComponentSortOrder( + components: readonly { sortOrder: number }[] +): number { + return components.length === 0 + ? 10 + : Math.max(...components.map((component) => component.sortOrder)) + 10; +} diff --git a/tests/distribution-board-component.test.ts b/tests/distribution-board-component.test.ts index 5652e9c..9396675 100644 --- a/tests/distribution-board-component.test.ts +++ b/tests/distribution-board-component.test.ts @@ -7,6 +7,13 @@ import { distributionBoardComponentRoles, } from "../src/shared/constants/distribution-board-component.js"; import { circuitGroupCategories } from "../src/shared/constants/circuit-group.js"; +import { + buildDistributionBoardComponentSnapshot, + getNextComponentSortOrder, + getSuggestedGroupComponentIdentifier, + toDistributionBoardComponentSnapshot, + updateDistributionBoardComponentSnapshot, +} from "../src/frontend/utils/distribution-board-component-editing.js"; import "./distribution-board-component-structure-project-command.repository.test.js"; describe("distribution board component catalog", () => { @@ -47,4 +54,92 @@ describe("distribution board component catalog", () => { placement: "header", }); }); + + it("builds exact editable component snapshots for persistent commands", () => { + const groupRcd = buildDistributionBoardComponentSnapshot({ + id: "component-1", + circuitListId: "list-1", + role: "group_residual_current_protection", + sectionId: "section-1", + sortOrder: 20, + values: { + equipmentIdentifier: " -1Q1.0 ", + name: " Gruppen-FI ", + protectionDevice: { + type: "FI", + ratedCurrentA: 40, + rcdType: "A", + ratedResidualCurrentMa: 30, + }, + }, + }); + + assert.deepEqual(groupRcd, { + component: { + id: "component-1", + circuitListId: "list-1", + sectionId: "section-1", + equipmentIdentifier: "-1Q1.0", + name: "Gruppen-FI", + role: "group_residual_current_protection", + placement: "group", + sortOrder: 20, + }, + protectionDevice: { + componentId: "component-1", + type: "FI", + ratedCurrentA: 40, + fuseUtilizationCategory: null, + tripCharacteristic: null, + rcdType: "A", + ratedResidualCurrentMa: 30, + }, + }); + assert.deepEqual( + updateDistributionBoardComponentSnapshot(groupRcd, { + equipmentIdentifier: "-1Q1.0", + name: "FI Beleuchtung", + protectionDevice: { + type: "FI", + ratedCurrentA: 63, + rcdType: "B+", + ratedResidualCurrentMa: 300, + }, + }).component, + { ...groupRcd.component, name: "FI Beleuchtung" } + ); + }); + + it("maps tree DTOs and derives stable group suggestions and sort positions", () => { + const snapshot = toDistributionBoardComponentSnapshot({ + id: "actor-1", + circuitListId: "list-1", + equipmentIdentifier: "-K1", + name: "KNX Aktor", + role: "auxiliary", + placement: "footer", + sortOrder: 10, + }); + assert.equal(snapshot.component.sectionId, null); + assert.equal(snapshot.protectionDevice, null); + assert.equal(getNextComponentSortOrder([{ sortOrder: 10 }, { sortOrder: 40 }]), 50); + assert.equal( + getSuggestedGroupComponentIdentifier( + { + id: "section-1", + key: "lighting", + displayName: "Beleuchtung 2", + prefix: "-1F2", + sortOrder: 10, + category: "lighting", + groupNumber: 2, + sectionTotalPower: 0, + components: [], + circuits: [], + }, + "group_upstream_protection" + ), + "-1F2.0" + ); + }); }); From 0c92fbd09eb2abf4dad650a2efc6b4249396f472 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Fri, 31 Jul 2026 07:32:46 +0200 Subject: [PATCH 04/59] Manage circuit groups --- docs/current-architecture.md | 6 + docs/spec/08-current-product-backlog.md | 4 +- ...-board-components-and-protection-groups.md | 9 +- .../components/circuit-group-modal.tsx | 97 +++++++++++++ .../components/circuit-tree-editor.tsx | 134 ++++++++++++++++++ src/frontend/utils/api.ts | 55 +++++++ src/frontend/utils/circuit-group-editing.ts | 105 ++++++++++++++ tests/circuit-group-numbering.test.ts | 72 ++++++++++ 8 files changed, 480 insertions(+), 2 deletions(-) create mode 100644 src/frontend/components/circuit-group-modal.tsx create mode 100644 src/frontend/utils/circuit-group-editing.ts diff --git a/docs/current-architecture.md b/docs/current-architecture.md index ed66858..d47765b 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -365,6 +365,12 @@ werden über beschriftete Modale angelegt, bearbeitet und nach Bestätigung entfernt. Der Frontend-API-Adapter serialisiert dafür die vorhandenen `distribution-board-component.*`-Commands; die verbindliche geräteabhängige Validierung und atomare Revision bleiben serverseitig. +Neue Stromkreisgruppen erhalten im Editor die höchste vorhandene +Gruppennummer ihrer Kategorie plus eins und ein daraus abgeleitetes Präfix. +Das Bearbeiten ändert ausschließlich den Anzeigenamen. Nur vollständig leere +Gruppen können über `circuit-group.delete` entfernt werden; für befüllte +Gruppen bleibt die Aktion bis zum gesonderten Warn- und Unterbaumdialog +gesperrt. `distribution-board.update` versioniert Etage, Netzart und den verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und diff --git a/docs/spec/08-current-product-backlog.md b/docs/spec/08-current-product-backlog.md index 6880875..01123a7 100644 --- a/docs/spec/08-current-product-backlog.md +++ b/docs/spec/08-current-product-backlog.md @@ -35,7 +35,9 @@ requirements and intended sequencing, not proof of implementation. - [x] Phase E2a: pure header/group/footer structure projection. - [x] Phase E2b: render the structure projection in the editor grid. - [x] Phase E3a: create, edit and delete mutable group and footer components. -- [ ] Phase E3b: group management and circuit-protection editing. +- [x] Phase E3b1: create, rename and safely delete empty circuit groups. +- [ ] Phase E3b2: circuit-protection editing. +- [ ] Phase E4: group reorder, renumber, circuit moves and populated-delete warning. - [ ] Phase E: editor projection and editing. - [ ] Phase F: documentation and full GUI verification. - [ ] Keep full electrical sizing and cable-dimensioning rules separate until diff --git a/docs/spec/09-distribution-board-components-and-protection-groups.md b/docs/spec/09-distribution-board-components-and-protection-groups.md index 26bc6df..20ca71f 100644 --- a/docs/spec/09-distribution-board-components-and-protection-groups.md +++ b/docs/spec/09-distribution-board-components-and-protection-groups.md @@ -741,7 +741,8 @@ Acceptance: Status: In progress. The complete tree read model E1, pure structural projection E2a, grid rendering E2b and mutable component editing E3a are -complete. Group management and circuit-protection editing remain pending. +complete. Basic group management E3b1 is also complete. Circuit-protection +editing and structural drag/renumber/delete workflows remain pending. - render fixed header components - render group components and circuit blocks @@ -770,6 +771,12 @@ Implemented in E1/E2a/E2b/E3a: - mutable group protection and auxiliary footer components use labeled modals, type-dependent protection fields and persistent project commands - fixed main-switch and surge-protection header components remain read-only +- users can create a group in any supported category; its number is the + category's highest existing number plus one and its prefix is generated +- group editing changes only the display name; category, number, prefix and + child BMKs remain stable +- an exactly empty group can be removed through the persistent group command; + populated deletion stays disabled until the dedicated warning UI is present Acceptance: diff --git a/src/frontend/components/circuit-group-modal.tsx b/src/frontend/components/circuit-group-modal.tsx new file mode 100644 index 0000000..ccfbc51 --- /dev/null +++ b/src/frontend/components/circuit-group-modal.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { type FormEvent, useState } from "react"; +import { + circuitGroupCategories, + circuitGroupCategoryLabels, + type CircuitGroupCategory, +} from "../../shared/constants/circuit-group"; +import type { CircuitTreeSectionDto } from "../types"; +import { FormModal } from "./form-modal"; + +interface CircuitGroupModalProps { + initialSection?: CircuitTreeSectionDto; + isSaving: boolean; + onClose: () => void; + onSave: (values: { + category: CircuitGroupCategory; + displayName: string; + }) => Promise; +} + +export function CircuitGroupModal({ + initialSection, + isSaving, + onClose, + onSave, +}: CircuitGroupModalProps) { + const [category, setCategory] = useState( + initialSection?.category ?? "lighting" + ); + const [displayName, setDisplayName] = useState( + initialSection?.displayName ?? "" + ); + + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + await onSave({ category, displayName }); + } + + return ( + +
+
+ + +
+
+ + setDisplayName(event.target.value)} + placeholder={ + initialSection + ? undefined + : "Leer lassen für den automatisch erzeugten Namen" + } + required={Boolean(initialSection)} + value={displayName} + /> +
+
+
+ ); +} diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx index b3ae1fc..6ecd482 100644 --- a/src/frontend/components/circuit-tree-editor.tsx +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -47,6 +47,12 @@ import { import { buildCircuitSectionRenumberAssignments, } from "../utils/circuit-section-renumber-command"; +import { + buildNewCircuitGroupSnapshot, + canDeleteCircuitGroup, + renameCircuitGroupSnapshot, + toCircuitGroupSnapshot, +} from "../utils/circuit-group-editing"; import { buildDistributionBoardComponentSnapshot, getNextComponentSortOrder, @@ -71,12 +77,14 @@ import type { VisibleGridRow } from "../utils/circuit-grid-projection"; import { deleteCircuitCommand, deleteCircuitDeviceRowCommand, + deleteCircuitGroupCommand, deleteDistributionBoardComponentCommand, getCircuitTree, getNextCircuitIdentifier, getProjectHistory, insertCircuitCommand, insertCircuitDeviceRowCommand, + insertCircuitGroupCommand, insertDistributionBoardComponentCommand, listProjectDevices, moveCircuitDeviceRowsCommand, @@ -87,6 +95,7 @@ import { redoProjectCommand, updateCircuitById, updateCircuitDeviceRowById, + updateCircuitGroupCommand, updateDistributionBoardComponentCommand, undoProjectCommand, } from "../utils/api"; @@ -107,6 +116,8 @@ import type { CircuitSnapshot, } from "../../domain/models/circuit-structure-project-command.model"; import { DistributionBoardComponentModal } from "./distribution-board-component-modal"; +import { CircuitGroupModal } from "./circuit-group-modal"; +import type { CircuitGroupCategory } from "../../shared/constants/circuit-group"; type SaveDirection = "stay" | "next" | "prev"; type StartEditMode = "selectExisting" | "replaceWithTypedChar"; @@ -187,6 +198,10 @@ type StructureComponentEditorIntent = component: CircuitTreeComponentDto; }; +type CircuitGroupEditorIntent = + | { kind: "create" } + | { kind: "edit"; section: CircuitTreeResponseDto["sections"][number] }; + function getFullColumnLabel(column: ColumnDef): string { return column.fullLabel ?? column.label; } @@ -229,6 +244,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const [isSaving, setIsSaving] = useState(false); const [componentEditorIntent, setComponentEditorIntent] = useState(null); + const [circuitGroupEditorIntent, setCircuitGroupEditorIntent] = + useState(null); const [projectDevices, setProjectDevices] = useState([]); const [isProjectDeviceDrawerOpen, setIsProjectDeviceDrawerOpen] = useState(false); @@ -1138,6 +1155,76 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str }); } + async function handleSaveCircuitGroup(values: { + category: CircuitGroupCategory; + displayName: string; + }) { + const intent = circuitGroupEditorIntent; + if (!intent || !data) { + return; + } + await runCommand({ + label: + intent.kind === "create" + ? "Stromkreisgruppe hinzufügen" + : "Stromkreisgruppe bearbeiten", + redo: async () => { + const result = + intent.kind === "create" + ? await insertCircuitGroupCommand( + projectId, + getExpectedProjectRevision(), + buildNewCircuitGroupSnapshot({ + id: crypto.randomUUID(), + circuitListId, + category: values.category, + displayName: values.displayName, + sections: data.sections, + }) + ) + : await (() => { + const expected = toCircuitGroupSnapshot( + intent.section, + circuitListId + ); + return updateCircuitGroupCommand( + projectId, + getExpectedProjectRevision(), + expected, + renameCircuitGroupSnapshot(expected, values.displayName) + ); + })(); + applyProjectCommandResult(result); + setCircuitGroupEditorIntent(null); + return null; + }, + }); + } + + async function handleDeleteEmptyCircuitGroup( + section: CircuitTreeResponseDto["sections"][number] + ) { + if (!canDeleteCircuitGroup(section)) { + return; + } + if (!confirm(`Leere Stromkreisgruppe „${section.displayName}“ entfernen?`)) { + return; + } + await runCommand({ + label: "Leere Stromkreisgruppe entfernen", + redo: async () => { + const result = await deleteCircuitGroupCommand( + projectId, + getExpectedProjectRevision(), + toCircuitGroupSnapshot(section, circuitListId) + ); + applyProjectCommandResult(result); + setCircuitGroupEditorIntent(null); + return null; + }, + }); + } + async function handleRedo() { if (historyBusy || isSaving || !historyState || historyState.redoDepth === 0) { return; @@ -2735,6 +2822,18 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str } /> ) : null} + {circuitGroupEditorIntent ? ( + setCircuitGroupEditorIntent(null)} + onSave={handleSaveCircuitGroup} + /> + ) : null}
+
+ + + + +
+
+
+

+ Verteilung kopieren +

+

+ Erstellt eine vollständige Kopie des zuletzt gespeicherten + Stands einschließlich Gruppen, Stromkreisen, Gerätezeilen und + Schutzgeräten. Noch nicht gespeicherte Änderungen oben werden + nicht übernommen. +

+
+
+ + + setEditingBoardCopyName(event.target.value) + } + value={editingBoardCopyName} + /> +
+
+ +
+
+
+
+
+

+ Gefahrenbereich +

+

+ Entfernt die vollständige Verteilung. Die Aktion wird erst + nach einer ausdrücklichen Warnung ausgeführt und kann über die + projektweite Historie rückgängig gemacht werden. +

+ +
) : null} diff --git a/src/frontend/types.ts b/src/frontend/types.ts index baf92c6..71c832f 100644 --- a/src/frontend/types.ts +++ b/src/frontend/types.ts @@ -114,6 +114,11 @@ export interface DistributionBoardCommandResultDto distributionBoard: DistributionBoardDto; } +export interface DistributionBoardCopyCommandResultDto + extends DistributionBoardCommandResultDto { + circuitList: CircuitListDto; +} + export interface DistributionBoardDeleteCommandResultDto extends ProjectCommandResultDto { distributionBoardId: string; diff --git a/src/frontend/utils/api.ts b/src/frontend/utils/api.ts index defe0cd..5b6b624 100644 --- a/src/frontend/utils/api.ts +++ b/src/frontend/utils/api.ts @@ -6,6 +6,7 @@ import type { CreateGlobalDeviceInput, DistributionBoardDto, DistributionBoardCommandResultDto, + DistributionBoardCopyCommandResultDto, DistributionBoardDeleteCommandResultDto, FloorDto, GlobalDeviceDto, @@ -338,7 +339,7 @@ export function copyDistributionBoard( name: string, expectedRevision: number ) { - return request( + return request( `/api/projects/${projectId}/distribution-boards/${distributionBoardId}/copy`, { method: "POST", diff --git a/src/server/controllers/distribution-board.controller.ts b/src/server/controllers/distribution-board.controller.ts index 70ab7e9..1ca8b1b 100644 --- a/src/server/controllers/distribution-board.controller.ts +++ b/src/server/controllers/distribution-board.controller.ts @@ -140,6 +140,7 @@ export async function copyDistributionBoard( return res.status(201).json({ ...result, distributionBoard: copy.distributionBoard, + circuitList: copy.circuitList, }); } catch (error) { return respondWithProjectCommandError(error, res); From 734a2bcfc4a8d57a682634207567759e96cb7b70 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Fri, 31 Jul 2026 14:07:26 +0200 Subject: [PATCH 21/59] Create clean release database baseline --- AGENTS.md | 22 +- README.md | 8 +- ...eistungsbilanz-2026-05-03T19-47-12-976Z.db | Bin 90112 -> 0 bytes docs/README.md | 3 - docs/circuit-list-editor-api.md | 8 - docs/circuit-list-editor-architecture.md | 5 - docs/circuit-list-editor-known-limitations.md | 1 - docs/circuit-list-editor-migration.md | 90 - docs/current-architecture.md | 53 +- docs/development-workflow.md | 17 +- docs/local-db-circuit-first-migration.md | 137 - ...history-and-external-model-architecture.md | 2 - docs/spec/07-implementation-phases-todo.md | 13 +- package.json | 8 +- scripts/db-backfill-sections.ts | 26 - scripts/db-migrate-legacy-consumers.ts | 61 - scripts/db-verify-circuit-schema.js | 18 +- scripts/run-tests.mjs | 30 + src/db/migrations/0000_bizarre_colossus.sql | 28 - src/db/migrations/0000_whole_surge.sql | 300 +++ src/db/migrations/0001_global_devices.sql | 12 - .../0002_project_voltage_defaults.sql | 3 - .../migrations/0003_project_floors_rooms.sql | 19 - .../0004_circuit_lists_and_entry_fields.sql | 44 - src/db/migrations/0005_project_devices.sql | 14 - .../migrations/0006_device_display_name.sql | 7 - .../migrations/0007_consumer_device_link.sql | 3 - .../migrations/0008_circuit_first_model.sql | 99 - .../0009_project_device_circuit_fields.sql | 24 - .../0010_circuit_control_requirement.sql | 1 - .../0011_project_device_canonical_fields.sql | 9 - .../0012_project_revision_foundation.sql | 24 - .../0013_project_history_stacks.sql | 11 - src/db/migrations/0014_project_snapshots.sql | 16 - .../migrations/0015_modern_madame_masque.sql | 2 - src/db/migrations/0016_dashing_darkstar.sql | 4 - src/db/migrations/0017_vengeful_romulus.sql | 2 - src/db/migrations/0018_fancy_argent.sql | 1 - .../0019_normalize_project_voltages.sql | 48 - .../migrations/0020_normalize_phase_types.sql | 85 - .../0021_fill_missing_phase_types.sql | 75 - .../0022_default_remaining_phase_types.sql | 7 - .../0023_cheerful_night_thrasher.sql | 1 - .../migrations/0024_damp_mister_sinister.sql | 150 -- src/db/migrations/meta/0000_snapshot.json | 1586 +++++++++++- src/db/migrations/meta/0011_snapshot.json | 1450 ----------- src/db/migrations/meta/0012_snapshot.json | 1614 ------------ src/db/migrations/meta/0013_snapshot.json | 1694 ------------- src/db/migrations/meta/0014_snapshot.json | 1805 ------------- src/db/migrations/meta/0015_snapshot.json | 1822 -------------- src/db/migrations/meta/0016_snapshot.json | 1850 -------------- src/db/migrations/meta/0017_snapshot.json | 1877 -------------- src/db/migrations/meta/0018_snapshot.json | 1885 -------------- src/db/migrations/meta/0019_snapshot.json | 1885 -------------- src/db/migrations/meta/0020_snapshot.json | 1885 -------------- src/db/migrations/meta/0021_snapshot.json | 1885 -------------- src/db/migrations/meta/0022_snapshot.json | 1885 -------------- src/db/migrations/meta/0023_snapshot.json | 1893 -------------- src/db/migrations/meta/0024_snapshot.json | 2242 ----------------- src/db/migrations/meta/_journal.json | 172 +- ...ice-row-move-project-command.repository.ts | 8 - ...ow-structure-project-command.repository.ts | 5 - ...ircuit-device-row-structure.persistence.ts | 1 - .../circuit-device-row.persistence.ts | 2 - .../circuit-device-row.repository.ts | 1 - ...it-structure-project-command.repository.ts | 11 - src/db/repositories/circuit.persistence.ts | 16 - ...rd-structure-project-command.repository.ts | 12 +- .../legacy-consumer-migration.repository.ts | 142 -- ...ce-structure-project-command.repository.ts | 1 - ...on-structure-project-command.repository.ts | 9 +- ...ect-settings-project-command.repository.ts | 54 +- .../project-snapshot.repository.ts | 20 +- ...roject-state-restore-command.repository.ts | 137 - .../project-state-snapshot.persistence.ts | 4 - src/db/schema/circuit-device-rows.ts | 1 - src/db/schema/circuits.ts | 3 - src/db/schema/consumers.ts | 47 - .../legacy-consumer-circuit-migrations.ts | 19 - .../legacy-consumer-migration-report.ts | 24 - ...ice-row-structure-project-command.model.ts | 2 - src/domain/models/circuit-device-row.model.ts | 1 - .../models/circuit-project-command.model.ts | 7 - ...circuit-structure-project-command.model.ts | 9 - src/domain/models/circuit-tree.model.ts | 15 - src/domain/models/circuit.model.ts | 3 - ...istribution-board-project-command.model.ts | 27 +- ...n-board-structure-project-command.model.ts | 205 +- ...stribution-board-subtree-snapshot.model.ts | 1 - .../project-settings-project-command.model.ts | 92 +- .../models/project-state-snapshot.model.ts | 328 +-- src/domain/models/project-transfer.model.ts | 1 - .../ports/legacy-consumer-migration.store.ts | 143 -- .../legacy-consumer-migration-planner.ts | 60 - .../legacy-consumer-migration.service.ts | 269 -- .../services/project-voltage.service.ts | 28 - .../components/circuit-tree-editor.tsx | 25 +- .../components/circuit-tree-preview.tsx | 18 +- src/frontend/types.ts | 19 - src/frontend/utils/circuit-grid-model.ts | 53 +- src/frontend/utils/circuit-group-editing.ts | 6 - .../utils/circuit-structure-command.ts | 4 - .../controllers/circuit-tree.controller.ts | 4 - ...ow-move-project-command.repository.test.ts | 3 - ...ructure-project-command.repository.test.ts | 6 - tests/circuit-grid-model.test.ts | 49 +- tests/circuit-group-numbering.test.ts | 2 +- ...subtree-project-command.repository.test.ts | 1 - tests/circuit-group-subtree-snapshot.test.ts | 4 - tests/circuit-power-calculation.test.ts | 12 - ...circuit-project-command.repository.test.ts | 14 +- tests/circuit-structure-command.test.ts | 3 +- ...ructure-project-command.repository.test.ts | 16 - ...n-board-component-schema-migration.test.ts | 466 ---- ...ructure-project-command.repository.test.ts | 183 -- tests/distribution-board-subtree.test.ts | 5 - .../legacy-consumer-migration-planner.test.ts | 57 - ...gacy-consumer-migration.repository.test.ts | 245 -- .../legacy-consumer-migration.service.test.ts | 130 - tests/project-command.model.test.ts | 11 +- tests/project-command.service.test.ts | 7 - tests/project-device-schema-migration.test.ts | 98 - tests/project-device-schema.test.ts | 4 +- tests/project-device-sync.service.test.ts | 1 - ...ructure-project-command.repository.test.ts | 48 - ...ettings-project-command.repository.test.ts | 31 - tests/project-snapshot.repository.test.ts | 96 +- ...t-state-restore-command.repository.test.ts | 83 +- tests/project-state-snapshot.test.ts | 395 +-- 129 files changed, 2121 insertions(+), 30607 deletions(-) delete mode 100644 data/backups/leistungsbilanz-2026-05-03T19-47-12-976Z.db delete mode 100644 docs/circuit-list-editor-migration.md delete mode 100644 docs/local-db-circuit-first-migration.md delete mode 100644 scripts/db-backfill-sections.ts delete mode 100644 scripts/db-migrate-legacy-consumers.ts create mode 100644 scripts/run-tests.mjs delete mode 100644 src/db/migrations/0000_bizarre_colossus.sql create mode 100644 src/db/migrations/0000_whole_surge.sql delete mode 100644 src/db/migrations/0001_global_devices.sql delete mode 100644 src/db/migrations/0002_project_voltage_defaults.sql delete mode 100644 src/db/migrations/0003_project_floors_rooms.sql delete mode 100644 src/db/migrations/0004_circuit_lists_and_entry_fields.sql delete mode 100644 src/db/migrations/0005_project_devices.sql delete mode 100644 src/db/migrations/0006_device_display_name.sql delete mode 100644 src/db/migrations/0007_consumer_device_link.sql delete mode 100644 src/db/migrations/0008_circuit_first_model.sql delete mode 100644 src/db/migrations/0009_project_device_circuit_fields.sql delete mode 100644 src/db/migrations/0010_circuit_control_requirement.sql delete mode 100644 src/db/migrations/0011_project_device_canonical_fields.sql delete mode 100644 src/db/migrations/0012_project_revision_foundation.sql delete mode 100644 src/db/migrations/0013_project_history_stacks.sql delete mode 100644 src/db/migrations/0014_project_snapshots.sql delete mode 100644 src/db/migrations/0015_modern_madame_masque.sql delete mode 100644 src/db/migrations/0016_dashing_darkstar.sql delete mode 100644 src/db/migrations/0017_vengeful_romulus.sql delete mode 100644 src/db/migrations/0018_fancy_argent.sql delete mode 100644 src/db/migrations/0019_normalize_project_voltages.sql delete mode 100644 src/db/migrations/0020_normalize_phase_types.sql delete mode 100644 src/db/migrations/0021_fill_missing_phase_types.sql delete mode 100644 src/db/migrations/0022_default_remaining_phase_types.sql delete mode 100644 src/db/migrations/0023_cheerful_night_thrasher.sql delete mode 100644 src/db/migrations/0024_damp_mister_sinister.sql delete mode 100644 src/db/migrations/meta/0011_snapshot.json delete mode 100644 src/db/migrations/meta/0012_snapshot.json delete mode 100644 src/db/migrations/meta/0013_snapshot.json delete mode 100644 src/db/migrations/meta/0014_snapshot.json delete mode 100644 src/db/migrations/meta/0015_snapshot.json delete mode 100644 src/db/migrations/meta/0016_snapshot.json delete mode 100644 src/db/migrations/meta/0017_snapshot.json delete mode 100644 src/db/migrations/meta/0018_snapshot.json delete mode 100644 src/db/migrations/meta/0019_snapshot.json delete mode 100644 src/db/migrations/meta/0020_snapshot.json delete mode 100644 src/db/migrations/meta/0021_snapshot.json delete mode 100644 src/db/migrations/meta/0022_snapshot.json delete mode 100644 src/db/migrations/meta/0023_snapshot.json delete mode 100644 src/db/migrations/meta/0024_snapshot.json delete mode 100644 src/db/repositories/legacy-consumer-migration.repository.ts delete mode 100644 src/db/schema/consumers.ts delete mode 100644 src/db/schema/legacy-consumer-circuit-migrations.ts delete mode 100644 src/db/schema/legacy-consumer-migration-report.ts delete mode 100644 src/domain/ports/legacy-consumer-migration.store.ts delete mode 100644 src/domain/services/legacy-consumer-migration-planner.ts delete mode 100644 src/domain/services/legacy-consumer-migration.service.ts delete mode 100644 tests/distribution-board-component-schema-migration.test.ts delete mode 100644 tests/legacy-consumer-migration-planner.test.ts delete mode 100644 tests/legacy-consumer-migration.repository.test.ts delete mode 100644 tests/legacy-consumer-migration.service.test.ts delete mode 100644 tests/project-device-schema-migration.test.ts diff --git a/AGENTS.md b/AGENTS.md index 3470390..18896c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,17 +33,12 @@ It must support circuits, device rows, project devices, drag-and-drop restructur - General application repositories also require an explicit `AppDatabase`; `src/server/composition/application-repositories.ts` owns their runtime instances, and controllers never import the global SQLite client. -- The upgrade-only legacy migration service follows the same dependency rule; - its concrete repositories are instantiated only in - `scripts/db-migrate-legacy-consumers.ts`. - General Circuit, CircuitDeviceRow, CircuitList and DistributionBoard repositories expose only active reads. Runtime writes belong in typed command repositories; direct integration fixtures belong under `tests/support`. -The supported runtime model is Circuit-First. The former Consumer UI and API are -removed. Retained `consumers` rows, migration mappings and reports are upgrade-only -data accessed by `npm run db:migrate:legacy-consumers`; do not build application -features on them. +The supported runtime model is Circuit-First. The former Consumer UI, API, +tables and upgrade tooling are removed; do not reintroduce them. See `docs/current-architecture.md` for the complete module and request flow. @@ -255,9 +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 project data atomically and records a new `restore` revision with a complete inverse command. Restore can therefore be undone and redone after a restart. -Snapshot schema version 7 also contains circuit-group identity, +Baseline snapshot schema version 1 contains circuit-group identity, distribution-board components and the separate circuit/component protection -records. Versions 1 through 6 remain readable. Portable duplicate imports +records. Pre-baseline snapshots are unsupported. Portable duplicate imports remap component ids and protection-owner references together with the existing project graph. The central revision boundary creates an automatic logical snapshot after each @@ -294,13 +289,11 @@ change in one revision and Undo/Redo restores them together; the project PUT route requires `expectedRevision`. The system catalog is `AV`, `SV`, `EV`, `USV`, `MSR`, `SiBe`; at least one must be enabled and a type used by a board cannot be disabled. -Distribution-board setup uses `distribution-board.insert` schema version 3 +Distribution-board setup uses `distribution-board.insert` schema version 1 with a complete stable snapshot of the board, circuit list, three default groups, main switch `-Q0` and surge protective device `-FA`. Its inverse removes only the same unchanged and still-empty structure; the POST route requires `expectedRevision` and returns the updated history state. -Stored schema-version 1 and 2 setup commands remain executable with their four -legacy sections and without invented components. Complete populated distribution-board copying and deletion use `distribution-board.insert-subtree` and `distribution-board.delete-subtree`. Their exact snapshots include the board, circuit list, groups, circuits, @@ -389,7 +382,10 @@ Users must be able to override sizing suggestions. ## Persistence and Migration Rules - SQLite is the currently supported database. -- Never edit an already applied migration. +- Migration `0000` is the clean release baseline for empty databases. +- Pre-baseline development databases, snapshots and stored commands are not + supported. +- After the first release, never edit an already applied migration. - Back up an existing database before applying a new migration. - Inspect generated SQL; it must contain only the intended schema change. - Keep database backups separate from logical project snapshots. diff --git a/README.md b/README.md index 86e93df..1b60bcd 100644 --- a/README.md +++ b/README.md @@ -108,11 +108,9 @@ npm run db:generate ``` `db:backup` verwendet die SQLite-Online-Backup-API und prüft das Ergebnis auf -Integrität und Fremdschlüsselverletzungen. Vor jeder Migration einer bestehenden -Datenbank ist ein Backup erforderlich. - -`db:migrate:legacy-consumers` und `db:backfill:sections` sind ausschließlich -Upgrade-Werkzeuge für ältere Datenbanken. Neue Installationen benötigen sie nicht. +Integrität und Fremdschlüsselverletzungen. Die Migration `0000` ist die +Release-Baseline und richtet ausschließlich eine neue, leere Datenbank ein. +Datenbanken aus Entwicklungsständen vor dieser Baseline werden nicht unterstützt. ## Dokumentation diff --git a/data/backups/leistungsbilanz-2026-05-03T19-47-12-976Z.db b/data/backups/leistungsbilanz-2026-05-03T19-47-12-976Z.db deleted file mode 100644 index c803dbab44f737ea2121c0b34592b3350222cefc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90112 zcmeI5U5wjSc7VzGkw%&yImvDWBXG^eMQzn#&Ho=P0%u~&hU>}L9$QJ=Y{4P#y&Nmi zh+~p+;-o-3<4v&bOSc7zb{8=E5G)pb3W|NobFn}d=tHsSgNp*)b~jn{r7s1FKI}g9 zTxz5tJ(8<<$8l^N+9QdFm*?L5oy&874n!1M<@t>V1N*LHXu*8NdC)bh&=?sQ8`P)DJq0MBSvf$>L=AVdl(QbNO0Ry0-l4N>f@N z%Gae6)yjICu1hIJpqI9z zt`|gpOua!mes0&_WV`C~XAwVT8&Q0_e~2f%+4H+;I|WI*3&&VoC_=lNq<*KvsJ9ip z%VKW}{(9YZ>b>*sx)d|NQ%FtOrr)Jr=#w;xM;yNuby9zWdAIt)|wa2uVz=k ziF~_n6zsZtK}?9A_z3|%&~A$AM_)O`oc5=gFw;SMsy4ROXUcW|xVXKMxs^iO@%HP7 z7RRrwn>|)Y`RuL>X{}zk(rQW5Tfy&l-TQ4;21ia7s$o$tE?x9lswd3O) zW1(Q4TG)N&PQ6yHA3rXBxY>7xxr-?&q&#$=Q_LrP#E!AxtoAOAytvp@avr$YKAwP< zxZC`zjmA=`{&IbsQMng|J}!H!?Ierkdc7`w*vyQW3A2}_qR}$hK^QCSMBu*i9T!*a z*iNCO?IJr=&pFTX!Kz*H_+J9f%sm+(`hYyQ)Zf7RyOZ%@2 zMI-fv2Vterz}J%~PNDaqyfeFh@Bsbmx#iZ%HA&g$e-HMesy4n+E!8hI#^KxOM1kMg z8_X5==XX{s<$9wbe(Q}4XYpupuUf=gJUAidF(jXwVOh0tYOYj2b7Y(~27Yrms7@-| z-z=BwM~;Yh<}(WnQVibbiqr? zG@7MS{klGm$Nqj8eDMfjd1qm^T-SB+&gqQOh$JIKr6Ri#DJI0YOMU353qE;tn|_oX zX|X-3JW3dVSGRFW6@yb!iFm993|E-bI)r-tB1VS`D)jsC1LH?0FXfuTAd zy2C6VI!_p$6PTcWy|T#Xs*8Uu)P7hCYloNqap{kiw8hUsfEyA(0!RP}AOR$R1dsp{ zKmter34AF8F3!yg+l#Z7ZCJLV7@@CFO9@q5(fLkyy7 zL{UTHGdFa!KojqN{;S^-E|xQ8lu)V@;*-#^9ozC%-LP%TB+PbA-EVyF%z)Wy61{;xm#{=@Euh#G}SUG(`g9GN@s+NQ<*ZVX;gJ}uoI@lsGmdK z(4-33Dhzd32bDUp0}Tpi6312y-O&TXQJLWw4gmxF^!N{Fs?eI}YX2hCepUP5+JD#n ztM;F@pV$5k1h^pqB!C2v01`j~NB{{S0VIF~kN^@u0$+UsGt+`N$6w0)rNm!m`O6G{ zndUE3)6+s_v3gIKsr*Q&NlVXEezbV1{5So7U;PSV36TI2Kmter2|Tp~b`H%QX|-DJ zu}ZljR4Rhd65PGYwklM&RNJ9MR^3384c#?mmpFm!!#=YVqA-YAs1PK9kcsTN0hJ9U zbmhSEO&Ov|#1AdsQhnc4sPDKMm6_s(vcZ@pLzu;sA@pE}Duk4DM|qWXSdZLDd)*DG zDX+A%)ivb+LP)Y7@Ia3nn7XPwbx2ANL_#3s23Sm4QC%WK2*{QlFtqGL>tHr< zb>E;*84^L5dgDPCj$%_60&9+}=+ux6S688>Q7S{=&7#ayL&No-J|w^WAS4DvKn;cJ zvJQjbf=g{;6FC5jehyt-yIZ$i|9E!5-WzNSI_WZHE z=Z}RsU57Zl=4-M8O$z)dG%(k-137dZswxnr_w56_(w5_ZwbNv)20?EKzso% znlT2~8C4C#v7x`!!Hf8oCPUC(i zL9N#&SwG~@y<3}PRc(f6J;~BhR>nzIV5W{ww)!I~UhBN4sBcVh#Xd7!LR4M0;LH88VFj-2z=H^69kjZJ@593qCb}!2 zueb?{wKqhrc#<3ICND8ptn%;wPuD&YvgrRuPmhVQCP)AYAOR$R1dsp{Kmter2_OL^ zfCRpJ1hShMCRmP`<_RZRUdZDAlPnkT`2SSx2SV+i;RQD&fCP{L5h?3qr+LGOw0 zFy@Q|kN^@u0!RP}AOR$R1dsp{KmthMVF~c(|4*`1z~leIp@+={&qD%800|%gB!C2v z01`j~NB{{S0VIF~o&*9s{*UwjC&5x!MkIg)kN^@u0!RP}AOR$R1dsp{Kmrd>0OS7; z&koN-0!RP}AOR$R1dsp{Kmter2_OL^@T3sH`2Uk)DJ&-vKmter2_OL^fCP{L5G zX(uV$UYN>?<1@o0MAc=B*@0|WfeXdabXkY8xQ6c&oB5LzXA(XA;xwT++Wz8*S#eB- z`i`qnnJI1vbeJXwfo94MrFN(iMb#Z;lHyFFXRkQ>=9`7#X*OE#z)$N^(S_8rr5bixeHF(>&Vz^kmo zdgMmh>u%&ep8oW^eEXj8i=0qu$}8*kN=CcdvO2%fmIL{Aps@t>V1N*&3+)v@j86_h3Rtr=uz=Iv#B3+7>T+`ZIxE=J; zcGUHP$d9QvNXO6Z`kQQ5ef})sr)(pNZ}$)Jgg1MBH*KdNX?Nimiwi|)cazlbbQtxv zqIX&BZNXo!+fKcA-d&es=64FIDckhB)C+x*M)8Q_x1vt!Z!qsx|5#SX8-Bt(67{<2 zumqVr%Py}et5uZl*6Jl`J&vNyVV$M5=DFrtbM;K~YX3;Geu5KjNS9Wn*5$L@4BbeA zk^wI+JKJ1of-GUF)a`XTppUd;(u1n;$BK;Bn@{oZ`n$c&fB_zEd&riugv9MFUap*s zH$5-Cy|q`}t&CJbl*T?~UK9gADDp-e^|pp8vORCF89+|Zo>FMoyulRI2?2HW}td_ythZ+82>HN)j*WXSB=Ps=^FPvY^ zu7DHycHb!2b@zgp5Iyk|0(_v|6xENua*8?aPcvbrgZ5Ny2*T{2*Zt$-_D1Gb3T?;R zuOC_*zp`%jSRv)JyDp@)df`f|DP34S+kCUwRFelPTjy=P(6kfqKIt3L*2* zX1QEHazwl{pP6NlV(>m!G{r#t@J8U}PTJiK7_an)RJl><{X^j4HKFMWoR;2**%vv> zP?ScqRH|Rs$MM+T55s(8gs{A`Fk7zcx_IYwMrlNn5u#F&-G~$uV%(*^=biF%iqQ?d zIOfVJC=RrS_MB+6zB{C=Hq@C?y?JyT)!{_TQ->dC&rg-=Z<*sx@jM#0-+vz(#b$d0 zM!_&h21|^RPH#8Fa@{n=?OtYzQE5iWrV7S*aa58Q`MeOeHkGb6*DfrtWT_F=qmh6Bw%Vp}WtgGcXBl6P~=WIQOSQwG;}qzhC;hrEe`RE&P1`!u(S8_er>u=!!8Y<`tdHNn>N&Uy4`vQuZEUO0l%Lw=%@oG| zxz)}zdFD>NR<0jEE`GR~HQ&O)!p@FYeUl%k{&D#XGk% z6ZNJ2*M*{y`oe>-(wOl=62&P5*C>zGja?Xe)QhFn#y6^^`lZG=e8r_nPc81x{~r&E BgF^rS diff --git a/docs/README.md b/docs/README.md index 652a097..fbbd4bd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,9 +20,6 @@ ausdrücklich getrennt und dürfen nicht als bereits implementiert verstanden we - [Entwicklungs- und Contributor-Workflow](development-workflow.md) - [Deployment und Betrieb](deployment.md) -- [Lokale Circuit-First-Datenbankmigration](local-db-circuit-first-migration.md) -- [Retained Legacy-Datenmigration](circuit-list-editor-migration.md) – - ausschließlich für Upgrades alter Datenbanken ## Zukunftsarchitektur diff --git a/docs/circuit-list-editor-api.md b/docs/circuit-list-editor-api.md index ff92fbf..7e3d2d3 100644 --- a/docs/circuit-list-editor-api.md +++ b/docs/circuit-list-editor-api.md @@ -225,8 +225,6 @@ returns HTTP `409` with `PROJECT_HISTORY_OPERATION_UNAVAILABLE`. `{ "distributionBoard": { ... }, "revision": { ... }, "history": { ... } }` - persistent Undo removes only the unchanged and still-empty generated structure; Redo restores the same ids - - stored command schema versions 1 and 2 remain executable with their four - legacy sections and without generated components - stale revisions return `409 PROJECT_REVISION_CONFLICT` - `PUT /projects/:projectId/distribution-boards/:distributionBoardId` - body: @@ -358,12 +356,6 @@ available only as the corresponding versioned commands through structure POST, move, reorder, renumber, identifier-restore, Circuit DELETE or CircuitDeviceRow DELETE routes. -## Removed Legacy Endpoints - -The former `/consumers` read/write endpoints were removed after every retained -consumer had a verified Circuit-First migration mapping. Database upgrade tooling -reads retained legacy rows directly; application features must use the Circuit-First -endpoints above. ## Linked Project Device Review diff --git a/docs/circuit-list-editor-architecture.md b/docs/circuit-list-editor-architecture.md index 2a761df..c58589b 100644 --- a/docs/circuit-list-editor-architecture.md +++ b/docs/circuit-list-editor-architecture.md @@ -49,11 +49,6 @@ The pure grid modules have no React state and are covered by focused unit tests. - Represents fixed header components, optional group protection and manually named auxiliary footer devices with their own unique BMK. - Group protection may own a separate one-to-one protection configuration. -- Retained legacy migration source - - Old `consumers` rows, mappings and reports are available only to explicit - database upgrade tooling. - - They are not application-domain entities and have no UI or API. - ## Why A Circuit Is Not One Row A circuit can contain zero, one, or many device rows. Treating a circuit as a single row breaks: diff --git a/docs/circuit-list-editor-known-limitations.md b/docs/circuit-list-editor-known-limitations.md index d0f3ab5..85fd206 100644 --- a/docs/circuit-list-editor-known-limitations.md +++ b/docs/circuit-list-editor-known-limitations.md @@ -23,4 +23,3 @@ - Protection and cable values are planner-owned manual selections. The later sizing/warning engine does not yet validate them against load, length or voltage-drop rules. -- Legacy consumer UI and server endpoints are removed; retained source rows and migration mappings remain available only for upgrade traceability. diff --git a/docs/circuit-list-editor-migration.md b/docs/circuit-list-editor-migration.md deleted file mode 100644 index 1f6364c..0000000 --- a/docs/circuit-list-editor-migration.md +++ /dev/null @@ -1,90 +0,0 @@ -# Circuit List Editor Migration - -## Status - -Upgrade-only. New installations already use the Circuit-First schema and do not -run this workflow. - -## Goal - -Migrate legacy row-first consumers into the circuit-first model without deleting legacy data. - -## Legacy Mapping - -Legacy `Consumer` rows map into: - -- `Circuit` for shared circuit identity and circuit-level technical fields -- `CircuitDeviceRow` for per-device load rows - -Multiple legacy consumers can map into one circuit when they share normalized circuit identity. - -For one circuit list, all new circuits, device rows, trace mappings and the migration report are committed in one SQLite transaction. A failed run therefore leaves none of those prepared migration writes behind. - -## Grouping Strategy (`circuitNumber`) - -Migration groups legacy rows by normalized `circuitNumber`: - -- valid/normalizable values become one target circuit per normalized value -- duplicates are grouped under that circuit (multiple `CircuitDeviceRow`s) -- missing/invalid values trigger generated identifiers and may fall back to `unassigned` section - -## Default Section Backfill - -Before migration, default sections are created/backfilled per circuit list. -This guarantees a valid target section space, including `unassigned` when no section can be inferred. - -## Migration Commands - -Run in this order for local database workflows: - -1. Backup: - - `npm run db:backup` -2. Migrate schema: - - `npm run db:migrate` -3. Verify circuit schema: - - `npm run db:verify:circuit-schema` -4. Backfill missing sections: - - `npm run db:backfill:sections` -5. 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. The legacy multi-list UI was removed only after this check passed; -old `/projects/:projectId/circuit-lists` bookmarks redirect to the project page. - -## Validation Checks - -After migration, verify: - -- tree endpoint returns sections/circuits/rows -- grouped duplicate circuit numbers are reported -- generated identifiers are reported where expected -- migrated rows include `legacyConsumerId` traceability -- no duplicate BMKs exist inside one circuit list - -## If Tree Endpoint Returns Empty Sections - -Likely causes: - -- circuit-first tables missing (migration not run) -- sections not backfilled yet -- migrated dataset genuinely empty for selected list - -Actions: - -1. Run schema migration and verification commands. -2. Run section backfill command. -3. Run legacy-consumer migration command. -4. Retry tree endpoint. - -## Legacy Data Retention - -Do not delete legacy consumers yet. - -- legacy read/write endpoints have been removed from the application server -- migration trace tables reference old/new mapping -- retained rows allow upgrade verification and audit of the completed mapping -- a future automatic cleanup migration must migrate and verify old databases - before dropping these source and trace tables diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 9b17158..076178f 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -9,10 +9,8 @@ Der unterstützte Editor ist Circuit-First: Ein Stromkreis ist nicht dasselbe wie eine Gerätezeile. BMK, Schutz- und Kabeldaten gehören zum Stromkreis; Last-, Raum- und Kategoriedaten gehören zur Gerätezeile. -Die frühere Consumer-Oberfläche und ihre API sind entfernt. Die Tabelle -`consumers` sowie Mappings und Reports bleiben ausschließlich erhalten, damit -ältere Datenbanken über den expliziten Upgrade-Befehl migriert und geprüft werden -können. +Die frühere Consumer-Oberfläche, ihre API, Tabellen und Upgrade-Werkzeuge sind +entfernt. Neue Funktionen bauen ausschließlich auf dem Circuit-First-Modell auf. ## Laufzeit @@ -130,8 +128,8 @@ Datenbank-Backups gespeichert. `POST /api/projects/:projectId/snapshots` erzeugt bei passender erwarteter Revision transaktional einen vollständigen, schema-versionierten Projektzustand mit SHA-256-Prüfwert. Enthalten sind Projekteinstellungen, Verteiler, Stromkreislisten, Bereiche, Stromkreise und -Gerätezeilen sowie Projektgeräte, Geschosse und Räume. Globale Geräte, -Legacy-Consumer und Migrationsberichte sind nicht Teil des Projekt-Snapshots. +Gerätezeilen sowie Projektgeräte, Geschosse und Räume. Globale Geräte sind nicht +Teil des Projekt-Snapshots. Create/List verändern weder Projektrevision noch Undo-/Redo-Stapel. `kind` unterscheidet benannte und automatische Stände. Die zentrale Revisionspersistenz erzeugt nach jeweils 25 weiteren Projektänderungen @@ -144,9 +142,7 @@ Prüfsumme, erwartete Revision und den unmittelbar zuvor gelesenen Projektzustand. Der Restore ersetzt alle unterstützten Projektdaten in einer Transaktion und schreibt dabei eine neue Revision mit Quelle `restore` sowie ein vollständiges inverses Kommando. Undo und Redo können deshalb auch einen -Restore nach einem Neustart exakt zurücknehmen oder wiederholen. Kompatible -Upgrade-only-Verknüpfungen und Migrationsnachweise bleiben erhalten, obwohl sie -nicht Bestandteil des logischen Snapshots sind. +Restore nach einem Neustart exakt zurücknehmen oder wiederholen. Die Projektseite bindet diese APIs in einem einklappbaren Bereich „Versionen und Sicherungspunkte“ ein. Dort können Benutzer Sicherungspunkte benennen, jeden aufgeführten benannten oder automatischen Stand nach @@ -246,8 +242,7 @@ Store leitet das inverse Kommando aus dem gespeicherten Projekt ab und schreibt Werte, Revision und Historienstapel gemeinsam. `PUT /api/projects/:projectId` verlangt deshalb `expectedRevision`, liefert Projekt plus aktualisierten Historienstand und besitzt keinen separaten direkten Settings-Schreibweg mehr. -Kommando- und Snapshot-Versionen vor dieser Erweiterung bleiben les- und -ausführbar; fehlende Metadaten werden dabei als `null` behandelt. +Kommando- und Snapshot-Payloads müssen dem aktuellen Baseline-Schema entsprechen. Die Projektseite zeigt Verteilungen, Etagen und Räume als kompakte Bestandsübersichten; ihre versionierten Erstellwege öffnen beschriftete Modals statt dauerhafter Eingabezeilen. Projektgeräte werden als durchsuchbare, @@ -264,8 +259,8 @@ rückgängig machbare Projektrevision. Der Modus `duplicate` ordnet Projekt-, Struktur-, Gruppen-, Verteilerkomponenten-, Raum-, Stromkreis- und Gerätezeilen-UUIDs sowie Schutzgeräte-Referenzen vollständig neu zu und legt die Kopie mit Revision `0` in einer Transaktion an. Upgrade-only- -Consumer-Verweise werden nicht in die Kopie übernommen; fachliche Verknüpfungen -innerhalb des unterstützten Laufzeitmodells bleiben erhalten. Die +Fachliche Verknüpfungen innerhalb des unterstützten Laufzeitmodells bleiben +erhalten. Die Projektübersicht verwendet dafür den separaten Collection-Endpunkt `POST /api/projects/import`, der ausschließlich eine neue Kopie anlegt und deshalb weder eine bestehende Projekt-ID noch `expectedRevision` annimmt. Das @@ -283,8 +278,6 @@ Controller-Schreibweg ist entfernt. Verteilungen besitzen eine optionale Etagenreferenz sowie eine Netzart aus dem Projektkatalog. Anlage und nachträgliche Bearbeitung prüfen die Projektzugehörigkeit der Etage und die Freigabe der Netzart in den Projekteinstellungen. -Gespeicherte Anlage-Commands der Schemas 1 und 2 bleiben mit ihren vier -Legacy-Abschnitten und ohne nachträglich erfundene Komponenten ausführbar. Bereits befüllte Verteilungen werden über `distribution-board.insert-subtree` und `distribution-board.delete-subtree` als vollständiger Unterbaum kopiert beziehungsweise gelöscht. Der Snapshot @@ -462,32 +455,30 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät. - SQLite ist die aktuell unterstützte Datenbank. - Fremdschlüssel werden für jeden Datenbankkontext aktiviert. -- `npm run db:migrate` wendet Drizzle-Migrationen an. +- `npm run db:migrate` richtet mit der einzelnen Migration `0000` das komplette + aktuelle Schema einer leeren Datenbank ein. - `npm run db:verify:circuit-schema` prüft erforderliche und entfernte Spalten. - `npm run db:backup` erzeugt ein konsistentes und verifiziertes Online-Backup. -- `npm run typecheck:scripts` prüft alle TypeScript-Wartungs- und - Upgrade-Skripte mit ihren Anwendungspfaden, ohne Code zu erzeugen. -- Angewendete Migrationen werden niemals nachträglich verändert. -- `db:migrate:legacy-consumers` ist Upgrade-Werkzeug, kein Anwendungspfad. - Der fachliche Migrationsdienst kennt nur schmale Reader-/Store-Ports unter - `src/domain/ports`; konkrete SQLite-Repositories werden ausschließlich im - CLI-Skript zusammengesetzt. +- `npm run typecheck:scripts` prüft die TypeScript-Wartungsskripte, ohne Code zu + erzeugen. +- Die Baseline ersetzt alle vorigen Entwicklungsmigrationen. Datenbanken, + Snapshots und persistierte Commands aus Vor-Baseline-Ständen werden bewusst + nicht unterstützt. Ab der ersten veröffentlichten Version sind angewendete + Migrationen unveränderlich und Änderungen erfolgen additiv. - Allgemeine Circuit-, Gerätezeilen-, CircuitList- und DistributionBoard- Repositories stellen im Anwendungspfad nur noch benötigte Leseabfragen bereit. - Fachliche Schreibvorgänge liegen in den typisierten Command-Repositories; - Upgrade-Schreibvorgänge bleiben in expliziten Migrationsadaptern. + Fachliche Schreibvorgänge liegen in den typisierten Command-Repositories. - Die vollständige Verteilungs-Testfixture liegt unter `tests/support/distribution-board-fixture.ts` und ist kein exportierter Produktions-Schreibweg. -- Migration `0024` bildet die additive relationale Grundlage für geschützte - Stromkreisgruppen und Verteilerkomponenten. Die drei bestehenden - Stromkreisabschnitte erhalten Kategorie und Gruppennummer 1. Separate - 1:1-Tabellen halten künftig Stromkreis- und Komponenten-Schutzgeräte; die - bisherigen flachen Schutzfelder bleiben in dieser Übergangsphase unverändert. +- Die Baseline bildet die relationale Grundlage für geschützte Stromkreisgruppen + und Verteilerkomponenten. Separate + 1:1-Tabellen halten Stromkreis- und Komponenten-Schutzgeräte. Die früheren + flachen Stromkreis-Schutzfelder sind aus der Baseline entfernt. Ein triggergeführtes Register erzwingt bereits eine normalisierte, stromkreislistenweite BMK-Eindeutigkeit über Stromkreise und Verteilerkomponenten. Snapshot- und Transfer-Integration verwenden - Snapshot-Schema 7. Persistente Insert/Delete/Update-Commands für + Snapshot-Schema 1 der Baseline. Persistente Insert/Delete/Update-Commands für veränderliche Verteilerkomponenten, Gruppen einschließlich befüllter Unterbäume sowie vollständige Gruppensortierung sind integriert. Der Editor zeigt die geschützte Struktur an und bearbeitet veränderliche Gruppen- und diff --git a/docs/development-workflow.md b/docs/development-workflow.md index b4447f0..b19b7ff 100644 --- a/docs/development-workflow.md +++ b/docs/development-workflow.md @@ -96,18 +96,19 @@ erneut direkte Schreibmethoden erhalten. ## Migrationen -1. Bestehende lokale Datenbank sichern: `npm run db:backup`. -2. Schema unter `src/db/schema/` ändern. -3. `npm run db:generate` ausführen. -4. Das generierte SQL vollständig prüfen; es darf nur die beabsichtigte Änderung +1. Schema unter `src/db/schema/` ändern. +2. `npm run db:generate` ausführen. +3. Das generierte SQL vollständig prüfen; es darf nur die beabsichtigte Änderung enthalten. -5. Upgrade-/Erhaltungstest ergänzen, wenn Spalten oder Beziehungen geändert +4. Upgrade-/Erhaltungstest ergänzen, wenn Spalten oder Beziehungen geändert werden. -6. `npm test`, `npm run db:migrate` und +5. `npm test`, `npm run db:migrate` und `npm run db:verify:circuit-schema` ausführen. -Bereits angewendete SQL-Migrationen dürfen nicht geändert oder neu sortiert -werden. Korrekturen erfolgen immer über eine neue Migration. +`0000` ist die saubere Baseline für leere Neuinstallationen. Vor-Baseline- +Entwicklungsdatenbanken werden nicht aktualisiert. Nach Veröffentlichung der +ersten Version dürfen angewendete SQL-Migrationen nicht geändert oder neu +sortiert werden; Korrekturen erfolgen dann immer über eine neue Migration. ## Pull-Request-Handoff diff --git a/docs/local-db-circuit-first-migration.md b/docs/local-db-circuit-first-migration.md deleted file mode 100644 index 995f6bf..0000000 --- a/docs/local-db-circuit-first-migration.md +++ /dev/null @@ -1,137 +0,0 @@ -# Local DB Migration: Circuit-First Schema - -This project uses SQLite at `data/leistungsbilanz.db` and Drizzle migrations in `src/db/migrations`. - -This document is an upgrade runbook for databases created before the Circuit-First -cutover. A clean installation only needs the normal `db:migrate` and schema -verification steps documented in the main README. - -## Safe command order - -1. Backup local DB (required before schema/data migration) - -```bash -npm run db:backup -``` - -The command uses SQLite's online backup API, so committed WAL changes are included -even while the application is running. It opens the resulting standalone database -and requires both `PRAGMA integrity_check` and `PRAGMA foreign_key_check` to pass. -Database backups are operational recovery files and remain separate from the future -user-visible project version history. - -2. Apply pending schema migrations (includes `0008_circuit_first_model`, the additive - `0009_project_device_circuit_fields` transition and the post-cutover - `0011_project_device_canonical_fields` cleanup) - -```bash -npm run db:migrate -``` - -3. Verify circuit-first tables exist - -```bash -npm run db:verify:circuit-schema -``` - -4. Backfill default sections for existing `circuit_lists` - -```bash -npm run db:backfill:sections -``` - -5. Run legacy consumer -> circuit/device-row migration explicitly - -```bash -npm run db:migrate:legacy-consumers -``` - -## Verification SQL - -Run these against `data/leistungsbilanz.db`: - -```sql -SELECT name -FROM sqlite_master -WHERE type = 'table' - AND name IN ( - 'circuit_sections', - 'circuits', - 'circuit_device_rows', - 'legacy_consumer_circuit_migrations', - 'legacy_consumer_migration_reports' - ) -ORDER BY name; -``` - -The verification command also checks the circuit-first project-device columns added by migration `0009`. - -```sql -SELECT circuit_list_id, key, prefix, sort_order -FROM circuit_sections -ORDER BY circuit_list_id, sort_order; -``` - -```sql -SELECT circuit_list_id, COUNT(*) AS circuits -FROM circuits -GROUP BY circuit_list_id; -``` - -```sql -SELECT c.circuit_list_id, COUNT(r.id) AS device_rows -FROM circuits c -LEFT JOIN circuit_device_rows r ON r.circuit_id = c.id -GROUP BY c.circuit_list_id; -``` - -## API verification - -After the steps above: - -`GET /api/projects/:projectId/circuit-lists/:circuitListId/tree` - -Expected response shape: - -```json -{ - "circuitListId": "string", - "sections": [ - { - "id": "string", - "key": "lighting|single_phase|three_phase|unassigned|...", - "displayName": "string", - "prefix": "-1F|-2F|-3F|-UF|...", - "sortOrder": 10, - "circuits": [ - { - "id": "string", - "equipmentIdentifier": "-2F1", - "displayName": "string", - "sortOrder": 10, - "isReserve": false, - "circuitTotalPower": 1.23, - "deviceRows": [ - { - "id": "string", - "displayName": "string", - "quantity": 1, - "powerPerUnit": 0.3, - "simultaneityFactor": 1, - "rowTotalPower": 0.3 - } - ] - } - ] - } - ] -} -``` - -If migrations were not applied, endpoint may return an empty fallback with a warning. - -## Visual verification - -Manual device rows and multi-device circuits can be created directly in the -circuit-list editor. The former development scripts for direct database writes -were removed because they bypassed project revisions and persistent undo/redo. diff --git a/docs/project-history-and-external-model-architecture.md b/docs/project-history-and-external-model-architecture.md index dffecfc..ae03868 100644 --- a/docs/project-history-and-external-model-architecture.md +++ b/docs/project-history-and-external-model-architecture.md @@ -93,8 +93,6 @@ Implemented named-snapshot foundation: - restore records a new `restore` revision containing both the selected target state and the complete pre-restore inverse; undo/redo therefore remains available after application restarts -- compatible upgrade-only Consumer links, mappings and reports are preserved - separately and are never imported into the logical snapshot payload - arbitrary restore payloads are rejected by the generic user-command endpoint; only server-stored snapshots can initiate a forward restore - the project page provides an initially collapsed snapshot/timeline panel, diff --git a/docs/spec/07-implementation-phases-todo.md b/docs/spec/07-implementation-phases-todo.md index 03b6a4c..16124c0 100644 --- a/docs/spec/07-implementation-phases-todo.md +++ b/docs/spec/07-implementation-phases-todo.md @@ -342,9 +342,8 @@ Implemented foundation: - circuit-device-row transaction tests cover both successful commits and forced SQLite rollbacks - project-device row synchronization has real multi-row SQLite commit and rollback coverage - BMK swaps and section reorder have real SQLite commit and rollback coverage -- the legacy consumer migration service depends only on narrow domain ports; - all concrete repositories are composed at the script entry point -- legacy circuit, row, mapping and report writes have real late-failure rollback coverage +- the former Consumer migration service, mappings and reports were removed with + the pre-release database cutover - circuit and device-row persistence value mapping is separated from the general repositories - obsolete direct Circuit, CircuitDeviceRow, CircuitList, DistributionBoard and Project repository writes are removed; integration fixtures live under @@ -362,8 +361,9 @@ Implemented foundation: direct global SQLite access - TypeScript maintenance and upgrade scripts have a dedicated no-emit typecheck, including all referenced application modules -- the legacy consumer UI and application read/write endpoints are removed after verified data cutover -- retained legacy rows are accessible only through explicit database upgrade tooling +- the former Consumer UI, API, tables and upgrade tooling are removed +- migration `0000` is the clean baseline for new installations; pre-baseline + development databases are intentionally unsupported - project devices no longer persist duplicate legacy power, phase, cosPhi or remark fields ## Phase 12: Project Revisions and Persistent Undo / Redo @@ -618,7 +618,8 @@ Implemented handoff: - README is a concise setup and project entry point without prototype status lists - `docs/README.md` separates current, operational, future, specification and archived material -- current runtime, module boundaries and upgrade-only legacy data are documented explicitly +- current runtime and module boundaries are documented without unsupported + upgrade-only data paths - contributor workflow covers clean databases, safe demo data, migrations, tests and commits - development Compose is clearly separated from the currently unsupported production deployment - `AGENTS.md` points LLM-assisted work to supported code paths and deferred architecture diff --git a/package.json b/package.json index 4216834..7f61d4d 100644 --- a/package.json +++ b/package.json @@ -15,14 +15,12 @@ "build:web": "next build", "typecheck:scripts": "tsc --noEmit -p tsconfig.scripts.json", "start": "node dist/server/index.js", - "test": "tsx --test tests/protection-device.test.ts tests/distribution-board-component.test.ts tests/distribution-board-component-schema-migration.test.ts tests/circuit-group-numbering.test.ts tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.service.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board-structure-project-command.repository.test.ts tests/project-location-structure-project-command.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.persistence.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-version-history.test.ts tests/project-overview-import.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts", - "test:watch": "tsx --watch --test tests/protection-device.test.ts tests/distribution-board-component.test.ts tests/distribution-board-component-schema-migration.test.ts tests/circuit-group-numbering.test.ts tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.service.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board-structure-project-command.repository.test.ts tests/project-location-structure-project-command.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.persistence.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-version-history.test.ts tests/project-overview-import.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts", + "test": "node scripts/run-tests.mjs", + "test:watch": "node scripts/run-tests.mjs --watch", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:backup": "tsx scripts/db-backup.ts", - "db:verify:circuit-schema": "node scripts/db-verify-circuit-schema.js", - "db:backfill:sections": "tsx scripts/db-backfill-sections.ts", - "db:migrate:legacy-consumers": "tsx scripts/db-migrate-legacy-consumers.ts" + "db:verify:circuit-schema": "node scripts/db-verify-circuit-schema.js" }, "keywords": [], "author": "", diff --git a/scripts/db-backfill-sections.ts b/scripts/db-backfill-sections.ts deleted file mode 100644 index 08deb06..0000000 --- a/scripts/db-backfill-sections.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { db } from "../src/db/client.js"; -import { CircuitListRepository } from "../src/db/repositories/circuit-list.repository.js"; -import { CircuitSectionRepository } from "../src/db/repositories/circuit-section.repository.js"; -import { ProjectRepository } from "../src/db/repositories/project.repository.js"; - -const projectRepository = new ProjectRepository(db); -const circuitListRepository = new CircuitListRepository(db); -const circuitSectionRepository = new CircuitSectionRepository(db); - -async function run() { - const projects = await projectRepository.list(); - let totalLists = 0; - for (const project of projects) { - const lists = await circuitListRepository.listByProject(project.id); - for (const list of lists) { - await circuitSectionRepository.createDefaults(list.id); - totalLists += 1; - } - } - console.log(`Section backfill done for ${totalLists} circuit list(s).`); -} - -run().catch((error) => { - console.error("Section backfill failed:", error); - process.exit(1); -}); diff --git a/scripts/db-migrate-legacy-consumers.ts b/scripts/db-migrate-legacy-consumers.ts deleted file mode 100644 index 7da8d70..0000000 --- a/scripts/db-migrate-legacy-consumers.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { db } from "../src/db/client.js"; -import { CircuitRepository } from "../src/db/repositories/circuit.repository.js"; -import { CircuitListRepository } from "../src/db/repositories/circuit-list.repository.js"; -import { CircuitSectionRepository } from "../src/db/repositories/circuit-section.repository.js"; -import { LegacyConsumerMigrationRepository } from "../src/db/repositories/legacy-consumer-migration.repository.js"; -import { ProjectRepository } from "../src/db/repositories/project.repository.js"; -import { RoomRepository } from "../src/db/repositories/room.repository.js"; -import { LegacyConsumerMigrationService } from "../src/domain/services/legacy-consumer-migration.service.js"; - -const projectRepository = new ProjectRepository(db); -const circuitListRepository = new CircuitListRepository(db); -const migrationRepository = new LegacyConsumerMigrationRepository(db); -const migrationService = new LegacyConsumerMigrationService({ - circuitListReader: circuitListRepository, - sectionStore: new CircuitSectionRepository(db), - circuitReader: new CircuitRepository(db), - roomReader: new RoomRepository(db), - migrationStore: migrationRepository, -}); - -async function run() { - const projects = await projectRepository.list(); - const reports = []; - - for (const project of projects) { - const lists = await circuitListRepository.listByProject(project.id); - for (const list of lists) { - const report = await migrationService.migrateCircuitList(project.id, list.id); - reports.push({ - projectId: project.id, - circuitListId: list.id, - legacyConsumerCount: report.legacyConsumerCount, - createdCircuitCount: report.createdCircuitCount, - createdDeviceRowCount: report.createdDeviceRowCount, - generatedIdentifiers: report.generatedIdentifiers.length, - unassignedRows: report.unassignedRows.length, - }); - } - } - - 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.table(reports); - console.log("Cutover verification passed: all legacy consumers are mapped."); -} - -run().catch((error) => { - console.error("Legacy consumer migration failed:", error); - process.exit(1); -}); diff --git a/scripts/db-verify-circuit-schema.js b/scripts/db-verify-circuit-schema.js index 0fff995..6d291f0 100644 --- a/scripts/db-verify-circuit-schema.js +++ b/scripts/db-verify-circuit-schema.js @@ -12,8 +12,6 @@ const requiredTables = [ "circuit_protection_devices", "distribution_board_components", "distribution_board_component_protection_devices", - "legacy_consumer_circuit_migrations", - "legacy_consumer_migration_reports", ]; const tablePlaceholders = requiredTables.map(() => "?").join(", "); @@ -57,6 +55,14 @@ const circuitColumns = new Set( db.prepare("PRAGMA table_info(circuits)").all().map((column) => column.name) ); const missingCircuitColumns = requiredCircuitColumns.filter((name) => !circuitColumns.has(name)); +const removedCircuitColumns = [ + "protection_type", + "protection_rated_current", + "protection_characteristic", +]; +const remainingRemovedCircuitColumns = removedCircuitColumns.filter((name) => + circuitColumns.has(name) +); const requiredCircuitSectionColumns = ["category", "group_number"]; const circuitSectionColumns = new Set( db @@ -164,6 +170,14 @@ if (missingCircuitColumns.length > 0) { process.exit(1); } +if (remainingRemovedCircuitColumns.length > 0) { + console.error( + "Transitional circuit protection columns still present:", + remainingRemovedCircuitColumns.join(", ") + ); + process.exit(1); +} + if (missingCircuitSectionColumns.length > 0) { console.error( "Missing circuit-section columns:", diff --git a/scripts/run-tests.mjs b/scripts/run-tests.mjs new file mode 100644 index 0000000..ab48b36 --- /dev/null +++ b/scripts/run-tests.mjs @@ -0,0 +1,30 @@ +import { readdirSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import path from "node:path"; + +const testFiles = readdirSync(path.resolve("tests"), { + withFileTypes: true, +}) + .filter((entry) => entry.isFile() && entry.name.endsWith(".test.ts")) + .map((entry) => path.join("tests", entry.name)) + .sort(); + +if (testFiles.length === 0) { + throw new Error("No TypeScript test files found."); +} + +const watch = process.argv.includes("--watch"); +const result = spawnSync( + process.execPath, + [ + "--import", + "tsx", + "--test", + "--test-concurrency=1", + ...(watch ? ["--watch"] : []), + ...testFiles, + ], + { stdio: "inherit" } +); + +process.exit(result.status ?? 1); diff --git a/src/db/migrations/0000_bizarre_colossus.sql b/src/db/migrations/0000_bizarre_colossus.sql deleted file mode 100644 index 71e123f..0000000 --- a/src/db/migrations/0000_bizarre_colossus.sql +++ /dev/null @@ -1,28 +0,0 @@ -CREATE TABLE `consumers` ( - `id` text PRIMARY KEY NOT NULL, - `project_id` text NOT NULL, - `distribution_board_id` text, - `name` text NOT NULL, - `category` text, - `quantity` integer NOT NULL, - `installed_power_per_unit_kw` real NOT NULL, - `demand_factor` real NOT NULL, - `voltage_v` real, - `phase_count` integer, - `power_factor` real, - `note` text, - FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade, - FOREIGN KEY (`distribution_board_id`) REFERENCES `distribution_boards`(`id`) ON UPDATE no action ON DELETE set null -); ---> statement-breakpoint -CREATE TABLE `distribution_boards` ( - `id` text PRIMARY KEY NOT NULL, - `project_id` text NOT NULL, - `name` text NOT NULL, - FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE TABLE `projects` ( - `id` text PRIMARY KEY NOT NULL, - `name` text NOT NULL -); diff --git a/src/db/migrations/0000_whole_surge.sql b/src/db/migrations/0000_whole_surge.sql new file mode 100644 index 0000000..ad5a66f --- /dev/null +++ b/src/db/migrations/0000_whole_surge.sql @@ -0,0 +1,300 @@ +CREATE TABLE `circuit_device_rows` ( + `id` text PRIMARY KEY NOT NULL, + `circuit_id` text NOT NULL, + `linked_project_device_id` text, + `sort_order` integer DEFAULT 0 NOT NULL, + `name` text NOT NULL, + `display_name` text NOT NULL, + `phase_type` text, + `connection_kind` text, + `cost_group` text, + `category` text, + `level` text, + `room_id` text, + `room_number_snapshot` text, + `room_name_snapshot` text, + `quantity` integer NOT NULL, + `power_per_unit` real NOT NULL, + `simultaneity_factor` real NOT NULL, + `cos_phi` real, + `remark` text, + `overridden_fields` text, + FOREIGN KEY (`circuit_id`) REFERENCES `circuits`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`linked_project_device_id`) REFERENCES `project_devices`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`room_id`) REFERENCES `rooms`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE TABLE `circuit_list_equipment_identifiers` ( + `owner_type` text NOT NULL, + `owner_id` text NOT NULL, + `circuit_list_id` text NOT NULL, + `equipment_identifier` text NOT NULL, + FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `circuit_list_equipment_identifiers_owner_unique` ON `circuit_list_equipment_identifiers` (`owner_type`,`owner_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `circuit_list_equipment_identifiers_list_identifier_unique` ON `circuit_list_equipment_identifiers` (`circuit_list_id`,`equipment_identifier`);--> statement-breakpoint +CREATE TABLE `circuit_lists` ( + `id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `distribution_board_id` text NOT NULL, + `name` text NOT NULL, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`distribution_board_id`) REFERENCES `distribution_boards`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `circuit_lists_distribution_board_id_unique` ON `circuit_lists` (`distribution_board_id`);--> statement-breakpoint +CREATE TABLE `circuit_protection_devices` ( + `circuit_id` text PRIMARY KEY NOT NULL, + `type` text NOT NULL, + `rated_current_a` real NOT NULL, + `fuse_utilization_category` text, + `trip_characteristic` text, + `rcd_type` text, + `rated_residual_current_ma` real, + FOREIGN KEY (`circuit_id`) REFERENCES `circuits`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `circuit_sections` ( + `id` text PRIMARY KEY NOT NULL, + `circuit_list_id` text NOT NULL, + `key` text NOT NULL, + `display_name` text NOT NULL, + `prefix` text NOT NULL, + `sort_order` integer DEFAULT 0 NOT NULL, + `category` text, + `group_number` integer, + FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `circuit_sections_list_key_unique` ON `circuit_sections` (`circuit_list_id`,`key`);--> statement-breakpoint +CREATE UNIQUE INDEX `circuit_sections_list_prefix_unique` ON `circuit_sections` (`circuit_list_id`,`prefix`);--> statement-breakpoint +CREATE UNIQUE INDEX `circuit_sections_list_category_group_unique` ON `circuit_sections` (`circuit_list_id`,`category`,`group_number`);--> statement-breakpoint +CREATE TABLE `circuits` ( + `id` text PRIMARY KEY NOT NULL, + `circuit_list_id` text NOT NULL, + `section_id` text NOT NULL, + `equipment_identifier` text NOT NULL, + `display_name` text, + `sort_order` integer DEFAULT 0 NOT NULL, + `cable_type` text, + `cable_cross_section` text, + `cable_length` real, + `rcd_assignment` text, + `terminal_designation` text, + `voltage` real, + `control_requirement` text, + `status` text, + `is_reserve` integer DEFAULT 0 NOT NULL, + `remark` text, + FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`section_id`) REFERENCES `circuit_sections`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `circuits_list_equipment_identifier_unique` ON `circuits` (`circuit_list_id`,`equipment_identifier`);--> statement-breakpoint +CREATE TABLE `distribution_board_component_protection_devices` ( + `component_id` text PRIMARY KEY NOT NULL, + `type` text NOT NULL, + `rated_current_a` real NOT NULL, + `fuse_utilization_category` text, + `trip_characteristic` text, + `rcd_type` text, + `rated_residual_current_ma` real, + FOREIGN KEY (`component_id`) REFERENCES `distribution_board_components`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `distribution_board_components` ( + `id` text PRIMARY KEY NOT NULL, + `circuit_list_id` text NOT NULL, + `section_id` text, + `equipment_identifier` text NOT NULL, + `name` text NOT NULL, + `role` text NOT NULL, + `placement` text NOT NULL, + `sort_order` integer DEFAULT 0 NOT NULL, + FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`section_id`) REFERENCES `circuit_sections`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `distribution_board_components_list_identifier_unique` ON `distribution_board_components` (`circuit_list_id`,`equipment_identifier`);--> statement-breakpoint +CREATE UNIQUE INDEX `distribution_board_components_section_role_unique` ON `distribution_board_components` (`section_id`,`role`);--> statement-breakpoint +CREATE TABLE `distribution_boards` ( + `id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `name` text NOT NULL, + `floor_id` text, + `supply_type` text, + `simultaneity_factor` real DEFAULT 1 NOT NULL, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`floor_id`) REFERENCES `floors`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE TABLE `floors` ( + `id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `name` text NOT NULL, + `sort_order` integer DEFAULT 0 NOT NULL, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `global_devices` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `display_name` text NOT NULL, + `category` text, + `quantity` integer NOT NULL, + `installed_power_per_unit_kw` real NOT NULL, + `demand_factor` real NOT NULL, + `voltage_v` real, + `phase_count` integer, + `power_factor` real, + `note` text +); +--> statement-breakpoint +CREATE TABLE `project_change_sets` ( + `id` text PRIMARY KEY NOT NULL, + `project_revision_id` text NOT NULL, + `command_type` text NOT NULL, + `payload_schema_version` integer NOT NULL, + `forward_payload_json` text NOT NULL, + `inverse_payload_json` text NOT NULL, + FOREIGN KEY (`project_revision_id`) REFERENCES `project_revisions`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `project_change_sets_revision_unique` ON `project_change_sets` (`project_revision_id`);--> statement-breakpoint +CREATE TABLE `project_devices` ( + `id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `name` text NOT NULL, + `display_name` text NOT NULL, + `phase_type` text DEFAULT 'single_phase' NOT NULL, + `connection_kind` text, + `cost_group` text, + `category` text, + `quantity` integer NOT NULL, + `power_per_unit` real DEFAULT 0 NOT NULL, + `simultaneity_factor` real DEFAULT 1 NOT NULL, + `cos_phi` real, + `remark` text, + `voltage_v` real, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `project_history_stack_entries` ( + `change_set_id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `stack` text NOT NULL, + `position` integer NOT NULL, + FOREIGN KEY (`change_set_id`) REFERENCES `project_change_sets`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade, + CONSTRAINT "project_history_stack_entries_stack_check" CHECK("project_history_stack_entries"."stack" in ('undo', 'redo')) +); +--> statement-breakpoint +CREATE UNIQUE INDEX `project_history_stack_entries_position_unique` ON `project_history_stack_entries` (`project_id`,`stack`,`position`);--> statement-breakpoint +CREATE TABLE `project_revisions` ( + `id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `revision_number` integer NOT NULL, + `created_at_iso` text NOT NULL, + `actor_id` text, + `source` text NOT NULL, + `description` text, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `project_revisions_project_number_unique` ON `project_revisions` (`project_id`,`revision_number`);--> statement-breakpoint +CREATE TABLE `project_snapshots` ( + `id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `source_revision` integer NOT NULL, + `schema_version` integer NOT NULL, + `kind` text DEFAULT 'named' NOT NULL, + `name` text NOT NULL, + `description` text, + `payload_json` text NOT NULL, + `payload_sha256` text NOT NULL, + `created_at_iso` text NOT NULL, + `created_by_actor_id` text, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `project_snapshots_project_created_idx` ON `project_snapshots` (`project_id`,`created_at_iso`);--> statement-breakpoint +CREATE INDEX `project_snapshots_project_kind_revision_idx` ON `project_snapshots` (`project_id`,`kind`,`source_revision`);--> statement-breakpoint +CREATE UNIQUE INDEX `project_snapshots_project_name_unique` ON `project_snapshots` (`project_id`,`name`);--> statement-breakpoint +CREATE TABLE `projects` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `internal_project_number` text, + `external_project_number` text, + `building_owner` text, + `description` text, + `single_phase_voltage_v` integer DEFAULT 230 NOT NULL, + `three_phase_voltage_v` integer DEFAULT 400 NOT NULL, + `enabled_distribution_board_supply_types` text DEFAULT '["AV","SV","EV","USV","MSR","SiBe"]' NOT NULL, + `current_revision` integer DEFAULT 0 NOT NULL +); +--> statement-breakpoint +CREATE TABLE `rooms` ( + `id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `floor_id` text, + `room_number` text NOT NULL, + `room_name` text NOT NULL, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`floor_id`) REFERENCES `floors`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE UNIQUE INDEX `circuit_list_equipment_identifiers_normalized_unique` +ON `circuit_list_equipment_identifiers` (`circuit_list_id`, lower(trim(`equipment_identifier`))); +--> statement-breakpoint +CREATE UNIQUE INDEX `distribution_board_components_fixed_role_unique` +ON `distribution_board_components` (`circuit_list_id`, `role`) +WHERE `role` IN ('main_switch', 'surge_protective_device'); +--> statement-breakpoint +CREATE TRIGGER `circuits_equipment_identifier_insert` +AFTER INSERT ON `circuits` +BEGIN + INSERT INTO `circuit_list_equipment_identifiers` + (`owner_type`, `owner_id`, `circuit_list_id`, `equipment_identifier`) + VALUES ('circuit', NEW.`id`, NEW.`circuit_list_id`, NEW.`equipment_identifier`); +END; +--> statement-breakpoint +CREATE TRIGGER `circuits_equipment_identifier_update` +AFTER UPDATE OF `circuit_list_id`, `equipment_identifier` ON `circuits` +BEGIN + UPDATE `circuit_list_equipment_identifiers` + SET `circuit_list_id` = NEW.`circuit_list_id`, + `equipment_identifier` = NEW.`equipment_identifier` + WHERE `owner_type` = 'circuit' AND `owner_id` = OLD.`id`; +END; +--> statement-breakpoint +CREATE TRIGGER `circuits_equipment_identifier_delete` +AFTER DELETE ON `circuits` +BEGIN + DELETE FROM `circuit_list_equipment_identifiers` + WHERE `owner_type` = 'circuit' AND `owner_id` = OLD.`id`; +END; +--> statement-breakpoint +CREATE TRIGGER `distribution_board_components_equipment_identifier_insert` +AFTER INSERT ON `distribution_board_components` +BEGIN + INSERT INTO `circuit_list_equipment_identifiers` + (`owner_type`, `owner_id`, `circuit_list_id`, `equipment_identifier`) + VALUES ('distribution_board_component', NEW.`id`, NEW.`circuit_list_id`, NEW.`equipment_identifier`); +END; +--> statement-breakpoint +CREATE TRIGGER `distribution_board_components_equipment_identifier_update` +AFTER UPDATE OF `circuit_list_id`, `equipment_identifier` ON `distribution_board_components` +BEGIN + UPDATE `circuit_list_equipment_identifiers` + SET `circuit_list_id` = NEW.`circuit_list_id`, + `equipment_identifier` = NEW.`equipment_identifier` + WHERE `owner_type` = 'distribution_board_component' AND `owner_id` = OLD.`id`; +END; +--> statement-breakpoint +CREATE TRIGGER `distribution_board_components_equipment_identifier_delete` +AFTER DELETE ON `distribution_board_components` +BEGIN + DELETE FROM `circuit_list_equipment_identifiers` + WHERE `owner_type` = 'distribution_board_component' AND `owner_id` = OLD.`id`; +END; diff --git a/src/db/migrations/0001_global_devices.sql b/src/db/migrations/0001_global_devices.sql deleted file mode 100644 index 8a065b1..0000000 --- a/src/db/migrations/0001_global_devices.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE TABLE `global_devices` ( - `id` text PRIMARY KEY NOT NULL, - `name` text NOT NULL, - `category` text, - `quantity` integer NOT NULL, - `installed_power_per_unit_kw` real NOT NULL, - `demand_factor` real NOT NULL, - `voltage_v` real, - `phase_count` integer, - `power_factor` real, - `note` text -); diff --git a/src/db/migrations/0002_project_voltage_defaults.sql b/src/db/migrations/0002_project_voltage_defaults.sql deleted file mode 100644 index a1671c9..0000000 --- a/src/db/migrations/0002_project_voltage_defaults.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE `projects` ADD COLUMN `single_phase_voltage_v` integer NOT NULL DEFAULT 230; ---> statement-breakpoint -ALTER TABLE `projects` ADD COLUMN `three_phase_voltage_v` integer NOT NULL DEFAULT 400; diff --git a/src/db/migrations/0003_project_floors_rooms.sql b/src/db/migrations/0003_project_floors_rooms.sql deleted file mode 100644 index 1cc95e3..0000000 --- a/src/db/migrations/0003_project_floors_rooms.sql +++ /dev/null @@ -1,19 +0,0 @@ -CREATE TABLE `floors` ( - `id` text PRIMARY KEY NOT NULL, - `project_id` text NOT NULL, - `name` text NOT NULL, - `sort_order` integer NOT NULL DEFAULT 0, - FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE TABLE `rooms` ( - `id` text PRIMARY KEY NOT NULL, - `project_id` text NOT NULL, - `floor_id` text, - `room_number` text NOT NULL, - `room_name` text NOT NULL, - FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade, - FOREIGN KEY (`floor_id`) REFERENCES `floors`(`id`) ON UPDATE no action ON DELETE set null -); ---> statement-breakpoint -ALTER TABLE `consumers` ADD COLUMN `room_id` text REFERENCES `rooms`(`id`) ON UPDATE no action ON DELETE set null; diff --git a/src/db/migrations/0004_circuit_lists_and_entry_fields.sql b/src/db/migrations/0004_circuit_lists_and_entry_fields.sql deleted file mode 100644 index 263a255..0000000 --- a/src/db/migrations/0004_circuit_lists_and_entry_fields.sql +++ /dev/null @@ -1,44 +0,0 @@ -CREATE TABLE `circuit_lists` ( - `id` text PRIMARY KEY NOT NULL, - `project_id` text NOT NULL, - `distribution_board_id` text NOT NULL, - `name` text NOT NULL, - FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade, - FOREIGN KEY (`distribution_board_id`) REFERENCES `distribution_boards`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE UNIQUE INDEX `circuit_lists_distribution_board_id_unique` ON `circuit_lists` (`distribution_board_id`); ---> statement-breakpoint -INSERT INTO `circuit_lists` (`id`, `project_id`, `distribution_board_id`, `name`) -SELECT `id`, `project_id`, `id`, `name` || ' Stromkreisliste' -FROM `distribution_boards`; ---> statement-breakpoint -ALTER TABLE `consumers` ADD COLUMN `circuit_list_id` text REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE set null; ---> statement-breakpoint -UPDATE `consumers` SET `circuit_list_id` = `distribution_board_id` WHERE `distribution_board_id` IS NOT NULL; ---> statement-breakpoint -ALTER TABLE `consumers` ADD COLUMN `circuit_number` text; ---> statement-breakpoint -ALTER TABLE `consumers` ADD COLUMN `description` text; ---> statement-breakpoint -UPDATE `consumers` SET `description` = `name` WHERE `description` IS NULL; ---> statement-breakpoint -ALTER TABLE `consumers` ADD COLUMN `device_type` text; ---> statement-breakpoint -ALTER TABLE `consumers` ADD COLUMN `phase_type` text; ---> statement-breakpoint -ALTER TABLE `consumers` ADD COLUMN `trade_or_cost_group` text; ---> statement-breakpoint -ALTER TABLE `consumers` ADD COLUMN `group_name` text; ---> statement-breakpoint -ALTER TABLE `consumers` ADD COLUMN `protection_type` text; ---> statement-breakpoint -ALTER TABLE `consumers` ADD COLUMN `protection_rated_current` real; ---> statement-breakpoint -ALTER TABLE `consumers` ADD COLUMN `protection_characteristic` text; ---> statement-breakpoint -ALTER TABLE `consumers` ADD COLUMN `cable_type` text; ---> statement-breakpoint -ALTER TABLE `consumers` ADD COLUMN `cable_cross_section` text; ---> statement-breakpoint -ALTER TABLE `consumers` ADD COLUMN `comment` text; diff --git a/src/db/migrations/0005_project_devices.sql b/src/db/migrations/0005_project_devices.sql deleted file mode 100644 index 5199be3..0000000 --- a/src/db/migrations/0005_project_devices.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE `project_devices` ( - `id` text PRIMARY KEY NOT NULL, - `project_id` text NOT NULL, - `name` text NOT NULL, - `category` text, - `quantity` integer NOT NULL, - `installed_power_per_unit_kw` real NOT NULL, - `demand_factor` real NOT NULL, - `voltage_v` real, - `phase_count` integer, - `power_factor` real, - `note` text, - FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade -); diff --git a/src/db/migrations/0006_device_display_name.sql b/src/db/migrations/0006_device_display_name.sql deleted file mode 100644 index ce753a0..0000000 --- a/src/db/migrations/0006_device_display_name.sql +++ /dev/null @@ -1,7 +0,0 @@ -ALTER TABLE `global_devices` ADD COLUMN `display_name` text; ---> statement-breakpoint -UPDATE `global_devices` SET `display_name` = `name` WHERE `display_name` IS NULL; ---> statement-breakpoint -ALTER TABLE `project_devices` ADD COLUMN `display_name` text; ---> statement-breakpoint -UPDATE `project_devices` SET `display_name` = `name` WHERE `display_name` IS NULL; diff --git a/src/db/migrations/0007_consumer_device_link.sql b/src/db/migrations/0007_consumer_device_link.sql deleted file mode 100644 index d5d667d..0000000 --- a/src/db/migrations/0007_consumer_device_link.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE `consumers` ADD COLUMN `project_device_id` text REFERENCES `project_devices`(`id`) ON UPDATE no action ON DELETE set null; ---> statement-breakpoint -ALTER TABLE `consumers` ADD COLUMN `is_linked_to_device` integer NOT NULL DEFAULT 0; diff --git a/src/db/migrations/0008_circuit_first_model.sql b/src/db/migrations/0008_circuit_first_model.sql deleted file mode 100644 index 485a911..0000000 --- a/src/db/migrations/0008_circuit_first_model.sql +++ /dev/null @@ -1,99 +0,0 @@ -CREATE TABLE `circuit_sections` ( - `id` text PRIMARY KEY NOT NULL, - `circuit_list_id` text NOT NULL, - `key` text NOT NULL, - `display_name` text NOT NULL, - `prefix` text NOT NULL, - `sort_order` integer NOT NULL DEFAULT 0, - FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE UNIQUE INDEX `circuit_sections_list_key_unique` ON `circuit_sections` (`circuit_list_id`,`key`); ---> statement-breakpoint -CREATE UNIQUE INDEX `circuit_sections_list_prefix_unique` ON `circuit_sections` (`circuit_list_id`,`prefix`); ---> statement-breakpoint - -CREATE TABLE `circuits` ( - `id` text PRIMARY KEY NOT NULL, - `circuit_list_id` text NOT NULL, - `section_id` text NOT NULL, - `equipment_identifier` text NOT NULL, - `display_name` text, - `sort_order` integer NOT NULL DEFAULT 0, - `protection_type` text, - `protection_rated_current` real, - `protection_characteristic` text, - `cable_type` text, - `cable_cross_section` text, - `cable_length` real, - `rcd_assignment` text, - `terminal_designation` text, - `voltage` real, - `status` text, - `is_reserve` integer NOT NULL DEFAULT 0, - `remark` text, - FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade, - FOREIGN KEY (`section_id`) REFERENCES `circuit_sections`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE UNIQUE INDEX `circuits_list_equipment_identifier_unique` ON `circuits` (`circuit_list_id`,`equipment_identifier`); ---> statement-breakpoint - -CREATE TABLE `circuit_device_rows` ( - `id` text PRIMARY KEY NOT NULL, - `circuit_id` text NOT NULL, - `linked_project_device_id` text, - `legacy_consumer_id` text, - `sort_order` integer NOT NULL DEFAULT 0, - `name` text NOT NULL, - `display_name` text NOT NULL, - `phase_type` text, - `connection_kind` text, - `cost_group` text, - `category` text, - `level` text, - `room_id` text, - `room_number_snapshot` text, - `room_name_snapshot` text, - `quantity` integer NOT NULL, - `power_per_unit` real NOT NULL, - `simultaneity_factor` real NOT NULL, - `cos_phi` real, - `remark` text, - `overridden_fields` text, - FOREIGN KEY (`circuit_id`) REFERENCES `circuits`(`id`) ON UPDATE no action ON DELETE cascade, - FOREIGN KEY (`linked_project_device_id`) REFERENCES `project_devices`(`id`) ON UPDATE no action ON DELETE set null, - FOREIGN KEY (`room_id`) REFERENCES `rooms`(`id`) ON UPDATE no action ON DELETE set null -); ---> statement-breakpoint - -CREATE TABLE `legacy_consumer_circuit_migrations` ( - `consumer_id` text PRIMARY KEY NOT NULL, - `circuit_id` text NOT NULL, - `circuit_device_row_id` text NOT NULL, - `circuit_list_id` text NOT NULL, - `created_at_iso` text NOT NULL, - FOREIGN KEY (`circuit_id`) REFERENCES `circuits`(`id`) ON UPDATE no action ON DELETE cascade, - FOREIGN KEY (`circuit_device_row_id`) REFERENCES `circuit_device_rows`(`id`) ON UPDATE no action ON DELETE cascade, - FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint - -CREATE TABLE `legacy_consumer_migration_reports` ( - `id` text PRIMARY KEY NOT NULL, - `circuit_list_id` text NOT NULL, - `legacy_consumer_count` integer NOT NULL, - `created_circuit_count` integer NOT NULL, - `created_device_row_count` integer NOT NULL, - `duplicate_grouped_count` integer NOT NULL, - `generated_identifier_count` integer NOT NULL, - `unassigned_row_count` integer NOT NULL, - `warnings_json` text NOT NULL, - `generated_identifiers_json` text NOT NULL, - `duplicate_groups_json` text NOT NULL, - `created_at_iso` text NOT NULL, - FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE UNIQUE INDEX `legacy_consumer_migration_reports_list_unique` ON `legacy_consumer_migration_reports` (`circuit_list_id`); - diff --git a/src/db/migrations/0009_project_device_circuit_fields.sql b/src/db/migrations/0009_project_device_circuit_fields.sql deleted file mode 100644 index 2c2c631..0000000 --- a/src/db/migrations/0009_project_device_circuit_fields.sql +++ /dev/null @@ -1,24 +0,0 @@ -ALTER TABLE `project_devices` ADD COLUMN `phase_type` text NOT NULL DEFAULT 'single_phase'; ---> statement-breakpoint -UPDATE `project_devices` -SET `phase_type` = CASE WHEN `phase_count` = 3 THEN 'three_phase' ELSE 'single_phase' END; ---> statement-breakpoint -ALTER TABLE `project_devices` ADD COLUMN `connection_kind` text; ---> statement-breakpoint -ALTER TABLE `project_devices` ADD COLUMN `cost_group` text; ---> statement-breakpoint -ALTER TABLE `project_devices` ADD COLUMN `power_per_unit` real NOT NULL DEFAULT 0; ---> statement-breakpoint -UPDATE `project_devices` SET `power_per_unit` = `installed_power_per_unit_kw`; ---> statement-breakpoint -ALTER TABLE `project_devices` ADD COLUMN `simultaneity_factor` real NOT NULL DEFAULT 1; ---> statement-breakpoint -UPDATE `project_devices` SET `simultaneity_factor` = `demand_factor`; ---> statement-breakpoint -ALTER TABLE `project_devices` ADD COLUMN `cos_phi` real; ---> statement-breakpoint -UPDATE `project_devices` SET `cos_phi` = `power_factor`; ---> statement-breakpoint -ALTER TABLE `project_devices` ADD COLUMN `remark` text; ---> statement-breakpoint -UPDATE `project_devices` SET `remark` = `note`; diff --git a/src/db/migrations/0010_circuit_control_requirement.sql b/src/db/migrations/0010_circuit_control_requirement.sql deleted file mode 100644 index c51fdf8..0000000 --- a/src/db/migrations/0010_circuit_control_requirement.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `circuits` ADD COLUMN `control_requirement` text; diff --git a/src/db/migrations/0011_project_device_canonical_fields.sql b/src/db/migrations/0011_project_device_canonical_fields.sql deleted file mode 100644 index 1ed5969..0000000 --- a/src/db/migrations/0011_project_device_canonical_fields.sql +++ /dev/null @@ -1,9 +0,0 @@ -ALTER TABLE `project_devices` DROP COLUMN `installed_power_per_unit_kw`; ---> statement-breakpoint -ALTER TABLE `project_devices` DROP COLUMN `demand_factor`; ---> statement-breakpoint -ALTER TABLE `project_devices` DROP COLUMN `phase_count`; ---> statement-breakpoint -ALTER TABLE `project_devices` DROP COLUMN `power_factor`; ---> statement-breakpoint -ALTER TABLE `project_devices` DROP COLUMN `note`; diff --git a/src/db/migrations/0012_project_revision_foundation.sql b/src/db/migrations/0012_project_revision_foundation.sql deleted file mode 100644 index 7df8d19..0000000 --- a/src/db/migrations/0012_project_revision_foundation.sql +++ /dev/null @@ -1,24 +0,0 @@ -CREATE TABLE `project_change_sets` ( - `id` text PRIMARY KEY NOT NULL, - `project_revision_id` text NOT NULL, - `command_type` text NOT NULL, - `payload_schema_version` integer NOT NULL, - `forward_payload_json` text NOT NULL, - `inverse_payload_json` text NOT NULL, - FOREIGN KEY (`project_revision_id`) REFERENCES `project_revisions`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE UNIQUE INDEX `project_change_sets_revision_unique` ON `project_change_sets` (`project_revision_id`);--> statement-breakpoint -CREATE TABLE `project_revisions` ( - `id` text PRIMARY KEY NOT NULL, - `project_id` text NOT NULL, - `revision_number` integer NOT NULL, - `created_at_iso` text NOT NULL, - `actor_id` text, - `source` text NOT NULL, - `description` text, - FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE UNIQUE INDEX `project_revisions_project_number_unique` ON `project_revisions` (`project_id`,`revision_number`);--> statement-breakpoint -ALTER TABLE `projects` ADD `current_revision` integer DEFAULT 0 NOT NULL; diff --git a/src/db/migrations/0013_project_history_stacks.sql b/src/db/migrations/0013_project_history_stacks.sql deleted file mode 100644 index 8fd8b30..0000000 --- a/src/db/migrations/0013_project_history_stacks.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE `project_history_stack_entries` ( - `change_set_id` text PRIMARY KEY NOT NULL, - `project_id` text NOT NULL, - `stack` text NOT NULL, - `position` integer NOT NULL, - FOREIGN KEY (`change_set_id`) REFERENCES `project_change_sets`(`id`) ON UPDATE no action ON DELETE cascade, - FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade, - CONSTRAINT "project_history_stack_entries_stack_check" CHECK("project_history_stack_entries"."stack" in ('undo', 'redo')) -); ---> statement-breakpoint -CREATE UNIQUE INDEX `project_history_stack_entries_position_unique` ON `project_history_stack_entries` (`project_id`,`stack`,`position`); diff --git a/src/db/migrations/0014_project_snapshots.sql b/src/db/migrations/0014_project_snapshots.sql deleted file mode 100644 index a088cb4..0000000 --- a/src/db/migrations/0014_project_snapshots.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE `project_snapshots` ( - `id` text PRIMARY KEY NOT NULL, - `project_id` text NOT NULL, - `source_revision` integer NOT NULL, - `schema_version` integer NOT NULL, - `name` text NOT NULL, - `description` text, - `payload_json` text NOT NULL, - `payload_sha256` text NOT NULL, - `created_at_iso` text NOT NULL, - `created_by_actor_id` text, - FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE INDEX `project_snapshots_project_created_idx` ON `project_snapshots` (`project_id`,`created_at_iso`);--> statement-breakpoint -CREATE UNIQUE INDEX `project_snapshots_project_name_unique` ON `project_snapshots` (`project_id`,`name`); \ No newline at end of file diff --git a/src/db/migrations/0015_modern_madame_masque.sql b/src/db/migrations/0015_modern_madame_masque.sql deleted file mode 100644 index 2007787..0000000 --- a/src/db/migrations/0015_modern_madame_masque.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE `project_snapshots` ADD `kind` text DEFAULT 'named' NOT NULL;--> statement-breakpoint -CREATE INDEX `project_snapshots_project_kind_revision_idx` ON `project_snapshots` (`project_id`,`kind`,`source_revision`); \ No newline at end of file diff --git a/src/db/migrations/0016_dashing_darkstar.sql b/src/db/migrations/0016_dashing_darkstar.sql deleted file mode 100644 index 2228b1b..0000000 --- a/src/db/migrations/0016_dashing_darkstar.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE `projects` ADD `internal_project_number` text;--> statement-breakpoint -ALTER TABLE `projects` ADD `external_project_number` text;--> statement-breakpoint -ALTER TABLE `projects` ADD `building_owner` text;--> statement-breakpoint -ALTER TABLE `projects` ADD `description` text; \ No newline at end of file diff --git a/src/db/migrations/0017_vengeful_romulus.sql b/src/db/migrations/0017_vengeful_romulus.sql deleted file mode 100644 index 8198de7..0000000 --- a/src/db/migrations/0017_vengeful_romulus.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE `distribution_boards` ADD `floor_id` text REFERENCES floors(id);--> statement-breakpoint -ALTER TABLE `distribution_boards` ADD `supply_type` text; \ No newline at end of file diff --git a/src/db/migrations/0018_fancy_argent.sql b/src/db/migrations/0018_fancy_argent.sql deleted file mode 100644 index feab398..0000000 --- a/src/db/migrations/0018_fancy_argent.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `projects` ADD `enabled_distribution_board_supply_types` text DEFAULT '["AV","SV","EV","USV","MSR","SiBe"]' NOT NULL; \ No newline at end of file diff --git a/src/db/migrations/0019_normalize_project_voltages.sql b/src/db/migrations/0019_normalize_project_voltages.sql deleted file mode 100644 index 6d74e31..0000000 --- a/src/db/migrations/0019_normalize_project_voltages.sql +++ /dev/null @@ -1,48 +0,0 @@ -UPDATE `global_devices` -SET `voltage_v` = NULL; ---> statement-breakpoint -UPDATE `project_devices` AS `pd` -SET `voltage_v` = CASE - WHEN `pd`.`phase_type` = 'three_phase' THEN `p`.`three_phase_voltage_v` - ELSE `p`.`single_phase_voltage_v` -END -FROM `projects` AS `p` -WHERE `p`.`id` = `pd`.`project_id`; ---> statement-breakpoint -WITH `derived_circuit_voltages` AS ( - SELECT - `c`.`id` AS `circuit_id`, - CASE - WHEN `cs`.`key` = 'three_phase' THEN `p`.`three_phase_voltage_v` - WHEN `cs`.`key` IN ('lighting', 'single_phase') THEN `p`.`single_phase_voltage_v` - WHEN EXISTS ( - SELECT 1 - FROM `circuit_device_rows` AS `three_phase_row` - WHERE `three_phase_row`.`circuit_id` = `c`.`id` - AND `three_phase_row`.`phase_type` = 'three_phase' - ) AND NOT EXISTS ( - SELECT 1 - FROM `circuit_device_rows` AS `other_row` - WHERE `other_row`.`circuit_id` = `c`.`id` - AND `other_row`.`phase_type` = 'single_phase' - ) THEN `p`.`three_phase_voltage_v` - ELSE `p`.`single_phase_voltage_v` - END AS `derived_voltage` - FROM `circuits` AS `c` - INNER JOIN `circuit_lists` AS `cl` - ON `cl`.`id` = `c`.`circuit_list_id` - INNER JOIN `projects` AS `p` - ON `p`.`id` = `cl`.`project_id` - INNER JOIN `circuit_sections` AS `cs` - ON `cs`.`id` = `c`.`section_id` -) -UPDATE `circuits` -SET `voltage` = ( - SELECT `derived_voltage` - FROM `derived_circuit_voltages` - WHERE `circuit_id` = `circuits`.`id` -) -WHERE `id` IN ( - SELECT `circuit_id` - FROM `derived_circuit_voltages` -); diff --git a/src/db/migrations/0020_normalize_phase_types.sql b/src/db/migrations/0020_normalize_phase_types.sql deleted file mode 100644 index 2ec4303..0000000 --- a/src/db/migrations/0020_normalize_phase_types.sql +++ /dev/null @@ -1,85 +0,0 @@ -UPDATE `project_devices` -SET `phase_type` = CASE - WHEN LOWER(REPLACE(REPLACE(TRIM(`phase_type`), '-', ''), '_', '')) - IN ('1ph', '1phase', '1phasig', 'einphasig', 'singlephase') - THEN 'single_phase' - WHEN LOWER(REPLACE(REPLACE(TRIM(`phase_type`), '-', ''), '_', '')) - IN ('3ph', '3phase', '3phasig', 'dreiphasig', 'threephase') - THEN 'three_phase' - ELSE `phase_type` -END -WHERE LOWER(REPLACE(REPLACE(TRIM(`phase_type`), '-', ''), '_', '')) - IN ( - '1ph', - '1phase', - '1phasig', - 'einphasig', - 'singlephase', - '3ph', - '3phase', - '3phasig', - 'dreiphasig', - 'threephase' - ); ---> statement-breakpoint -UPDATE `circuit_device_rows` -SET `phase_type` = CASE - WHEN LOWER(REPLACE(REPLACE(TRIM(`phase_type`), '-', ''), '_', '')) - IN ('1ph', '1phase', '1phasig', 'einphasig', 'singlephase') - THEN 'single_phase' - WHEN LOWER(REPLACE(REPLACE(TRIM(`phase_type`), '-', ''), '_', '')) - IN ('3ph', '3phase', '3phasig', 'dreiphasig', 'threephase') - THEN 'three_phase' - ELSE `phase_type` -END -WHERE LOWER(REPLACE(REPLACE(TRIM(`phase_type`), '-', ''), '_', '')) - IN ( - '1ph', - '1phase', - '1phasig', - 'einphasig', - 'singlephase', - '3ph', - '3phase', - '3phasig', - 'dreiphasig', - 'threephase' - ); ---> statement-breakpoint -WITH `derived_circuit_voltages` AS ( - SELECT - `c`.`id` AS `circuit_id`, - CASE - WHEN `cs`.`key` = 'three_phase' THEN `p`.`three_phase_voltage_v` - WHEN `cs`.`key` IN ('lighting', 'single_phase') THEN `p`.`single_phase_voltage_v` - WHEN EXISTS ( - SELECT 1 - FROM `circuit_device_rows` AS `three_phase_row` - WHERE `three_phase_row`.`circuit_id` = `c`.`id` - AND `three_phase_row`.`phase_type` = 'three_phase' - ) AND NOT EXISTS ( - SELECT 1 - FROM `circuit_device_rows` AS `single_phase_row` - WHERE `single_phase_row`.`circuit_id` = `c`.`id` - AND `single_phase_row`.`phase_type` = 'single_phase' - ) THEN `p`.`three_phase_voltage_v` - ELSE `p`.`single_phase_voltage_v` - END AS `derived_voltage` - FROM `circuits` AS `c` - INNER JOIN `circuit_lists` AS `cl` - ON `cl`.`id` = `c`.`circuit_list_id` - INNER JOIN `projects` AS `p` - ON `p`.`id` = `cl`.`project_id` - INNER JOIN `circuit_sections` AS `cs` - ON `cs`.`id` = `c`.`section_id` -) -UPDATE `circuits` -SET `voltage` = ( - SELECT `derived_voltage` - FROM `derived_circuit_voltages` - WHERE `circuit_id` = `circuits`.`id` -) -WHERE `id` IN ( - SELECT `circuit_id` - FROM `derived_circuit_voltages` -); diff --git a/src/db/migrations/0021_fill_missing_phase_types.sql b/src/db/migrations/0021_fill_missing_phase_types.sql deleted file mode 100644 index fe6cbf4..0000000 --- a/src/db/migrations/0021_fill_missing_phase_types.sql +++ /dev/null @@ -1,75 +0,0 @@ -UPDATE `circuit_device_rows` -SET `phase_type` = COALESCE( - ( - SELECT `pd`.`phase_type` - FROM `project_devices` AS `pd` - WHERE `pd`.`id` = `circuit_device_rows`.`linked_project_device_id` - AND `pd`.`phase_type` IN ('single_phase', 'three_phase') - ), - ( - SELECT CASE - WHEN `cs`.`key` = 'three_phase' THEN 'three_phase' - WHEN `cs`.`key` IN ('lighting', 'single_phase') THEN 'single_phase' - ELSE NULL - END - FROM `circuits` AS `c` - INNER JOIN `circuit_sections` AS `cs` - ON `cs`.`id` = `c`.`section_id` - WHERE `c`.`id` = `circuit_device_rows`.`circuit_id` - ) -) -WHERE `phase_type` IS NULL - AND ( - EXISTS ( - SELECT 1 - FROM `project_devices` AS `pd` - WHERE `pd`.`id` = `circuit_device_rows`.`linked_project_device_id` - AND `pd`.`phase_type` IN ('single_phase', 'three_phase') - ) - OR EXISTS ( - SELECT 1 - FROM `circuits` AS `c` - INNER JOIN `circuit_sections` AS `cs` - ON `cs`.`id` = `c`.`section_id` - WHERE `c`.`id` = `circuit_device_rows`.`circuit_id` - AND `cs`.`key` IN ('lighting', 'single_phase', 'three_phase') - ) - ); ---> statement-breakpoint -WITH `derived_circuit_voltages` AS ( - SELECT - `c`.`id` AS `circuit_id`, - CASE - WHEN `cs`.`key` = 'three_phase' THEN `p`.`three_phase_voltage_v` - WHEN `cs`.`key` IN ('lighting', 'single_phase') THEN `p`.`single_phase_voltage_v` - WHEN EXISTS ( - SELECT 1 - FROM `circuit_device_rows` AS `three_phase_row` - WHERE `three_phase_row`.`circuit_id` = `c`.`id` - AND `three_phase_row`.`phase_type` = 'three_phase' - ) AND NOT EXISTS ( - SELECT 1 - FROM `circuit_device_rows` AS `single_phase_row` - WHERE `single_phase_row`.`circuit_id` = `c`.`id` - AND `single_phase_row`.`phase_type` = 'single_phase' - ) THEN `p`.`three_phase_voltage_v` - ELSE `p`.`single_phase_voltage_v` - END AS `derived_voltage` - FROM `circuits` AS `c` - INNER JOIN `circuit_lists` AS `cl` - ON `cl`.`id` = `c`.`circuit_list_id` - INNER JOIN `projects` AS `p` - ON `p`.`id` = `cl`.`project_id` - INNER JOIN `circuit_sections` AS `cs` - ON `cs`.`id` = `c`.`section_id` -) -UPDATE `circuits` -SET `voltage` = ( - SELECT `derived_voltage` - FROM `derived_circuit_voltages` - WHERE `circuit_id` = `circuits`.`id` -) -WHERE `id` IN ( - SELECT `circuit_id` - FROM `derived_circuit_voltages` -); diff --git a/src/db/migrations/0022_default_remaining_phase_types.sql b/src/db/migrations/0022_default_remaining_phase_types.sql deleted file mode 100644 index 139bf6b..0000000 --- a/src/db/migrations/0022_default_remaining_phase_types.sql +++ /dev/null @@ -1,7 +0,0 @@ -UPDATE `circuit_device_rows` -SET `phase_type` = 'single_phase' -WHERE `phase_type` IS NULL; ---> statement-breakpoint -UPDATE `global_devices` -SET `phase_count` = 1 -WHERE `phase_count` IS NULL OR `phase_count` NOT IN (1, 3); diff --git a/src/db/migrations/0023_cheerful_night_thrasher.sql b/src/db/migrations/0023_cheerful_night_thrasher.sql deleted file mode 100644 index c68bb46..0000000 --- a/src/db/migrations/0023_cheerful_night_thrasher.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `distribution_boards` ADD `simultaneity_factor` real DEFAULT 1 NOT NULL; \ No newline at end of file diff --git a/src/db/migrations/0024_damp_mister_sinister.sql b/src/db/migrations/0024_damp_mister_sinister.sql deleted file mode 100644 index 3e054c3..0000000 --- a/src/db/migrations/0024_damp_mister_sinister.sql +++ /dev/null @@ -1,150 +0,0 @@ -CREATE TABLE `circuit_list_equipment_identifiers` ( - `owner_type` text NOT NULL, - `owner_id` text NOT NULL, - `circuit_list_id` text NOT NULL, - `equipment_identifier` text NOT NULL, - FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE UNIQUE INDEX `circuit_list_equipment_identifiers_owner_unique` ON `circuit_list_equipment_identifiers` (`owner_type`,`owner_id`);--> statement-breakpoint -CREATE UNIQUE INDEX `circuit_list_equipment_identifiers_list_identifier_unique` ON `circuit_list_equipment_identifiers` (`circuit_list_id`,`equipment_identifier`);--> statement-breakpoint -CREATE TABLE `circuit_protection_devices` ( - `circuit_id` text PRIMARY KEY NOT NULL, - `type` text NOT NULL, - `rated_current_a` real NOT NULL, - `fuse_utilization_category` text, - `trip_characteristic` text, - `rcd_type` text, - `rated_residual_current_ma` real, - FOREIGN KEY (`circuit_id`) REFERENCES `circuits`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE TABLE `distribution_board_component_protection_devices` ( - `component_id` text PRIMARY KEY NOT NULL, - `type` text NOT NULL, - `rated_current_a` real NOT NULL, - `fuse_utilization_category` text, - `trip_characteristic` text, - `rcd_type` text, - `rated_residual_current_ma` real, - FOREIGN KEY (`component_id`) REFERENCES `distribution_board_components`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE TABLE `distribution_board_components` ( - `id` text PRIMARY KEY NOT NULL, - `circuit_list_id` text NOT NULL, - `section_id` text, - `equipment_identifier` text NOT NULL, - `name` text NOT NULL, - `role` text NOT NULL, - `placement` text NOT NULL, - `sort_order` integer DEFAULT 0 NOT NULL, - FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade, - FOREIGN KEY (`section_id`) REFERENCES `circuit_sections`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE UNIQUE INDEX `distribution_board_components_list_identifier_unique` ON `distribution_board_components` (`circuit_list_id`,`equipment_identifier`);--> statement-breakpoint -CREATE UNIQUE INDEX `distribution_board_components_section_role_unique` ON `distribution_board_components` (`section_id`,`role`);--> statement-breakpoint -ALTER TABLE `circuit_sections` ADD `category` text;--> statement-breakpoint -ALTER TABLE `circuit_sections` ADD `group_number` integer;--> statement-breakpoint -CREATE UNIQUE INDEX `circuit_sections_list_category_group_unique` ON `circuit_sections` (`circuit_list_id`,`category`,`group_number`);--> statement-breakpoint -UPDATE `circuit_sections` -SET - `category` = CASE `key` - WHEN 'lighting' THEN 'lighting' - WHEN 'single_phase' THEN 'single_phase' - WHEN 'three_phase' THEN 'three_phase' - END, - `group_number` = 1 -WHERE `key` IN ('lighting', 'single_phase', 'three_phase');--> statement-breakpoint -INSERT INTO `circuit_list_equipment_identifiers` ( - `owner_type`, - `owner_id`, - `circuit_list_id`, - `equipment_identifier` -) -SELECT - 'circuit', - `id`, - `circuit_list_id`, - `equipment_identifier` -FROM `circuits`;--> statement-breakpoint -CREATE UNIQUE INDEX `circuit_list_equipment_identifiers_normalized_unique` -ON `circuit_list_equipment_identifiers` ( - `circuit_list_id`, - lower(trim(`equipment_identifier`)) -);--> statement-breakpoint -CREATE UNIQUE INDEX `distribution_board_components_fixed_role_unique` -ON `distribution_board_components` (`circuit_list_id`, `role`) -WHERE `role` IN ('main_switch', 'surge_protective_device');--> statement-breakpoint -CREATE TRIGGER `circuits_equipment_identifier_insert` -AFTER INSERT ON `circuits` -BEGIN - INSERT INTO `circuit_list_equipment_identifiers` ( - `owner_type`, - `owner_id`, - `circuit_list_id`, - `equipment_identifier` - ) - VALUES ( - 'circuit', - NEW.`id`, - NEW.`circuit_list_id`, - NEW.`equipment_identifier` - ); -END;--> statement-breakpoint -CREATE TRIGGER `circuits_equipment_identifier_update` -AFTER UPDATE OF `circuit_list_id`, `equipment_identifier` ON `circuits` -BEGIN - UPDATE `circuit_list_equipment_identifiers` - SET - `circuit_list_id` = NEW.`circuit_list_id`, - `equipment_identifier` = NEW.`equipment_identifier` - WHERE - `owner_type` = 'circuit' - AND `owner_id` = OLD.`id`; -END;--> statement-breakpoint -CREATE TRIGGER `circuits_equipment_identifier_delete` -AFTER DELETE ON `circuits` -BEGIN - DELETE FROM `circuit_list_equipment_identifiers` - WHERE - `owner_type` = 'circuit' - AND `owner_id` = OLD.`id`; -END;--> statement-breakpoint -CREATE TRIGGER `distribution_board_components_equipment_identifier_insert` -AFTER INSERT ON `distribution_board_components` -BEGIN - INSERT INTO `circuit_list_equipment_identifiers` ( - `owner_type`, - `owner_id`, - `circuit_list_id`, - `equipment_identifier` - ) - VALUES ( - 'distribution_board_component', - NEW.`id`, - NEW.`circuit_list_id`, - NEW.`equipment_identifier` - ); -END;--> statement-breakpoint -CREATE TRIGGER `distribution_board_components_equipment_identifier_update` -AFTER UPDATE OF `circuit_list_id`, `equipment_identifier` -ON `distribution_board_components` -BEGIN - UPDATE `circuit_list_equipment_identifiers` - SET - `circuit_list_id` = NEW.`circuit_list_id`, - `equipment_identifier` = NEW.`equipment_identifier` - WHERE - `owner_type` = 'distribution_board_component' - AND `owner_id` = OLD.`id`; -END;--> statement-breakpoint -CREATE TRIGGER `distribution_board_components_equipment_identifier_delete` -AFTER DELETE ON `distribution_board_components` -BEGIN - DELETE FROM `circuit_list_equipment_identifiers` - WHERE - `owner_type` = 'distribution_board_component' - AND `owner_id` = OLD.`id`; -END; diff --git a/src/db/migrations/meta/0000_snapshot.json b/src/db/migrations/meta/0000_snapshot.json index 40ca397..1f5d1e6 100644 --- a/src/db/migrations/meta/0000_snapshot.json +++ b/src/db/migrations/meta/0000_snapshot.json @@ -1,11 +1,271 @@ { "version": "6", "dialect": "sqlite", - "id": "d219f155-9dd0-48e3-8fd2-8278f1f788ca", + "id": "2f464eb0-b1c4-4d2d-bfc8-a1466788c961", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { - "consumers": { - "name": "consumers", + "circuit_device_rows": { + "name": "circuit_device_rows", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_id": { + "name": "circuit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "linked_project_device_id": { + "name": "linked_project_device_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase_type": { + "name": "phase_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_kind": { + "name": "connection_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_group": { + "name": "cost_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_number_snapshot": { + "name": "room_number_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_name_snapshot": { + "name": "room_name_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "power_per_unit": { + "name": "power_per_unit", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cos_phi": { + "name": "cos_phi", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "overridden_fields": { + "name": "overridden_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "circuit_device_rows_circuit_id_circuits_id_fk": { + "name": "circuit_device_rows_circuit_id_circuits_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "circuits", + "columnsFrom": [ + "circuit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { + "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "project_devices", + "columnsFrom": [ + "linked_project_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "circuit_device_rows_room_id_rooms_id_fk": { + "name": "circuit_device_rows_room_id_rooms_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_list_equipment_identifiers": { + "name": "circuit_list_equipment_identifiers", + "columns": { + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "circuit_list_equipment_identifiers_owner_unique": { + "name": "circuit_list_equipment_identifiers_owner_unique", + "columns": [ + "owner_type", + "owner_id" + ], + "isUnique": true + }, + "circuit_list_equipment_identifiers_list_identifier_unique": { + "name": "circuit_list_equipment_identifiers_list_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk": { + "name": "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuit_list_equipment_identifiers", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_lists": { + "name": "circuit_lists", "columns": { "id": { "name": "id", @@ -25,7 +285,7 @@ "name": "distribution_board_id", "type": "text", "primaryKey": false, - "notNull": false, + "notNull": true, "autoincrement": false }, "name": { @@ -34,6 +294,729 @@ "primaryKey": false, "notNull": true, "autoincrement": false + } + }, + "indexes": { + "circuit_lists_distribution_board_id_unique": { + "name": "circuit_lists_distribution_board_id_unique", + "columns": [ + "distribution_board_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_lists_project_id_projects_id_fk": { + "name": "circuit_lists_project_id_projects_id_fk", + "tableFrom": "circuit_lists", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuit_lists_distribution_board_id_distribution_boards_id_fk": { + "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "circuit_lists", + "tableTo": "distribution_boards", + "columnsFrom": [ + "distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_protection_devices": { + "name": "circuit_protection_devices", + "columns": { + "circuit_id": { + "name": "circuit_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rated_current_a": { + "name": "rated_current_a", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fuse_utilization_category": { + "name": "fuse_utilization_category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trip_characteristic": { + "name": "trip_characteristic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_type": { + "name": "rcd_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rated_residual_current_ma": { + "name": "rated_residual_current_ma", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "circuit_protection_devices_circuit_id_circuits_id_fk": { + "name": "circuit_protection_devices_circuit_id_circuits_id_fk", + "tableFrom": "circuit_protection_devices", + "tableTo": "circuits", + "columnsFrom": [ + "circuit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_sections": { + "name": "circuit_sections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_number": { + "name": "group_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "circuit_sections_list_key_unique": { + "name": "circuit_sections_list_key_unique", + "columns": [ + "circuit_list_id", + "key" + ], + "isUnique": true + }, + "circuit_sections_list_prefix_unique": { + "name": "circuit_sections_list_prefix_unique", + "columns": [ + "circuit_list_id", + "prefix" + ], + "isUnique": true + }, + "circuit_sections_list_category_group_unique": { + "name": "circuit_sections_list_category_group_unique", + "columns": [ + "circuit_list_id", + "category", + "group_number" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_sections_circuit_list_id_circuit_lists_id_fk": { + "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuit_sections", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuits": { + "name": "circuits", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cable_type": { + "name": "cable_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cable_cross_section": { + "name": "cable_cross_section", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cable_length": { + "name": "cable_length", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_assignment": { + "name": "rcd_assignment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_designation": { + "name": "terminal_designation", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voltage": { + "name": "voltage", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "control_requirement": { + "name": "control_requirement", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_reserve": { + "name": "is_reserve", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "circuits_list_equipment_identifier_unique": { + "name": "circuits_list_equipment_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuits_circuit_list_id_circuit_lists_id_fk": { + "name": "circuits_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuits", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuits_section_id_circuit_sections_id_fk": { + "name": "circuits_section_id_circuit_sections_id_fk", + "tableFrom": "circuits", + "tableTo": "circuit_sections", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_board_component_protection_devices": { + "name": "distribution_board_component_protection_devices", + "columns": { + "component_id": { + "name": "component_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rated_current_a": { + "name": "rated_current_a", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fuse_utilization_category": { + "name": "fuse_utilization_category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trip_characteristic": { + "name": "trip_characteristic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_type": { + "name": "rcd_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rated_residual_current_ma": { + "name": "rated_residual_current_ma", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk": { + "name": "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk", + "tableFrom": "distribution_board_component_protection_devices", + "tableTo": "distribution_board_components", + "columnsFrom": [ + "component_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_board_components": { + "name": "distribution_board_components", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "placement": { + "name": "placement", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "distribution_board_components_list_identifier_unique": { + "name": "distribution_board_components_list_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + }, + "distribution_board_components_section_role_unique": { + "name": "distribution_board_components_section_role_unique", + "columns": [ + "section_id", + "role" + ], + "isUnique": true + } + }, + "foreignKeys": { + "distribution_board_components_circuit_list_id_circuit_lists_id_fk": { + "name": "distribution_board_components_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "distribution_board_components", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "distribution_board_components_section_id_circuit_sections_id_fk": { + "name": "distribution_board_components_section_id_circuit_sections_id_fk", + "tableFrom": "distribution_board_components", + "tableTo": "circuit_sections", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_boards": { + "name": "distribution_boards", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "floor_id": { + "name": "floor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supply_type": { + "name": "supply_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "distribution_boards_project_id_projects_id_fk": { + "name": "distribution_boards_project_id_projects_id_fk", + "tableFrom": "distribution_boards", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "distribution_boards_floor_id_floors_id_fk": { + "name": "distribution_boards_floor_id_floors_id_fk", + "tableFrom": "distribution_boards", + "tableTo": "floors", + "columnsFrom": [ + "floor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "floors": { + "name": "floors", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "floors_project_id_projects_id_fk": { + "name": "floors_project_id_projects_id_fk", + "tableFrom": "floors", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "global_devices": { + "name": "global_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false }, "category": { "name": "category", @@ -93,40 +1076,87 @@ } }, "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_change_sets": { + "name": "project_change_sets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_revision_id": { + "name": "project_revision_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_type": { + "name": "command_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_schema_version": { + "name": "payload_schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forward_payload_json": { + "name": "forward_payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inverse_payload_json": { + "name": "inverse_payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_change_sets_revision_unique": { + "name": "project_change_sets_revision_unique", + "columns": [ + "project_revision_id" + ], + "isUnique": true + } + }, "foreignKeys": { - "consumers_project_id_projects_id_fk": { - "name": "consumers_project_id_projects_id_fk", - "tableFrom": "consumers", - "tableTo": "projects", + "project_change_sets_project_revision_id_project_revisions_id_fk": { + "name": "project_change_sets_project_revision_id_project_revisions_id_fk", + "tableFrom": "project_change_sets", + "tableTo": "project_revisions", "columnsFrom": [ - "project_id" + "project_revision_id" ], "columnsTo": [ "id" ], "onDelete": "cascade", "onUpdate": "no action" - }, - "consumers_distribution_board_id_distribution_boards_id_fk": { - "name": "consumers_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "consumers", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": {}, "checkConstraints": {} }, - "distribution_boards": { - "name": "distribution_boards", + "project_devices": { + "name": "project_devices", "columns": { "id": { "name": "id", @@ -148,13 +1178,383 @@ "primaryKey": false, "notNull": true, "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase_type": { + "name": "phase_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'single_phase'" + }, + "connection_kind": { + "name": "connection_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_group": { + "name": "cost_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "power_per_unit": { + "name": "power_per_unit", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "cos_phi": { + "name": "cos_phi", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voltage_v": { + "name": "voltage_v", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false } }, "indexes": {}, "foreignKeys": { - "distribution_boards_project_id_projects_id_fk": { - "name": "distribution_boards_project_id_projects_id_fk", - "tableFrom": "distribution_boards", + "project_devices_project_id_projects_id_fk": { + "name": "project_devices_project_id_projects_id_fk", + "tableFrom": "project_devices", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_history_stack_entries": { + "name": "project_history_stack_entries", + "columns": { + "change_set_id": { + "name": "change_set_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stack": { + "name": "stack", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_history_stack_entries_position_unique": { + "name": "project_history_stack_entries_position_unique", + "columns": [ + "project_id", + "stack", + "position" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { + "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", + "tableFrom": "project_history_stack_entries", + "tableTo": "project_change_sets", + "columnsFrom": [ + "change_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_history_stack_entries_project_id_projects_id_fk": { + "name": "project_history_stack_entries_project_id_projects_id_fk", + "tableFrom": "project_history_stack_entries", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "project_history_stack_entries_stack_check": { + "name": "project_history_stack_entries_stack_check", + "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" + } + } + }, + "project_revisions": { + "name": "project_revisions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "project_revisions_project_number_unique": { + "name": "project_revisions_project_number_unique", + "columns": [ + "project_id", + "revision_number" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_revisions_project_id_projects_id_fk": { + "name": "project_revisions_project_id_projects_id_fk", + "tableFrom": "project_revisions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_snapshots": { + "name": "project_snapshots", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_revision": { + "name": "source_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'named'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_sha256": { + "name": "payload_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "project_snapshots_project_created_idx": { + "name": "project_snapshots_project_created_idx", + "columns": [ + "project_id", + "created_at_iso" + ], + "isUnique": false + }, + "project_snapshots_project_kind_revision_idx": { + "name": "project_snapshots_project_kind_revision_idx", + "columns": [ + "project_id", + "kind", + "source_revision" + ], + "isUnique": false + }, + "project_snapshots_project_name_unique": { + "name": "project_snapshots_project_name_unique", + "columns": [ + "project_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_snapshots_project_id_projects_id_fk": { + "name": "project_snapshots_project_id_projects_id_fk", + "tableFrom": "project_snapshots", "tableTo": "projects", "columnsFrom": [ "project_id" @@ -186,6 +1586,66 @@ "primaryKey": false, "notNull": true, "autoincrement": false + }, + "internal_project_number": { + "name": "internal_project_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_project_number": { + "name": "external_project_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "building_owner": { + "name": "building_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "single_phase_voltage_v": { + "name": "single_phase_voltage_v", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 230 + }, + "three_phase_voltage_v": { + "name": "three_phase_voltage_v", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 400 + }, + "enabled_distribution_board_supply_types": { + "name": "enabled_distribution_board_supply_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[\"AV\",\"SV\",\"EV\",\"USV\",\"MSR\",\"SiBe\"]'" + }, + "current_revision": { + "name": "current_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 } }, "indexes": {}, @@ -193,6 +1653,78 @@ "compositePrimaryKeys": {}, "uniqueConstraints": {}, "checkConstraints": {} + }, + "rooms": { + "name": "rooms", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "floor_id": { + "name": "floor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_number": { + "name": "room_number", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "room_name": { + "name": "room_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "rooms_project_id_projects_id_fk": { + "name": "rooms_project_id_projects_id_fk", + "tableFrom": "rooms", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rooms_floor_id_floors_id_fk": { + "name": "rooms_floor_id_floors_id_fk", + "tableFrom": "rooms", + "tableTo": "floors", + "columnsFrom": [ + "floor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} } }, "views": {}, diff --git a/src/db/migrations/meta/0011_snapshot.json b/src/db/migrations/meta/0011_snapshot.json deleted file mode 100644 index e9d5d3a..0000000 --- a/src/db/migrations/meta/0011_snapshot.json +++ /dev/null @@ -1,1450 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "863215c4-ed6e-4ec5-8c48-161054031da2", - "prevId": "d219f155-9dd0-48e3-8fd2-8278f1f788ca", - "tables": { - "circuit_device_rows": { - "name": "circuit_device_rows", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "linked_project_device_id": { - "name": "linked_project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "legacy_consumer_id": { - "name": "legacy_consumer_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "level": { - "name": "level", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number_snapshot": { - "name": "room_number_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_name_snapshot": { - "name": "room_name_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "overridden_fields": { - "name": "overridden_fields", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "circuit_device_rows_circuit_id_circuits_id_fk": { - "name": "circuit_device_rows_circuit_id_circuits_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { - "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "project_devices", - "columnsFrom": [ - "linked_project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "circuit_device_rows_room_id_rooms_id_fk": { - "name": "circuit_device_rows_room_id_rooms_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_lists": { - "name": "circuit_lists", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_lists_distribution_board_id_unique": { - "name": "circuit_lists_distribution_board_id_unique", - "columns": [ - "distribution_board_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_lists_project_id_projects_id_fk": { - "name": "circuit_lists_project_id_projects_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_lists_distribution_board_id_distribution_boards_id_fk": { - "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_sections": { - "name": "circuit_sections", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "prefix": { - "name": "prefix", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": { - "circuit_sections_list_key_unique": { - "name": "circuit_sections_list_key_unique", - "columns": [ - "circuit_list_id", - "key" - ], - "isUnique": true - }, - "circuit_sections_list_prefix_unique": { - "name": "circuit_sections_list_prefix_unique", - "columns": [ - "circuit_list_id", - "prefix" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_sections_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_sections", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuits": { - "name": "circuits", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_length": { - "name": "cable_length", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_assignment": { - "name": "rcd_assignment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "terminal_designation": { - "name": "terminal_designation", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage": { - "name": "voltage", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "control_requirement": { - "name": "control_requirement", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_reserve": { - "name": "is_reserve", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuits_list_equipment_identifier_unique": { - "name": "circuits_list_equipment_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuits_circuit_list_id_circuit_lists_id_fk": { - "name": "circuits_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuits_section_id_circuit_sections_id_fk": { - "name": "circuits_section_id_circuit_sections_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_sections", - "columnsFrom": [ - "section_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "consumers": { - "name": "consumers", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "project_device_id": { - "name": "project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_linked_to_device": { - "name": "is_linked_to_device", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_number": { - "name": "circuit_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "device_type": { - "name": "device_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trade_or_cost_group": { - "name": "trade_or_cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "group_name": { - "name": "group_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "comment": { - "name": "comment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "consumers_project_id_projects_id_fk": { - "name": "consumers_project_id_projects_id_fk", - "tableFrom": "consumers", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "consumers_distribution_board_id_distribution_boards_id_fk": { - "name": "consumers_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "consumers", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_circuit_list_id_circuit_lists_id_fk": { - "name": "consumers_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "consumers", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_project_device_id_project_devices_id_fk": { - "name": "consumers_project_device_id_project_devices_id_fk", - "tableFrom": "consumers", - "tableTo": "project_devices", - "columnsFrom": [ - "project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_room_id_rooms_id_fk": { - "name": "consumers_room_id_rooms_id_fk", - "tableFrom": "consumers", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_boards": { - "name": "distribution_boards", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_boards_project_id_projects_id_fk": { - "name": "distribution_boards_project_id_projects_id_fk", - "tableFrom": "distribution_boards", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "floors": { - "name": "floors", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "floors_project_id_projects_id_fk": { - "name": "floors_project_id_projects_id_fk", - "tableFrom": "floors", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "global_devices": { - "name": "global_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_circuit_migrations": { - "name": "legacy_consumer_circuit_migrations", - "columns": { - "consumer_id": { - "name": "consumer_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_device_row_id": { - "name": "circuit_device_row_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuit_device_rows", - "columnsFrom": [ - "circuit_device_row_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_migration_reports": { - "name": "legacy_consumer_migration_reports", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "legacy_consumer_count": { - "name": "legacy_consumer_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_circuit_count": { - "name": "created_circuit_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_device_row_count": { - "name": "created_device_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_grouped_count": { - "name": "duplicate_grouped_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifier_count": { - "name": "generated_identifier_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "unassigned_row_count": { - "name": "unassigned_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "warnings_json": { - "name": "warnings_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifiers_json": { - "name": "generated_identifiers_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_groups_json": { - "name": "duplicate_groups_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "legacy_consumer_migration_reports_list_unique": { - "name": "legacy_consumer_migration_reports_list_unique", - "columns": [ - "circuit_list_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_migration_reports", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_devices": { - "name": "project_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'single_phase'" - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "project_devices_project_id_projects_id_fk": { - "name": "project_devices_project_id_projects_id_fk", - "tableFrom": "project_devices", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "projects": { - "name": "projects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "single_phase_voltage_v": { - "name": "single_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 230 - }, - "three_phase_voltage_v": { - "name": "three_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 400 - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "rooms": { - "name": "rooms", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number": { - "name": "room_number", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "room_name": { - "name": "room_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "rooms_project_id_projects_id_fk": { - "name": "rooms_project_id_projects_id_fk", - "tableFrom": "rooms", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "rooms_floor_id_floors_id_fk": { - "name": "rooms_floor_id_floors_id_fk", - "tableFrom": "rooms", - "tableTo": "floors", - "columnsFrom": [ - "floor_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/src/db/migrations/meta/0012_snapshot.json b/src/db/migrations/meta/0012_snapshot.json deleted file mode 100644 index 8d689f0..0000000 --- a/src/db/migrations/meta/0012_snapshot.json +++ /dev/null @@ -1,1614 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "3f69cb15-3079-4bf5-96f7-3af62c8ac7e1", - "prevId": "863215c4-ed6e-4ec5-8c48-161054031da2", - "tables": { - "circuit_device_rows": { - "name": "circuit_device_rows", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "linked_project_device_id": { - "name": "linked_project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "legacy_consumer_id": { - "name": "legacy_consumer_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "level": { - "name": "level", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number_snapshot": { - "name": "room_number_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_name_snapshot": { - "name": "room_name_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "overridden_fields": { - "name": "overridden_fields", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "circuit_device_rows_circuit_id_circuits_id_fk": { - "name": "circuit_device_rows_circuit_id_circuits_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { - "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "project_devices", - "columnsFrom": [ - "linked_project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "circuit_device_rows_room_id_rooms_id_fk": { - "name": "circuit_device_rows_room_id_rooms_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_lists": { - "name": "circuit_lists", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_lists_distribution_board_id_unique": { - "name": "circuit_lists_distribution_board_id_unique", - "columns": [ - "distribution_board_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_lists_project_id_projects_id_fk": { - "name": "circuit_lists_project_id_projects_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_lists_distribution_board_id_distribution_boards_id_fk": { - "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_sections": { - "name": "circuit_sections", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "prefix": { - "name": "prefix", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": { - "circuit_sections_list_key_unique": { - "name": "circuit_sections_list_key_unique", - "columns": [ - "circuit_list_id", - "key" - ], - "isUnique": true - }, - "circuit_sections_list_prefix_unique": { - "name": "circuit_sections_list_prefix_unique", - "columns": [ - "circuit_list_id", - "prefix" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_sections_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_sections", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuits": { - "name": "circuits", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_length": { - "name": "cable_length", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_assignment": { - "name": "rcd_assignment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "terminal_designation": { - "name": "terminal_designation", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage": { - "name": "voltage", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "control_requirement": { - "name": "control_requirement", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_reserve": { - "name": "is_reserve", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuits_list_equipment_identifier_unique": { - "name": "circuits_list_equipment_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuits_circuit_list_id_circuit_lists_id_fk": { - "name": "circuits_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuits_section_id_circuit_sections_id_fk": { - "name": "circuits_section_id_circuit_sections_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_sections", - "columnsFrom": [ - "section_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "consumers": { - "name": "consumers", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "project_device_id": { - "name": "project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_linked_to_device": { - "name": "is_linked_to_device", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_number": { - "name": "circuit_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "device_type": { - "name": "device_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trade_or_cost_group": { - "name": "trade_or_cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "group_name": { - "name": "group_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "comment": { - "name": "comment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "consumers_project_id_projects_id_fk": { - "name": "consumers_project_id_projects_id_fk", - "tableFrom": "consumers", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "consumers_distribution_board_id_distribution_boards_id_fk": { - "name": "consumers_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "consumers", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_circuit_list_id_circuit_lists_id_fk": { - "name": "consumers_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "consumers", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_project_device_id_project_devices_id_fk": { - "name": "consumers_project_device_id_project_devices_id_fk", - "tableFrom": "consumers", - "tableTo": "project_devices", - "columnsFrom": [ - "project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_room_id_rooms_id_fk": { - "name": "consumers_room_id_rooms_id_fk", - "tableFrom": "consumers", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_boards": { - "name": "distribution_boards", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_boards_project_id_projects_id_fk": { - "name": "distribution_boards_project_id_projects_id_fk", - "tableFrom": "distribution_boards", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "floors": { - "name": "floors", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "floors_project_id_projects_id_fk": { - "name": "floors_project_id_projects_id_fk", - "tableFrom": "floors", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "global_devices": { - "name": "global_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_circuit_migrations": { - "name": "legacy_consumer_circuit_migrations", - "columns": { - "consumer_id": { - "name": "consumer_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_device_row_id": { - "name": "circuit_device_row_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuit_device_rows", - "columnsFrom": [ - "circuit_device_row_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_migration_reports": { - "name": "legacy_consumer_migration_reports", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "legacy_consumer_count": { - "name": "legacy_consumer_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_circuit_count": { - "name": "created_circuit_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_device_row_count": { - "name": "created_device_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_grouped_count": { - "name": "duplicate_grouped_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifier_count": { - "name": "generated_identifier_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "unassigned_row_count": { - "name": "unassigned_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "warnings_json": { - "name": "warnings_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifiers_json": { - "name": "generated_identifiers_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_groups_json": { - "name": "duplicate_groups_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "legacy_consumer_migration_reports_list_unique": { - "name": "legacy_consumer_migration_reports_list_unique", - "columns": [ - "circuit_list_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_migration_reports", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_change_sets": { - "name": "project_change_sets", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_revision_id": { - "name": "project_revision_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "command_type": { - "name": "command_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_schema_version": { - "name": "payload_schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "forward_payload_json": { - "name": "forward_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "inverse_payload_json": { - "name": "inverse_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_change_sets_revision_unique": { - "name": "project_change_sets_revision_unique", - "columns": [ - "project_revision_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_change_sets_project_revision_id_project_revisions_id_fk": { - "name": "project_change_sets_project_revision_id_project_revisions_id_fk", - "tableFrom": "project_change_sets", - "tableTo": "project_revisions", - "columnsFrom": [ - "project_revision_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_devices": { - "name": "project_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'single_phase'" - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "project_devices_project_id_projects_id_fk": { - "name": "project_devices_project_id_projects_id_fk", - "tableFrom": "project_devices", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_revisions": { - "name": "project_revisions", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "revision_number": { - "name": "revision_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "actor_id": { - "name": "actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "source": { - "name": "source", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_revisions_project_number_unique": { - "name": "project_revisions_project_number_unique", - "columns": [ - "project_id", - "revision_number" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_revisions_project_id_projects_id_fk": { - "name": "project_revisions_project_id_projects_id_fk", - "tableFrom": "project_revisions", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "projects": { - "name": "projects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "single_phase_voltage_v": { - "name": "single_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 230 - }, - "three_phase_voltage_v": { - "name": "three_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 400 - }, - "current_revision": { - "name": "current_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "rooms": { - "name": "rooms", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number": { - "name": "room_number", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "room_name": { - "name": "room_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "rooms_project_id_projects_id_fk": { - "name": "rooms_project_id_projects_id_fk", - "tableFrom": "rooms", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "rooms_floor_id_floors_id_fk": { - "name": "rooms_floor_id_floors_id_fk", - "tableFrom": "rooms", - "tableTo": "floors", - "columnsFrom": [ - "floor_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/src/db/migrations/meta/0013_snapshot.json b/src/db/migrations/meta/0013_snapshot.json deleted file mode 100644 index 9b28c31..0000000 --- a/src/db/migrations/meta/0013_snapshot.json +++ /dev/null @@ -1,1694 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "b4029fef-ed7b-47ed-a185-d151ad3f46da", - "prevId": "3f69cb15-3079-4bf5-96f7-3af62c8ac7e1", - "tables": { - "circuit_device_rows": { - "name": "circuit_device_rows", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "linked_project_device_id": { - "name": "linked_project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "legacy_consumer_id": { - "name": "legacy_consumer_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "level": { - "name": "level", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number_snapshot": { - "name": "room_number_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_name_snapshot": { - "name": "room_name_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "overridden_fields": { - "name": "overridden_fields", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "circuit_device_rows_circuit_id_circuits_id_fk": { - "name": "circuit_device_rows_circuit_id_circuits_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { - "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "project_devices", - "columnsFrom": [ - "linked_project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "circuit_device_rows_room_id_rooms_id_fk": { - "name": "circuit_device_rows_room_id_rooms_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_lists": { - "name": "circuit_lists", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_lists_distribution_board_id_unique": { - "name": "circuit_lists_distribution_board_id_unique", - "columns": [ - "distribution_board_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_lists_project_id_projects_id_fk": { - "name": "circuit_lists_project_id_projects_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_lists_distribution_board_id_distribution_boards_id_fk": { - "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_sections": { - "name": "circuit_sections", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "prefix": { - "name": "prefix", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": { - "circuit_sections_list_key_unique": { - "name": "circuit_sections_list_key_unique", - "columns": [ - "circuit_list_id", - "key" - ], - "isUnique": true - }, - "circuit_sections_list_prefix_unique": { - "name": "circuit_sections_list_prefix_unique", - "columns": [ - "circuit_list_id", - "prefix" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_sections_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_sections", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuits": { - "name": "circuits", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_length": { - "name": "cable_length", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_assignment": { - "name": "rcd_assignment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "terminal_designation": { - "name": "terminal_designation", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage": { - "name": "voltage", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "control_requirement": { - "name": "control_requirement", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_reserve": { - "name": "is_reserve", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuits_list_equipment_identifier_unique": { - "name": "circuits_list_equipment_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuits_circuit_list_id_circuit_lists_id_fk": { - "name": "circuits_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuits_section_id_circuit_sections_id_fk": { - "name": "circuits_section_id_circuit_sections_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_sections", - "columnsFrom": [ - "section_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "consumers": { - "name": "consumers", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "project_device_id": { - "name": "project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_linked_to_device": { - "name": "is_linked_to_device", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_number": { - "name": "circuit_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "device_type": { - "name": "device_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trade_or_cost_group": { - "name": "trade_or_cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "group_name": { - "name": "group_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "comment": { - "name": "comment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "consumers_project_id_projects_id_fk": { - "name": "consumers_project_id_projects_id_fk", - "tableFrom": "consumers", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "consumers_distribution_board_id_distribution_boards_id_fk": { - "name": "consumers_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "consumers", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_circuit_list_id_circuit_lists_id_fk": { - "name": "consumers_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "consumers", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_project_device_id_project_devices_id_fk": { - "name": "consumers_project_device_id_project_devices_id_fk", - "tableFrom": "consumers", - "tableTo": "project_devices", - "columnsFrom": [ - "project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_room_id_rooms_id_fk": { - "name": "consumers_room_id_rooms_id_fk", - "tableFrom": "consumers", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_boards": { - "name": "distribution_boards", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_boards_project_id_projects_id_fk": { - "name": "distribution_boards_project_id_projects_id_fk", - "tableFrom": "distribution_boards", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "floors": { - "name": "floors", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "floors_project_id_projects_id_fk": { - "name": "floors_project_id_projects_id_fk", - "tableFrom": "floors", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "global_devices": { - "name": "global_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_circuit_migrations": { - "name": "legacy_consumer_circuit_migrations", - "columns": { - "consumer_id": { - "name": "consumer_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_device_row_id": { - "name": "circuit_device_row_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuit_device_rows", - "columnsFrom": [ - "circuit_device_row_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_migration_reports": { - "name": "legacy_consumer_migration_reports", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "legacy_consumer_count": { - "name": "legacy_consumer_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_circuit_count": { - "name": "created_circuit_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_device_row_count": { - "name": "created_device_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_grouped_count": { - "name": "duplicate_grouped_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifier_count": { - "name": "generated_identifier_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "unassigned_row_count": { - "name": "unassigned_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "warnings_json": { - "name": "warnings_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifiers_json": { - "name": "generated_identifiers_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_groups_json": { - "name": "duplicate_groups_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "legacy_consumer_migration_reports_list_unique": { - "name": "legacy_consumer_migration_reports_list_unique", - "columns": [ - "circuit_list_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_migration_reports", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_change_sets": { - "name": "project_change_sets", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_revision_id": { - "name": "project_revision_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "command_type": { - "name": "command_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_schema_version": { - "name": "payload_schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "forward_payload_json": { - "name": "forward_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "inverse_payload_json": { - "name": "inverse_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_change_sets_revision_unique": { - "name": "project_change_sets_revision_unique", - "columns": [ - "project_revision_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_change_sets_project_revision_id_project_revisions_id_fk": { - "name": "project_change_sets_project_revision_id_project_revisions_id_fk", - "tableFrom": "project_change_sets", - "tableTo": "project_revisions", - "columnsFrom": [ - "project_revision_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_devices": { - "name": "project_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'single_phase'" - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "project_devices_project_id_projects_id_fk": { - "name": "project_devices_project_id_projects_id_fk", - "tableFrom": "project_devices", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_history_stack_entries": { - "name": "project_history_stack_entries", - "columns": { - "change_set_id": { - "name": "change_set_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "stack": { - "name": "stack", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "position": { - "name": "position", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_history_stack_entries_position_unique": { - "name": "project_history_stack_entries_position_unique", - "columns": [ - "project_id", - "stack", - "position" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { - "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "project_change_sets", - "columnsFrom": [ - "change_set_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "project_history_stack_entries_project_id_projects_id_fk": { - "name": "project_history_stack_entries_project_id_projects_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": { - "project_history_stack_entries_stack_check": { - "name": "project_history_stack_entries_stack_check", - "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" - } - } - }, - "project_revisions": { - "name": "project_revisions", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "revision_number": { - "name": "revision_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "actor_id": { - "name": "actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "source": { - "name": "source", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_revisions_project_number_unique": { - "name": "project_revisions_project_number_unique", - "columns": [ - "project_id", - "revision_number" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_revisions_project_id_projects_id_fk": { - "name": "project_revisions_project_id_projects_id_fk", - "tableFrom": "project_revisions", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "projects": { - "name": "projects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "single_phase_voltage_v": { - "name": "single_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 230 - }, - "three_phase_voltage_v": { - "name": "three_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 400 - }, - "current_revision": { - "name": "current_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "rooms": { - "name": "rooms", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number": { - "name": "room_number", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "room_name": { - "name": "room_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "rooms_project_id_projects_id_fk": { - "name": "rooms_project_id_projects_id_fk", - "tableFrom": "rooms", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "rooms_floor_id_floors_id_fk": { - "name": "rooms_floor_id_floors_id_fk", - "tableFrom": "rooms", - "tableTo": "floors", - "columnsFrom": [ - "floor_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/src/db/migrations/meta/0014_snapshot.json b/src/db/migrations/meta/0014_snapshot.json deleted file mode 100644 index 783e5ad..0000000 --- a/src/db/migrations/meta/0014_snapshot.json +++ /dev/null @@ -1,1805 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "bd9bc99c-0cc0-44e3-bff0-819b1cbbf8ac", - "prevId": "b4029fef-ed7b-47ed-a185-d151ad3f46da", - "tables": { - "circuit_device_rows": { - "name": "circuit_device_rows", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "linked_project_device_id": { - "name": "linked_project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "legacy_consumer_id": { - "name": "legacy_consumer_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "level": { - "name": "level", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number_snapshot": { - "name": "room_number_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_name_snapshot": { - "name": "room_name_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "overridden_fields": { - "name": "overridden_fields", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "circuit_device_rows_circuit_id_circuits_id_fk": { - "name": "circuit_device_rows_circuit_id_circuits_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { - "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "project_devices", - "columnsFrom": [ - "linked_project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "circuit_device_rows_room_id_rooms_id_fk": { - "name": "circuit_device_rows_room_id_rooms_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_lists": { - "name": "circuit_lists", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_lists_distribution_board_id_unique": { - "name": "circuit_lists_distribution_board_id_unique", - "columns": [ - "distribution_board_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_lists_project_id_projects_id_fk": { - "name": "circuit_lists_project_id_projects_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_lists_distribution_board_id_distribution_boards_id_fk": { - "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_sections": { - "name": "circuit_sections", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "prefix": { - "name": "prefix", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": { - "circuit_sections_list_key_unique": { - "name": "circuit_sections_list_key_unique", - "columns": [ - "circuit_list_id", - "key" - ], - "isUnique": true - }, - "circuit_sections_list_prefix_unique": { - "name": "circuit_sections_list_prefix_unique", - "columns": [ - "circuit_list_id", - "prefix" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_sections_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_sections", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuits": { - "name": "circuits", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_length": { - "name": "cable_length", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_assignment": { - "name": "rcd_assignment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "terminal_designation": { - "name": "terminal_designation", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage": { - "name": "voltage", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "control_requirement": { - "name": "control_requirement", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_reserve": { - "name": "is_reserve", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuits_list_equipment_identifier_unique": { - "name": "circuits_list_equipment_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuits_circuit_list_id_circuit_lists_id_fk": { - "name": "circuits_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuits_section_id_circuit_sections_id_fk": { - "name": "circuits_section_id_circuit_sections_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_sections", - "columnsFrom": [ - "section_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "consumers": { - "name": "consumers", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "project_device_id": { - "name": "project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_linked_to_device": { - "name": "is_linked_to_device", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_number": { - "name": "circuit_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "device_type": { - "name": "device_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trade_or_cost_group": { - "name": "trade_or_cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "group_name": { - "name": "group_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "comment": { - "name": "comment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "consumers_project_id_projects_id_fk": { - "name": "consumers_project_id_projects_id_fk", - "tableFrom": "consumers", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "consumers_distribution_board_id_distribution_boards_id_fk": { - "name": "consumers_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "consumers", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_circuit_list_id_circuit_lists_id_fk": { - "name": "consumers_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "consumers", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_project_device_id_project_devices_id_fk": { - "name": "consumers_project_device_id_project_devices_id_fk", - "tableFrom": "consumers", - "tableTo": "project_devices", - "columnsFrom": [ - "project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_room_id_rooms_id_fk": { - "name": "consumers_room_id_rooms_id_fk", - "tableFrom": "consumers", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_boards": { - "name": "distribution_boards", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_boards_project_id_projects_id_fk": { - "name": "distribution_boards_project_id_projects_id_fk", - "tableFrom": "distribution_boards", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "floors": { - "name": "floors", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "floors_project_id_projects_id_fk": { - "name": "floors_project_id_projects_id_fk", - "tableFrom": "floors", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "global_devices": { - "name": "global_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_circuit_migrations": { - "name": "legacy_consumer_circuit_migrations", - "columns": { - "consumer_id": { - "name": "consumer_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_device_row_id": { - "name": "circuit_device_row_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuit_device_rows", - "columnsFrom": [ - "circuit_device_row_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_migration_reports": { - "name": "legacy_consumer_migration_reports", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "legacy_consumer_count": { - "name": "legacy_consumer_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_circuit_count": { - "name": "created_circuit_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_device_row_count": { - "name": "created_device_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_grouped_count": { - "name": "duplicate_grouped_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifier_count": { - "name": "generated_identifier_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "unassigned_row_count": { - "name": "unassigned_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "warnings_json": { - "name": "warnings_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifiers_json": { - "name": "generated_identifiers_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_groups_json": { - "name": "duplicate_groups_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "legacy_consumer_migration_reports_list_unique": { - "name": "legacy_consumer_migration_reports_list_unique", - "columns": [ - "circuit_list_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_migration_reports", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_change_sets": { - "name": "project_change_sets", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_revision_id": { - "name": "project_revision_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "command_type": { - "name": "command_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_schema_version": { - "name": "payload_schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "forward_payload_json": { - "name": "forward_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "inverse_payload_json": { - "name": "inverse_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_change_sets_revision_unique": { - "name": "project_change_sets_revision_unique", - "columns": [ - "project_revision_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_change_sets_project_revision_id_project_revisions_id_fk": { - "name": "project_change_sets_project_revision_id_project_revisions_id_fk", - "tableFrom": "project_change_sets", - "tableTo": "project_revisions", - "columnsFrom": [ - "project_revision_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_devices": { - "name": "project_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'single_phase'" - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "project_devices_project_id_projects_id_fk": { - "name": "project_devices_project_id_projects_id_fk", - "tableFrom": "project_devices", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_history_stack_entries": { - "name": "project_history_stack_entries", - "columns": { - "change_set_id": { - "name": "change_set_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "stack": { - "name": "stack", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "position": { - "name": "position", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_history_stack_entries_position_unique": { - "name": "project_history_stack_entries_position_unique", - "columns": [ - "project_id", - "stack", - "position" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { - "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "project_change_sets", - "columnsFrom": [ - "change_set_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "project_history_stack_entries_project_id_projects_id_fk": { - "name": "project_history_stack_entries_project_id_projects_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": { - "project_history_stack_entries_stack_check": { - "name": "project_history_stack_entries_stack_check", - "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" - } - } - }, - "project_revisions": { - "name": "project_revisions", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "revision_number": { - "name": "revision_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "actor_id": { - "name": "actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "source": { - "name": "source", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_revisions_project_number_unique": { - "name": "project_revisions_project_number_unique", - "columns": [ - "project_id", - "revision_number" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_revisions_project_id_projects_id_fk": { - "name": "project_revisions_project_id_projects_id_fk", - "tableFrom": "project_revisions", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_snapshots": { - "name": "project_snapshots", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_revision": { - "name": "source_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "schema_version": { - "name": "schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "payload_json": { - "name": "payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_sha256": { - "name": "payload_sha256", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_by_actor_id": { - "name": "created_by_actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_snapshots_project_created_idx": { - "name": "project_snapshots_project_created_idx", - "columns": [ - "project_id", - "created_at_iso" - ], - "isUnique": false - }, - "project_snapshots_project_name_unique": { - "name": "project_snapshots_project_name_unique", - "columns": [ - "project_id", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_snapshots_project_id_projects_id_fk": { - "name": "project_snapshots_project_id_projects_id_fk", - "tableFrom": "project_snapshots", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "projects": { - "name": "projects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "single_phase_voltage_v": { - "name": "single_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 230 - }, - "three_phase_voltage_v": { - "name": "three_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 400 - }, - "current_revision": { - "name": "current_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "rooms": { - "name": "rooms", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number": { - "name": "room_number", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "room_name": { - "name": "room_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "rooms_project_id_projects_id_fk": { - "name": "rooms_project_id_projects_id_fk", - "tableFrom": "rooms", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "rooms_floor_id_floors_id_fk": { - "name": "rooms_floor_id_floors_id_fk", - "tableFrom": "rooms", - "tableTo": "floors", - "columnsFrom": [ - "floor_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/src/db/migrations/meta/0015_snapshot.json b/src/db/migrations/meta/0015_snapshot.json deleted file mode 100644 index e5ac2bf..0000000 --- a/src/db/migrations/meta/0015_snapshot.json +++ /dev/null @@ -1,1822 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "3248cb0b-5512-43d3-8193-e7218cb8ae66", - "prevId": "bd9bc99c-0cc0-44e3-bff0-819b1cbbf8ac", - "tables": { - "circuit_device_rows": { - "name": "circuit_device_rows", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "linked_project_device_id": { - "name": "linked_project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "legacy_consumer_id": { - "name": "legacy_consumer_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "level": { - "name": "level", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number_snapshot": { - "name": "room_number_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_name_snapshot": { - "name": "room_name_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "overridden_fields": { - "name": "overridden_fields", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "circuit_device_rows_circuit_id_circuits_id_fk": { - "name": "circuit_device_rows_circuit_id_circuits_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { - "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "project_devices", - "columnsFrom": [ - "linked_project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "circuit_device_rows_room_id_rooms_id_fk": { - "name": "circuit_device_rows_room_id_rooms_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_lists": { - "name": "circuit_lists", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_lists_distribution_board_id_unique": { - "name": "circuit_lists_distribution_board_id_unique", - "columns": [ - "distribution_board_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_lists_project_id_projects_id_fk": { - "name": "circuit_lists_project_id_projects_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_lists_distribution_board_id_distribution_boards_id_fk": { - "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_sections": { - "name": "circuit_sections", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "prefix": { - "name": "prefix", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": { - "circuit_sections_list_key_unique": { - "name": "circuit_sections_list_key_unique", - "columns": [ - "circuit_list_id", - "key" - ], - "isUnique": true - }, - "circuit_sections_list_prefix_unique": { - "name": "circuit_sections_list_prefix_unique", - "columns": [ - "circuit_list_id", - "prefix" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_sections_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_sections", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuits": { - "name": "circuits", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_length": { - "name": "cable_length", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_assignment": { - "name": "rcd_assignment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "terminal_designation": { - "name": "terminal_designation", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage": { - "name": "voltage", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "control_requirement": { - "name": "control_requirement", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_reserve": { - "name": "is_reserve", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuits_list_equipment_identifier_unique": { - "name": "circuits_list_equipment_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuits_circuit_list_id_circuit_lists_id_fk": { - "name": "circuits_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuits_section_id_circuit_sections_id_fk": { - "name": "circuits_section_id_circuit_sections_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_sections", - "columnsFrom": [ - "section_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "consumers": { - "name": "consumers", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "project_device_id": { - "name": "project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_linked_to_device": { - "name": "is_linked_to_device", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_number": { - "name": "circuit_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "device_type": { - "name": "device_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trade_or_cost_group": { - "name": "trade_or_cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "group_name": { - "name": "group_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "comment": { - "name": "comment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "consumers_project_id_projects_id_fk": { - "name": "consumers_project_id_projects_id_fk", - "tableFrom": "consumers", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "consumers_distribution_board_id_distribution_boards_id_fk": { - "name": "consumers_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "consumers", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_circuit_list_id_circuit_lists_id_fk": { - "name": "consumers_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "consumers", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_project_device_id_project_devices_id_fk": { - "name": "consumers_project_device_id_project_devices_id_fk", - "tableFrom": "consumers", - "tableTo": "project_devices", - "columnsFrom": [ - "project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_room_id_rooms_id_fk": { - "name": "consumers_room_id_rooms_id_fk", - "tableFrom": "consumers", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_boards": { - "name": "distribution_boards", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_boards_project_id_projects_id_fk": { - "name": "distribution_boards_project_id_projects_id_fk", - "tableFrom": "distribution_boards", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "floors": { - "name": "floors", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "floors_project_id_projects_id_fk": { - "name": "floors_project_id_projects_id_fk", - "tableFrom": "floors", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "global_devices": { - "name": "global_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_circuit_migrations": { - "name": "legacy_consumer_circuit_migrations", - "columns": { - "consumer_id": { - "name": "consumer_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_device_row_id": { - "name": "circuit_device_row_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuit_device_rows", - "columnsFrom": [ - "circuit_device_row_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_migration_reports": { - "name": "legacy_consumer_migration_reports", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "legacy_consumer_count": { - "name": "legacy_consumer_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_circuit_count": { - "name": "created_circuit_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_device_row_count": { - "name": "created_device_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_grouped_count": { - "name": "duplicate_grouped_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifier_count": { - "name": "generated_identifier_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "unassigned_row_count": { - "name": "unassigned_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "warnings_json": { - "name": "warnings_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifiers_json": { - "name": "generated_identifiers_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_groups_json": { - "name": "duplicate_groups_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "legacy_consumer_migration_reports_list_unique": { - "name": "legacy_consumer_migration_reports_list_unique", - "columns": [ - "circuit_list_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_migration_reports", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_change_sets": { - "name": "project_change_sets", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_revision_id": { - "name": "project_revision_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "command_type": { - "name": "command_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_schema_version": { - "name": "payload_schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "forward_payload_json": { - "name": "forward_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "inverse_payload_json": { - "name": "inverse_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_change_sets_revision_unique": { - "name": "project_change_sets_revision_unique", - "columns": [ - "project_revision_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_change_sets_project_revision_id_project_revisions_id_fk": { - "name": "project_change_sets_project_revision_id_project_revisions_id_fk", - "tableFrom": "project_change_sets", - "tableTo": "project_revisions", - "columnsFrom": [ - "project_revision_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_devices": { - "name": "project_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'single_phase'" - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "project_devices_project_id_projects_id_fk": { - "name": "project_devices_project_id_projects_id_fk", - "tableFrom": "project_devices", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_history_stack_entries": { - "name": "project_history_stack_entries", - "columns": { - "change_set_id": { - "name": "change_set_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "stack": { - "name": "stack", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "position": { - "name": "position", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_history_stack_entries_position_unique": { - "name": "project_history_stack_entries_position_unique", - "columns": [ - "project_id", - "stack", - "position" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { - "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "project_change_sets", - "columnsFrom": [ - "change_set_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "project_history_stack_entries_project_id_projects_id_fk": { - "name": "project_history_stack_entries_project_id_projects_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": { - "project_history_stack_entries_stack_check": { - "name": "project_history_stack_entries_stack_check", - "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" - } - } - }, - "project_revisions": { - "name": "project_revisions", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "revision_number": { - "name": "revision_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "actor_id": { - "name": "actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "source": { - "name": "source", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_revisions_project_number_unique": { - "name": "project_revisions_project_number_unique", - "columns": [ - "project_id", - "revision_number" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_revisions_project_id_projects_id_fk": { - "name": "project_revisions_project_id_projects_id_fk", - "tableFrom": "project_revisions", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_snapshots": { - "name": "project_snapshots", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_revision": { - "name": "source_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "schema_version": { - "name": "schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "kind": { - "name": "kind", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'named'" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "payload_json": { - "name": "payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_sha256": { - "name": "payload_sha256", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_by_actor_id": { - "name": "created_by_actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_snapshots_project_created_idx": { - "name": "project_snapshots_project_created_idx", - "columns": [ - "project_id", - "created_at_iso" - ], - "isUnique": false - }, - "project_snapshots_project_kind_revision_idx": { - "name": "project_snapshots_project_kind_revision_idx", - "columns": [ - "project_id", - "kind", - "source_revision" - ], - "isUnique": false - }, - "project_snapshots_project_name_unique": { - "name": "project_snapshots_project_name_unique", - "columns": [ - "project_id", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_snapshots_project_id_projects_id_fk": { - "name": "project_snapshots_project_id_projects_id_fk", - "tableFrom": "project_snapshots", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "projects": { - "name": "projects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "single_phase_voltage_v": { - "name": "single_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 230 - }, - "three_phase_voltage_v": { - "name": "three_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 400 - }, - "current_revision": { - "name": "current_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "rooms": { - "name": "rooms", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number": { - "name": "room_number", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "room_name": { - "name": "room_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "rooms_project_id_projects_id_fk": { - "name": "rooms_project_id_projects_id_fk", - "tableFrom": "rooms", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "rooms_floor_id_floors_id_fk": { - "name": "rooms_floor_id_floors_id_fk", - "tableFrom": "rooms", - "tableTo": "floors", - "columnsFrom": [ - "floor_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/src/db/migrations/meta/0016_snapshot.json b/src/db/migrations/meta/0016_snapshot.json deleted file mode 100644 index 1e43d03..0000000 --- a/src/db/migrations/meta/0016_snapshot.json +++ /dev/null @@ -1,1850 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "8502c7d2-e925-42c3-8044-3e965c170f97", - "prevId": "3248cb0b-5512-43d3-8193-e7218cb8ae66", - "tables": { - "circuit_device_rows": { - "name": "circuit_device_rows", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "linked_project_device_id": { - "name": "linked_project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "legacy_consumer_id": { - "name": "legacy_consumer_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "level": { - "name": "level", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number_snapshot": { - "name": "room_number_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_name_snapshot": { - "name": "room_name_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "overridden_fields": { - "name": "overridden_fields", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "circuit_device_rows_circuit_id_circuits_id_fk": { - "name": "circuit_device_rows_circuit_id_circuits_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { - "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "project_devices", - "columnsFrom": [ - "linked_project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "circuit_device_rows_room_id_rooms_id_fk": { - "name": "circuit_device_rows_room_id_rooms_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_lists": { - "name": "circuit_lists", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_lists_distribution_board_id_unique": { - "name": "circuit_lists_distribution_board_id_unique", - "columns": [ - "distribution_board_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_lists_project_id_projects_id_fk": { - "name": "circuit_lists_project_id_projects_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_lists_distribution_board_id_distribution_boards_id_fk": { - "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_sections": { - "name": "circuit_sections", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "prefix": { - "name": "prefix", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": { - "circuit_sections_list_key_unique": { - "name": "circuit_sections_list_key_unique", - "columns": [ - "circuit_list_id", - "key" - ], - "isUnique": true - }, - "circuit_sections_list_prefix_unique": { - "name": "circuit_sections_list_prefix_unique", - "columns": [ - "circuit_list_id", - "prefix" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_sections_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_sections", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuits": { - "name": "circuits", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_length": { - "name": "cable_length", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_assignment": { - "name": "rcd_assignment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "terminal_designation": { - "name": "terminal_designation", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage": { - "name": "voltage", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "control_requirement": { - "name": "control_requirement", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_reserve": { - "name": "is_reserve", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuits_list_equipment_identifier_unique": { - "name": "circuits_list_equipment_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuits_circuit_list_id_circuit_lists_id_fk": { - "name": "circuits_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuits_section_id_circuit_sections_id_fk": { - "name": "circuits_section_id_circuit_sections_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_sections", - "columnsFrom": [ - "section_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "consumers": { - "name": "consumers", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "project_device_id": { - "name": "project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_linked_to_device": { - "name": "is_linked_to_device", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_number": { - "name": "circuit_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "device_type": { - "name": "device_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trade_or_cost_group": { - "name": "trade_or_cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "group_name": { - "name": "group_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "comment": { - "name": "comment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "consumers_project_id_projects_id_fk": { - "name": "consumers_project_id_projects_id_fk", - "tableFrom": "consumers", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "consumers_distribution_board_id_distribution_boards_id_fk": { - "name": "consumers_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "consumers", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_circuit_list_id_circuit_lists_id_fk": { - "name": "consumers_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "consumers", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_project_device_id_project_devices_id_fk": { - "name": "consumers_project_device_id_project_devices_id_fk", - "tableFrom": "consumers", - "tableTo": "project_devices", - "columnsFrom": [ - "project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_room_id_rooms_id_fk": { - "name": "consumers_room_id_rooms_id_fk", - "tableFrom": "consumers", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_boards": { - "name": "distribution_boards", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_boards_project_id_projects_id_fk": { - "name": "distribution_boards_project_id_projects_id_fk", - "tableFrom": "distribution_boards", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "floors": { - "name": "floors", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "floors_project_id_projects_id_fk": { - "name": "floors_project_id_projects_id_fk", - "tableFrom": "floors", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "global_devices": { - "name": "global_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_circuit_migrations": { - "name": "legacy_consumer_circuit_migrations", - "columns": { - "consumer_id": { - "name": "consumer_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_device_row_id": { - "name": "circuit_device_row_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuit_device_rows", - "columnsFrom": [ - "circuit_device_row_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_migration_reports": { - "name": "legacy_consumer_migration_reports", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "legacy_consumer_count": { - "name": "legacy_consumer_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_circuit_count": { - "name": "created_circuit_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_device_row_count": { - "name": "created_device_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_grouped_count": { - "name": "duplicate_grouped_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifier_count": { - "name": "generated_identifier_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "unassigned_row_count": { - "name": "unassigned_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "warnings_json": { - "name": "warnings_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifiers_json": { - "name": "generated_identifiers_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_groups_json": { - "name": "duplicate_groups_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "legacy_consumer_migration_reports_list_unique": { - "name": "legacy_consumer_migration_reports_list_unique", - "columns": [ - "circuit_list_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_migration_reports", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_change_sets": { - "name": "project_change_sets", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_revision_id": { - "name": "project_revision_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "command_type": { - "name": "command_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_schema_version": { - "name": "payload_schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "forward_payload_json": { - "name": "forward_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "inverse_payload_json": { - "name": "inverse_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_change_sets_revision_unique": { - "name": "project_change_sets_revision_unique", - "columns": [ - "project_revision_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_change_sets_project_revision_id_project_revisions_id_fk": { - "name": "project_change_sets_project_revision_id_project_revisions_id_fk", - "tableFrom": "project_change_sets", - "tableTo": "project_revisions", - "columnsFrom": [ - "project_revision_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_devices": { - "name": "project_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'single_phase'" - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "project_devices_project_id_projects_id_fk": { - "name": "project_devices_project_id_projects_id_fk", - "tableFrom": "project_devices", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_history_stack_entries": { - "name": "project_history_stack_entries", - "columns": { - "change_set_id": { - "name": "change_set_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "stack": { - "name": "stack", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "position": { - "name": "position", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_history_stack_entries_position_unique": { - "name": "project_history_stack_entries_position_unique", - "columns": [ - "project_id", - "stack", - "position" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { - "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "project_change_sets", - "columnsFrom": [ - "change_set_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "project_history_stack_entries_project_id_projects_id_fk": { - "name": "project_history_stack_entries_project_id_projects_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": { - "project_history_stack_entries_stack_check": { - "name": "project_history_stack_entries_stack_check", - "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" - } - } - }, - "project_revisions": { - "name": "project_revisions", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "revision_number": { - "name": "revision_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "actor_id": { - "name": "actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "source": { - "name": "source", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_revisions_project_number_unique": { - "name": "project_revisions_project_number_unique", - "columns": [ - "project_id", - "revision_number" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_revisions_project_id_projects_id_fk": { - "name": "project_revisions_project_id_projects_id_fk", - "tableFrom": "project_revisions", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_snapshots": { - "name": "project_snapshots", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_revision": { - "name": "source_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "schema_version": { - "name": "schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "kind": { - "name": "kind", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'named'" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "payload_json": { - "name": "payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_sha256": { - "name": "payload_sha256", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_by_actor_id": { - "name": "created_by_actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_snapshots_project_created_idx": { - "name": "project_snapshots_project_created_idx", - "columns": [ - "project_id", - "created_at_iso" - ], - "isUnique": false - }, - "project_snapshots_project_kind_revision_idx": { - "name": "project_snapshots_project_kind_revision_idx", - "columns": [ - "project_id", - "kind", - "source_revision" - ], - "isUnique": false - }, - "project_snapshots_project_name_unique": { - "name": "project_snapshots_project_name_unique", - "columns": [ - "project_id", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_snapshots_project_id_projects_id_fk": { - "name": "project_snapshots_project_id_projects_id_fk", - "tableFrom": "project_snapshots", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "projects": { - "name": "projects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "internal_project_number": { - "name": "internal_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "external_project_number": { - "name": "external_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "building_owner": { - "name": "building_owner", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "single_phase_voltage_v": { - "name": "single_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 230 - }, - "three_phase_voltage_v": { - "name": "three_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 400 - }, - "current_revision": { - "name": "current_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "rooms": { - "name": "rooms", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number": { - "name": "room_number", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "room_name": { - "name": "room_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "rooms_project_id_projects_id_fk": { - "name": "rooms_project_id_projects_id_fk", - "tableFrom": "rooms", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "rooms_floor_id_floors_id_fk": { - "name": "rooms_floor_id_floors_id_fk", - "tableFrom": "rooms", - "tableTo": "floors", - "columnsFrom": [ - "floor_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/src/db/migrations/meta/0017_snapshot.json b/src/db/migrations/meta/0017_snapshot.json deleted file mode 100644 index 1ec7207..0000000 --- a/src/db/migrations/meta/0017_snapshot.json +++ /dev/null @@ -1,1877 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "f885efbf-98a4-4977-8416-9cc890cf64df", - "prevId": "8502c7d2-e925-42c3-8044-3e965c170f97", - "tables": { - "circuit_device_rows": { - "name": "circuit_device_rows", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "linked_project_device_id": { - "name": "linked_project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "legacy_consumer_id": { - "name": "legacy_consumer_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "level": { - "name": "level", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number_snapshot": { - "name": "room_number_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_name_snapshot": { - "name": "room_name_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "overridden_fields": { - "name": "overridden_fields", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "circuit_device_rows_circuit_id_circuits_id_fk": { - "name": "circuit_device_rows_circuit_id_circuits_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { - "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "project_devices", - "columnsFrom": [ - "linked_project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "circuit_device_rows_room_id_rooms_id_fk": { - "name": "circuit_device_rows_room_id_rooms_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_lists": { - "name": "circuit_lists", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_lists_distribution_board_id_unique": { - "name": "circuit_lists_distribution_board_id_unique", - "columns": [ - "distribution_board_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_lists_project_id_projects_id_fk": { - "name": "circuit_lists_project_id_projects_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_lists_distribution_board_id_distribution_boards_id_fk": { - "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_sections": { - "name": "circuit_sections", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "prefix": { - "name": "prefix", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": { - "circuit_sections_list_key_unique": { - "name": "circuit_sections_list_key_unique", - "columns": [ - "circuit_list_id", - "key" - ], - "isUnique": true - }, - "circuit_sections_list_prefix_unique": { - "name": "circuit_sections_list_prefix_unique", - "columns": [ - "circuit_list_id", - "prefix" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_sections_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_sections", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuits": { - "name": "circuits", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_length": { - "name": "cable_length", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_assignment": { - "name": "rcd_assignment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "terminal_designation": { - "name": "terminal_designation", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage": { - "name": "voltage", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "control_requirement": { - "name": "control_requirement", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_reserve": { - "name": "is_reserve", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuits_list_equipment_identifier_unique": { - "name": "circuits_list_equipment_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuits_circuit_list_id_circuit_lists_id_fk": { - "name": "circuits_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuits_section_id_circuit_sections_id_fk": { - "name": "circuits_section_id_circuit_sections_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_sections", - "columnsFrom": [ - "section_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "consumers": { - "name": "consumers", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "project_device_id": { - "name": "project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_linked_to_device": { - "name": "is_linked_to_device", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_number": { - "name": "circuit_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "device_type": { - "name": "device_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trade_or_cost_group": { - "name": "trade_or_cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "group_name": { - "name": "group_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "comment": { - "name": "comment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "consumers_project_id_projects_id_fk": { - "name": "consumers_project_id_projects_id_fk", - "tableFrom": "consumers", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "consumers_distribution_board_id_distribution_boards_id_fk": { - "name": "consumers_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "consumers", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_circuit_list_id_circuit_lists_id_fk": { - "name": "consumers_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "consumers", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_project_device_id_project_devices_id_fk": { - "name": "consumers_project_device_id_project_devices_id_fk", - "tableFrom": "consumers", - "tableTo": "project_devices", - "columnsFrom": [ - "project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_room_id_rooms_id_fk": { - "name": "consumers_room_id_rooms_id_fk", - "tableFrom": "consumers", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_boards": { - "name": "distribution_boards", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "supply_type": { - "name": "supply_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_boards_project_id_projects_id_fk": { - "name": "distribution_boards_project_id_projects_id_fk", - "tableFrom": "distribution_boards", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "distribution_boards_floor_id_floors_id_fk": { - "name": "distribution_boards_floor_id_floors_id_fk", - "tableFrom": "distribution_boards", - "tableTo": "floors", - "columnsFrom": [ - "floor_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "floors": { - "name": "floors", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "floors_project_id_projects_id_fk": { - "name": "floors_project_id_projects_id_fk", - "tableFrom": "floors", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "global_devices": { - "name": "global_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_circuit_migrations": { - "name": "legacy_consumer_circuit_migrations", - "columns": { - "consumer_id": { - "name": "consumer_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_device_row_id": { - "name": "circuit_device_row_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuit_device_rows", - "columnsFrom": [ - "circuit_device_row_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_migration_reports": { - "name": "legacy_consumer_migration_reports", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "legacy_consumer_count": { - "name": "legacy_consumer_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_circuit_count": { - "name": "created_circuit_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_device_row_count": { - "name": "created_device_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_grouped_count": { - "name": "duplicate_grouped_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifier_count": { - "name": "generated_identifier_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "unassigned_row_count": { - "name": "unassigned_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "warnings_json": { - "name": "warnings_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifiers_json": { - "name": "generated_identifiers_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_groups_json": { - "name": "duplicate_groups_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "legacy_consumer_migration_reports_list_unique": { - "name": "legacy_consumer_migration_reports_list_unique", - "columns": [ - "circuit_list_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_migration_reports", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_change_sets": { - "name": "project_change_sets", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_revision_id": { - "name": "project_revision_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "command_type": { - "name": "command_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_schema_version": { - "name": "payload_schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "forward_payload_json": { - "name": "forward_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "inverse_payload_json": { - "name": "inverse_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_change_sets_revision_unique": { - "name": "project_change_sets_revision_unique", - "columns": [ - "project_revision_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_change_sets_project_revision_id_project_revisions_id_fk": { - "name": "project_change_sets_project_revision_id_project_revisions_id_fk", - "tableFrom": "project_change_sets", - "tableTo": "project_revisions", - "columnsFrom": [ - "project_revision_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_devices": { - "name": "project_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'single_phase'" - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "project_devices_project_id_projects_id_fk": { - "name": "project_devices_project_id_projects_id_fk", - "tableFrom": "project_devices", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_history_stack_entries": { - "name": "project_history_stack_entries", - "columns": { - "change_set_id": { - "name": "change_set_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "stack": { - "name": "stack", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "position": { - "name": "position", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_history_stack_entries_position_unique": { - "name": "project_history_stack_entries_position_unique", - "columns": [ - "project_id", - "stack", - "position" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { - "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "project_change_sets", - "columnsFrom": [ - "change_set_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "project_history_stack_entries_project_id_projects_id_fk": { - "name": "project_history_stack_entries_project_id_projects_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": { - "project_history_stack_entries_stack_check": { - "name": "project_history_stack_entries_stack_check", - "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" - } - } - }, - "project_revisions": { - "name": "project_revisions", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "revision_number": { - "name": "revision_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "actor_id": { - "name": "actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "source": { - "name": "source", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_revisions_project_number_unique": { - "name": "project_revisions_project_number_unique", - "columns": [ - "project_id", - "revision_number" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_revisions_project_id_projects_id_fk": { - "name": "project_revisions_project_id_projects_id_fk", - "tableFrom": "project_revisions", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_snapshots": { - "name": "project_snapshots", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_revision": { - "name": "source_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "schema_version": { - "name": "schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "kind": { - "name": "kind", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'named'" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "payload_json": { - "name": "payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_sha256": { - "name": "payload_sha256", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_by_actor_id": { - "name": "created_by_actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_snapshots_project_created_idx": { - "name": "project_snapshots_project_created_idx", - "columns": [ - "project_id", - "created_at_iso" - ], - "isUnique": false - }, - "project_snapshots_project_kind_revision_idx": { - "name": "project_snapshots_project_kind_revision_idx", - "columns": [ - "project_id", - "kind", - "source_revision" - ], - "isUnique": false - }, - "project_snapshots_project_name_unique": { - "name": "project_snapshots_project_name_unique", - "columns": [ - "project_id", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_snapshots_project_id_projects_id_fk": { - "name": "project_snapshots_project_id_projects_id_fk", - "tableFrom": "project_snapshots", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "projects": { - "name": "projects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "internal_project_number": { - "name": "internal_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "external_project_number": { - "name": "external_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "building_owner": { - "name": "building_owner", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "single_phase_voltage_v": { - "name": "single_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 230 - }, - "three_phase_voltage_v": { - "name": "three_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 400 - }, - "current_revision": { - "name": "current_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "rooms": { - "name": "rooms", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number": { - "name": "room_number", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "room_name": { - "name": "room_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "rooms_project_id_projects_id_fk": { - "name": "rooms_project_id_projects_id_fk", - "tableFrom": "rooms", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "rooms_floor_id_floors_id_fk": { - "name": "rooms_floor_id_floors_id_fk", - "tableFrom": "rooms", - "tableTo": "floors", - "columnsFrom": [ - "floor_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/src/db/migrations/meta/0018_snapshot.json b/src/db/migrations/meta/0018_snapshot.json deleted file mode 100644 index b463316..0000000 --- a/src/db/migrations/meta/0018_snapshot.json +++ /dev/null @@ -1,1885 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "6297060c-1a3f-4f7c-867f-7c3e96c34645", - "prevId": "f885efbf-98a4-4977-8416-9cc890cf64df", - "tables": { - "circuit_device_rows": { - "name": "circuit_device_rows", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "linked_project_device_id": { - "name": "linked_project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "legacy_consumer_id": { - "name": "legacy_consumer_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "level": { - "name": "level", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number_snapshot": { - "name": "room_number_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_name_snapshot": { - "name": "room_name_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "overridden_fields": { - "name": "overridden_fields", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "circuit_device_rows_circuit_id_circuits_id_fk": { - "name": "circuit_device_rows_circuit_id_circuits_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { - "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "project_devices", - "columnsFrom": [ - "linked_project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "circuit_device_rows_room_id_rooms_id_fk": { - "name": "circuit_device_rows_room_id_rooms_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_lists": { - "name": "circuit_lists", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_lists_distribution_board_id_unique": { - "name": "circuit_lists_distribution_board_id_unique", - "columns": [ - "distribution_board_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_lists_project_id_projects_id_fk": { - "name": "circuit_lists_project_id_projects_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_lists_distribution_board_id_distribution_boards_id_fk": { - "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_sections": { - "name": "circuit_sections", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "prefix": { - "name": "prefix", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": { - "circuit_sections_list_key_unique": { - "name": "circuit_sections_list_key_unique", - "columns": [ - "circuit_list_id", - "key" - ], - "isUnique": true - }, - "circuit_sections_list_prefix_unique": { - "name": "circuit_sections_list_prefix_unique", - "columns": [ - "circuit_list_id", - "prefix" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_sections_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_sections", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuits": { - "name": "circuits", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_length": { - "name": "cable_length", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_assignment": { - "name": "rcd_assignment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "terminal_designation": { - "name": "terminal_designation", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage": { - "name": "voltage", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "control_requirement": { - "name": "control_requirement", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_reserve": { - "name": "is_reserve", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuits_list_equipment_identifier_unique": { - "name": "circuits_list_equipment_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuits_circuit_list_id_circuit_lists_id_fk": { - "name": "circuits_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuits_section_id_circuit_sections_id_fk": { - "name": "circuits_section_id_circuit_sections_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_sections", - "columnsFrom": [ - "section_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "consumers": { - "name": "consumers", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "project_device_id": { - "name": "project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_linked_to_device": { - "name": "is_linked_to_device", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_number": { - "name": "circuit_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "device_type": { - "name": "device_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trade_or_cost_group": { - "name": "trade_or_cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "group_name": { - "name": "group_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "comment": { - "name": "comment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "consumers_project_id_projects_id_fk": { - "name": "consumers_project_id_projects_id_fk", - "tableFrom": "consumers", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "consumers_distribution_board_id_distribution_boards_id_fk": { - "name": "consumers_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "consumers", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_circuit_list_id_circuit_lists_id_fk": { - "name": "consumers_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "consumers", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_project_device_id_project_devices_id_fk": { - "name": "consumers_project_device_id_project_devices_id_fk", - "tableFrom": "consumers", - "tableTo": "project_devices", - "columnsFrom": [ - "project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_room_id_rooms_id_fk": { - "name": "consumers_room_id_rooms_id_fk", - "tableFrom": "consumers", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_boards": { - "name": "distribution_boards", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "supply_type": { - "name": "supply_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_boards_project_id_projects_id_fk": { - "name": "distribution_boards_project_id_projects_id_fk", - "tableFrom": "distribution_boards", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "distribution_boards_floor_id_floors_id_fk": { - "name": "distribution_boards_floor_id_floors_id_fk", - "tableFrom": "distribution_boards", - "tableTo": "floors", - "columnsFrom": [ - "floor_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "floors": { - "name": "floors", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "floors_project_id_projects_id_fk": { - "name": "floors_project_id_projects_id_fk", - "tableFrom": "floors", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "global_devices": { - "name": "global_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_circuit_migrations": { - "name": "legacy_consumer_circuit_migrations", - "columns": { - "consumer_id": { - "name": "consumer_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_device_row_id": { - "name": "circuit_device_row_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuit_device_rows", - "columnsFrom": [ - "circuit_device_row_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_migration_reports": { - "name": "legacy_consumer_migration_reports", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "legacy_consumer_count": { - "name": "legacy_consumer_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_circuit_count": { - "name": "created_circuit_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_device_row_count": { - "name": "created_device_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_grouped_count": { - "name": "duplicate_grouped_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifier_count": { - "name": "generated_identifier_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "unassigned_row_count": { - "name": "unassigned_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "warnings_json": { - "name": "warnings_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifiers_json": { - "name": "generated_identifiers_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_groups_json": { - "name": "duplicate_groups_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "legacy_consumer_migration_reports_list_unique": { - "name": "legacy_consumer_migration_reports_list_unique", - "columns": [ - "circuit_list_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_migration_reports", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_change_sets": { - "name": "project_change_sets", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_revision_id": { - "name": "project_revision_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "command_type": { - "name": "command_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_schema_version": { - "name": "payload_schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "forward_payload_json": { - "name": "forward_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "inverse_payload_json": { - "name": "inverse_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_change_sets_revision_unique": { - "name": "project_change_sets_revision_unique", - "columns": [ - "project_revision_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_change_sets_project_revision_id_project_revisions_id_fk": { - "name": "project_change_sets_project_revision_id_project_revisions_id_fk", - "tableFrom": "project_change_sets", - "tableTo": "project_revisions", - "columnsFrom": [ - "project_revision_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_devices": { - "name": "project_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'single_phase'" - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "project_devices_project_id_projects_id_fk": { - "name": "project_devices_project_id_projects_id_fk", - "tableFrom": "project_devices", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_history_stack_entries": { - "name": "project_history_stack_entries", - "columns": { - "change_set_id": { - "name": "change_set_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "stack": { - "name": "stack", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "position": { - "name": "position", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_history_stack_entries_position_unique": { - "name": "project_history_stack_entries_position_unique", - "columns": [ - "project_id", - "stack", - "position" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { - "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "project_change_sets", - "columnsFrom": [ - "change_set_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "project_history_stack_entries_project_id_projects_id_fk": { - "name": "project_history_stack_entries_project_id_projects_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": { - "project_history_stack_entries_stack_check": { - "name": "project_history_stack_entries_stack_check", - "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" - } - } - }, - "project_revisions": { - "name": "project_revisions", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "revision_number": { - "name": "revision_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "actor_id": { - "name": "actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "source": { - "name": "source", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_revisions_project_number_unique": { - "name": "project_revisions_project_number_unique", - "columns": [ - "project_id", - "revision_number" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_revisions_project_id_projects_id_fk": { - "name": "project_revisions_project_id_projects_id_fk", - "tableFrom": "project_revisions", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_snapshots": { - "name": "project_snapshots", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_revision": { - "name": "source_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "schema_version": { - "name": "schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "kind": { - "name": "kind", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'named'" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "payload_json": { - "name": "payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_sha256": { - "name": "payload_sha256", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_by_actor_id": { - "name": "created_by_actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_snapshots_project_created_idx": { - "name": "project_snapshots_project_created_idx", - "columns": [ - "project_id", - "created_at_iso" - ], - "isUnique": false - }, - "project_snapshots_project_kind_revision_idx": { - "name": "project_snapshots_project_kind_revision_idx", - "columns": [ - "project_id", - "kind", - "source_revision" - ], - "isUnique": false - }, - "project_snapshots_project_name_unique": { - "name": "project_snapshots_project_name_unique", - "columns": [ - "project_id", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_snapshots_project_id_projects_id_fk": { - "name": "project_snapshots_project_id_projects_id_fk", - "tableFrom": "project_snapshots", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "projects": { - "name": "projects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "internal_project_number": { - "name": "internal_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "external_project_number": { - "name": "external_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "building_owner": { - "name": "building_owner", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "single_phase_voltage_v": { - "name": "single_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 230 - }, - "three_phase_voltage_v": { - "name": "three_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 400 - }, - "enabled_distribution_board_supply_types": { - "name": "enabled_distribution_board_supply_types", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'[\"AV\",\"SV\",\"EV\",\"USV\",\"MSR\",\"SiBe\"]'" - }, - "current_revision": { - "name": "current_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "rooms": { - "name": "rooms", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number": { - "name": "room_number", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "room_name": { - "name": "room_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "rooms_project_id_projects_id_fk": { - "name": "rooms_project_id_projects_id_fk", - "tableFrom": "rooms", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "rooms_floor_id_floors_id_fk": { - "name": "rooms_floor_id_floors_id_fk", - "tableFrom": "rooms", - "tableTo": "floors", - "columnsFrom": [ - "floor_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/src/db/migrations/meta/0019_snapshot.json b/src/db/migrations/meta/0019_snapshot.json deleted file mode 100644 index 74be78e..0000000 --- a/src/db/migrations/meta/0019_snapshot.json +++ /dev/null @@ -1,1885 +0,0 @@ -{ - "id": "38fade39-c2c3-494e-ae8d-ee4677b2cbc7", - "prevId": "6297060c-1a3f-4f7c-867f-7c3e96c34645", - "version": "6", - "dialect": "sqlite", - "tables": { - "circuit_device_rows": { - "name": "circuit_device_rows", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "linked_project_device_id": { - "name": "linked_project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "legacy_consumer_id": { - "name": "legacy_consumer_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "level": { - "name": "level", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number_snapshot": { - "name": "room_number_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_name_snapshot": { - "name": "room_name_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "overridden_fields": { - "name": "overridden_fields", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "circuit_device_rows_circuit_id_circuits_id_fk": { - "name": "circuit_device_rows_circuit_id_circuits_id_fk", - "tableFrom": "circuit_device_rows", - "columnsFrom": [ - "circuit_id" - ], - "tableTo": "circuits", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { - "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", - "tableFrom": "circuit_device_rows", - "columnsFrom": [ - "linked_project_device_id" - ], - "tableTo": "project_devices", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - }, - "circuit_device_rows_room_id_rooms_id_fk": { - "name": "circuit_device_rows_room_id_rooms_id_fk", - "tableFrom": "circuit_device_rows", - "columnsFrom": [ - "room_id" - ], - "tableTo": "rooms", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_lists": { - "name": "circuit_lists", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_lists_distribution_board_id_unique": { - "name": "circuit_lists_distribution_board_id_unique", - "columns": [ - "distribution_board_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_lists_project_id_projects_id_fk": { - "name": "circuit_lists_project_id_projects_id_fk", - "tableFrom": "circuit_lists", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "circuit_lists_distribution_board_id_distribution_boards_id_fk": { - "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "circuit_lists", - "columnsFrom": [ - "distribution_board_id" - ], - "tableTo": "distribution_boards", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_sections": { - "name": "circuit_sections", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "prefix": { - "name": "prefix", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": { - "circuit_sections_list_key_unique": { - "name": "circuit_sections_list_key_unique", - "columns": [ - "circuit_list_id", - "key" - ], - "isUnique": true - }, - "circuit_sections_list_prefix_unique": { - "name": "circuit_sections_list_prefix_unique", - "columns": [ - "circuit_list_id", - "prefix" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_sections_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_sections", - "columnsFrom": [ - "circuit_list_id" - ], - "tableTo": "circuit_lists", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuits": { - "name": "circuits", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_length": { - "name": "cable_length", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_assignment": { - "name": "rcd_assignment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "terminal_designation": { - "name": "terminal_designation", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage": { - "name": "voltage", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "control_requirement": { - "name": "control_requirement", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_reserve": { - "name": "is_reserve", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuits_list_equipment_identifier_unique": { - "name": "circuits_list_equipment_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuits_circuit_list_id_circuit_lists_id_fk": { - "name": "circuits_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuits", - "columnsFrom": [ - "circuit_list_id" - ], - "tableTo": "circuit_lists", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "circuits_section_id_circuit_sections_id_fk": { - "name": "circuits_section_id_circuit_sections_id_fk", - "tableFrom": "circuits", - "columnsFrom": [ - "section_id" - ], - "tableTo": "circuit_sections", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "consumers": { - "name": "consumers", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "project_device_id": { - "name": "project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_linked_to_device": { - "name": "is_linked_to_device", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_number": { - "name": "circuit_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "device_type": { - "name": "device_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trade_or_cost_group": { - "name": "trade_or_cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "group_name": { - "name": "group_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "comment": { - "name": "comment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "consumers_project_id_projects_id_fk": { - "name": "consumers_project_id_projects_id_fk", - "tableFrom": "consumers", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "consumers_distribution_board_id_distribution_boards_id_fk": { - "name": "consumers_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "consumers", - "columnsFrom": [ - "distribution_board_id" - ], - "tableTo": "distribution_boards", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - }, - "consumers_circuit_list_id_circuit_lists_id_fk": { - "name": "consumers_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "consumers", - "columnsFrom": [ - "circuit_list_id" - ], - "tableTo": "circuit_lists", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - }, - "consumers_project_device_id_project_devices_id_fk": { - "name": "consumers_project_device_id_project_devices_id_fk", - "tableFrom": "consumers", - "columnsFrom": [ - "project_device_id" - ], - "tableTo": "project_devices", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - }, - "consumers_room_id_rooms_id_fk": { - "name": "consumers_room_id_rooms_id_fk", - "tableFrom": "consumers", - "columnsFrom": [ - "room_id" - ], - "tableTo": "rooms", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_boards": { - "name": "distribution_boards", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "supply_type": { - "name": "supply_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_boards_project_id_projects_id_fk": { - "name": "distribution_boards_project_id_projects_id_fk", - "tableFrom": "distribution_boards", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "distribution_boards_floor_id_floors_id_fk": { - "name": "distribution_boards_floor_id_floors_id_fk", - "tableFrom": "distribution_boards", - "columnsFrom": [ - "floor_id" - ], - "tableTo": "floors", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "floors": { - "name": "floors", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "floors_project_id_projects_id_fk": { - "name": "floors_project_id_projects_id_fk", - "tableFrom": "floors", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "global_devices": { - "name": "global_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_circuit_migrations": { - "name": "legacy_consumer_circuit_migrations", - "columns": { - "consumer_id": { - "name": "consumer_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_device_row_id": { - "name": "circuit_device_row_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "columnsFrom": [ - "circuit_id" - ], - "tableTo": "circuits", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "columnsFrom": [ - "circuit_device_row_id" - ], - "tableTo": "circuit_device_rows", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "columnsFrom": [ - "circuit_list_id" - ], - "tableTo": "circuit_lists", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_migration_reports": { - "name": "legacy_consumer_migration_reports", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "legacy_consumer_count": { - "name": "legacy_consumer_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_circuit_count": { - "name": "created_circuit_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_device_row_count": { - "name": "created_device_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_grouped_count": { - "name": "duplicate_grouped_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifier_count": { - "name": "generated_identifier_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "unassigned_row_count": { - "name": "unassigned_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "warnings_json": { - "name": "warnings_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifiers_json": { - "name": "generated_identifiers_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_groups_json": { - "name": "duplicate_groups_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "legacy_consumer_migration_reports_list_unique": { - "name": "legacy_consumer_migration_reports_list_unique", - "columns": [ - "circuit_list_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_migration_reports", - "columnsFrom": [ - "circuit_list_id" - ], - "tableTo": "circuit_lists", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_change_sets": { - "name": "project_change_sets", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_revision_id": { - "name": "project_revision_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "command_type": { - "name": "command_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_schema_version": { - "name": "payload_schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "forward_payload_json": { - "name": "forward_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "inverse_payload_json": { - "name": "inverse_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_change_sets_revision_unique": { - "name": "project_change_sets_revision_unique", - "columns": [ - "project_revision_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_change_sets_project_revision_id_project_revisions_id_fk": { - "name": "project_change_sets_project_revision_id_project_revisions_id_fk", - "tableFrom": "project_change_sets", - "columnsFrom": [ - "project_revision_id" - ], - "tableTo": "project_revisions", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_devices": { - "name": "project_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'single_phase'" - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "project_devices_project_id_projects_id_fk": { - "name": "project_devices_project_id_projects_id_fk", - "tableFrom": "project_devices", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_history_stack_entries": { - "name": "project_history_stack_entries", - "columns": { - "change_set_id": { - "name": "change_set_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "stack": { - "name": "stack", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "position": { - "name": "position", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_history_stack_entries_position_unique": { - "name": "project_history_stack_entries_position_unique", - "columns": [ - "project_id", - "stack", - "position" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { - "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", - "tableFrom": "project_history_stack_entries", - "columnsFrom": [ - "change_set_id" - ], - "tableTo": "project_change_sets", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "project_history_stack_entries_project_id_projects_id_fk": { - "name": "project_history_stack_entries_project_id_projects_id_fk", - "tableFrom": "project_history_stack_entries", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": { - "project_history_stack_entries_stack_check": { - "name": "project_history_stack_entries_stack_check", - "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" - } - } - }, - "project_revisions": { - "name": "project_revisions", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "revision_number": { - "name": "revision_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "actor_id": { - "name": "actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "source": { - "name": "source", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_revisions_project_number_unique": { - "name": "project_revisions_project_number_unique", - "columns": [ - "project_id", - "revision_number" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_revisions_project_id_projects_id_fk": { - "name": "project_revisions_project_id_projects_id_fk", - "tableFrom": "project_revisions", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_snapshots": { - "name": "project_snapshots", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_revision": { - "name": "source_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "schema_version": { - "name": "schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "kind": { - "name": "kind", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'named'" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "payload_json": { - "name": "payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_sha256": { - "name": "payload_sha256", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_by_actor_id": { - "name": "created_by_actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_snapshots_project_created_idx": { - "name": "project_snapshots_project_created_idx", - "columns": [ - "project_id", - "created_at_iso" - ], - "isUnique": false - }, - "project_snapshots_project_kind_revision_idx": { - "name": "project_snapshots_project_kind_revision_idx", - "columns": [ - "project_id", - "kind", - "source_revision" - ], - "isUnique": false - }, - "project_snapshots_project_name_unique": { - "name": "project_snapshots_project_name_unique", - "columns": [ - "project_id", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_snapshots_project_id_projects_id_fk": { - "name": "project_snapshots_project_id_projects_id_fk", - "tableFrom": "project_snapshots", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "projects": { - "name": "projects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "internal_project_number": { - "name": "internal_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "external_project_number": { - "name": "external_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "building_owner": { - "name": "building_owner", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "single_phase_voltage_v": { - "name": "single_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 230 - }, - "three_phase_voltage_v": { - "name": "three_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 400 - }, - "enabled_distribution_board_supply_types": { - "name": "enabled_distribution_board_supply_types", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'[\"AV\",\"SV\",\"EV\",\"USV\",\"MSR\",\"SiBe\"]'" - }, - "current_revision": { - "name": "current_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "rooms": { - "name": "rooms", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number": { - "name": "room_number", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "room_name": { - "name": "room_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "rooms_project_id_projects_id_fk": { - "name": "rooms_project_id_projects_id_fk", - "tableFrom": "rooms", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "rooms_floor_id_floors_id_fk": { - "name": "rooms_floor_id_floors_id_fk", - "tableFrom": "rooms", - "columnsFrom": [ - "floor_id" - ], - "tableTo": "floors", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/src/db/migrations/meta/0020_snapshot.json b/src/db/migrations/meta/0020_snapshot.json deleted file mode 100644 index 73ccbae..0000000 --- a/src/db/migrations/meta/0020_snapshot.json +++ /dev/null @@ -1,1885 +0,0 @@ -{ - "id": "05a90b58-39b0-4e33-95fd-331b4dd38df0", - "prevId": "38fade39-c2c3-494e-ae8d-ee4677b2cbc7", - "version": "6", - "dialect": "sqlite", - "tables": { - "circuit_device_rows": { - "name": "circuit_device_rows", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "linked_project_device_id": { - "name": "linked_project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "legacy_consumer_id": { - "name": "legacy_consumer_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "level": { - "name": "level", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number_snapshot": { - "name": "room_number_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_name_snapshot": { - "name": "room_name_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "overridden_fields": { - "name": "overridden_fields", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "circuit_device_rows_circuit_id_circuits_id_fk": { - "name": "circuit_device_rows_circuit_id_circuits_id_fk", - "tableFrom": "circuit_device_rows", - "columnsFrom": [ - "circuit_id" - ], - "tableTo": "circuits", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { - "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", - "tableFrom": "circuit_device_rows", - "columnsFrom": [ - "linked_project_device_id" - ], - "tableTo": "project_devices", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - }, - "circuit_device_rows_room_id_rooms_id_fk": { - "name": "circuit_device_rows_room_id_rooms_id_fk", - "tableFrom": "circuit_device_rows", - "columnsFrom": [ - "room_id" - ], - "tableTo": "rooms", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_lists": { - "name": "circuit_lists", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_lists_distribution_board_id_unique": { - "name": "circuit_lists_distribution_board_id_unique", - "columns": [ - "distribution_board_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_lists_project_id_projects_id_fk": { - "name": "circuit_lists_project_id_projects_id_fk", - "tableFrom": "circuit_lists", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "circuit_lists_distribution_board_id_distribution_boards_id_fk": { - "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "circuit_lists", - "columnsFrom": [ - "distribution_board_id" - ], - "tableTo": "distribution_boards", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_sections": { - "name": "circuit_sections", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "prefix": { - "name": "prefix", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": { - "circuit_sections_list_key_unique": { - "name": "circuit_sections_list_key_unique", - "columns": [ - "circuit_list_id", - "key" - ], - "isUnique": true - }, - "circuit_sections_list_prefix_unique": { - "name": "circuit_sections_list_prefix_unique", - "columns": [ - "circuit_list_id", - "prefix" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_sections_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_sections", - "columnsFrom": [ - "circuit_list_id" - ], - "tableTo": "circuit_lists", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuits": { - "name": "circuits", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_length": { - "name": "cable_length", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_assignment": { - "name": "rcd_assignment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "terminal_designation": { - "name": "terminal_designation", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage": { - "name": "voltage", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "control_requirement": { - "name": "control_requirement", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_reserve": { - "name": "is_reserve", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuits_list_equipment_identifier_unique": { - "name": "circuits_list_equipment_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuits_circuit_list_id_circuit_lists_id_fk": { - "name": "circuits_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuits", - "columnsFrom": [ - "circuit_list_id" - ], - "tableTo": "circuit_lists", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "circuits_section_id_circuit_sections_id_fk": { - "name": "circuits_section_id_circuit_sections_id_fk", - "tableFrom": "circuits", - "columnsFrom": [ - "section_id" - ], - "tableTo": "circuit_sections", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "consumers": { - "name": "consumers", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "project_device_id": { - "name": "project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_linked_to_device": { - "name": "is_linked_to_device", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_number": { - "name": "circuit_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "device_type": { - "name": "device_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trade_or_cost_group": { - "name": "trade_or_cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "group_name": { - "name": "group_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "comment": { - "name": "comment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "consumers_project_id_projects_id_fk": { - "name": "consumers_project_id_projects_id_fk", - "tableFrom": "consumers", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "consumers_distribution_board_id_distribution_boards_id_fk": { - "name": "consumers_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "consumers", - "columnsFrom": [ - "distribution_board_id" - ], - "tableTo": "distribution_boards", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - }, - "consumers_circuit_list_id_circuit_lists_id_fk": { - "name": "consumers_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "consumers", - "columnsFrom": [ - "circuit_list_id" - ], - "tableTo": "circuit_lists", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - }, - "consumers_project_device_id_project_devices_id_fk": { - "name": "consumers_project_device_id_project_devices_id_fk", - "tableFrom": "consumers", - "columnsFrom": [ - "project_device_id" - ], - "tableTo": "project_devices", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - }, - "consumers_room_id_rooms_id_fk": { - "name": "consumers_room_id_rooms_id_fk", - "tableFrom": "consumers", - "columnsFrom": [ - "room_id" - ], - "tableTo": "rooms", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_boards": { - "name": "distribution_boards", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "supply_type": { - "name": "supply_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_boards_project_id_projects_id_fk": { - "name": "distribution_boards_project_id_projects_id_fk", - "tableFrom": "distribution_boards", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "distribution_boards_floor_id_floors_id_fk": { - "name": "distribution_boards_floor_id_floors_id_fk", - "tableFrom": "distribution_boards", - "columnsFrom": [ - "floor_id" - ], - "tableTo": "floors", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "floors": { - "name": "floors", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "floors_project_id_projects_id_fk": { - "name": "floors_project_id_projects_id_fk", - "tableFrom": "floors", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "global_devices": { - "name": "global_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_circuit_migrations": { - "name": "legacy_consumer_circuit_migrations", - "columns": { - "consumer_id": { - "name": "consumer_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_device_row_id": { - "name": "circuit_device_row_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "columnsFrom": [ - "circuit_id" - ], - "tableTo": "circuits", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "columnsFrom": [ - "circuit_device_row_id" - ], - "tableTo": "circuit_device_rows", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "columnsFrom": [ - "circuit_list_id" - ], - "tableTo": "circuit_lists", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_migration_reports": { - "name": "legacy_consumer_migration_reports", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "legacy_consumer_count": { - "name": "legacy_consumer_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_circuit_count": { - "name": "created_circuit_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_device_row_count": { - "name": "created_device_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_grouped_count": { - "name": "duplicate_grouped_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifier_count": { - "name": "generated_identifier_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "unassigned_row_count": { - "name": "unassigned_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "warnings_json": { - "name": "warnings_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifiers_json": { - "name": "generated_identifiers_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_groups_json": { - "name": "duplicate_groups_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "legacy_consumer_migration_reports_list_unique": { - "name": "legacy_consumer_migration_reports_list_unique", - "columns": [ - "circuit_list_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_migration_reports", - "columnsFrom": [ - "circuit_list_id" - ], - "tableTo": "circuit_lists", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_change_sets": { - "name": "project_change_sets", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_revision_id": { - "name": "project_revision_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "command_type": { - "name": "command_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_schema_version": { - "name": "payload_schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "forward_payload_json": { - "name": "forward_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "inverse_payload_json": { - "name": "inverse_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_change_sets_revision_unique": { - "name": "project_change_sets_revision_unique", - "columns": [ - "project_revision_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_change_sets_project_revision_id_project_revisions_id_fk": { - "name": "project_change_sets_project_revision_id_project_revisions_id_fk", - "tableFrom": "project_change_sets", - "columnsFrom": [ - "project_revision_id" - ], - "tableTo": "project_revisions", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_devices": { - "name": "project_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'single_phase'" - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "project_devices_project_id_projects_id_fk": { - "name": "project_devices_project_id_projects_id_fk", - "tableFrom": "project_devices", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_history_stack_entries": { - "name": "project_history_stack_entries", - "columns": { - "change_set_id": { - "name": "change_set_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "stack": { - "name": "stack", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "position": { - "name": "position", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_history_stack_entries_position_unique": { - "name": "project_history_stack_entries_position_unique", - "columns": [ - "project_id", - "stack", - "position" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { - "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", - "tableFrom": "project_history_stack_entries", - "columnsFrom": [ - "change_set_id" - ], - "tableTo": "project_change_sets", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "project_history_stack_entries_project_id_projects_id_fk": { - "name": "project_history_stack_entries_project_id_projects_id_fk", - "tableFrom": "project_history_stack_entries", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": { - "project_history_stack_entries_stack_check": { - "name": "project_history_stack_entries_stack_check", - "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" - } - } - }, - "project_revisions": { - "name": "project_revisions", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "revision_number": { - "name": "revision_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "actor_id": { - "name": "actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "source": { - "name": "source", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_revisions_project_number_unique": { - "name": "project_revisions_project_number_unique", - "columns": [ - "project_id", - "revision_number" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_revisions_project_id_projects_id_fk": { - "name": "project_revisions_project_id_projects_id_fk", - "tableFrom": "project_revisions", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_snapshots": { - "name": "project_snapshots", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_revision": { - "name": "source_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "schema_version": { - "name": "schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "kind": { - "name": "kind", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'named'" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "payload_json": { - "name": "payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_sha256": { - "name": "payload_sha256", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_by_actor_id": { - "name": "created_by_actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_snapshots_project_created_idx": { - "name": "project_snapshots_project_created_idx", - "columns": [ - "project_id", - "created_at_iso" - ], - "isUnique": false - }, - "project_snapshots_project_kind_revision_idx": { - "name": "project_snapshots_project_kind_revision_idx", - "columns": [ - "project_id", - "kind", - "source_revision" - ], - "isUnique": false - }, - "project_snapshots_project_name_unique": { - "name": "project_snapshots_project_name_unique", - "columns": [ - "project_id", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_snapshots_project_id_projects_id_fk": { - "name": "project_snapshots_project_id_projects_id_fk", - "tableFrom": "project_snapshots", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "projects": { - "name": "projects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "internal_project_number": { - "name": "internal_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "external_project_number": { - "name": "external_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "building_owner": { - "name": "building_owner", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "single_phase_voltage_v": { - "name": "single_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 230 - }, - "three_phase_voltage_v": { - "name": "three_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 400 - }, - "enabled_distribution_board_supply_types": { - "name": "enabled_distribution_board_supply_types", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'[\"AV\",\"SV\",\"EV\",\"USV\",\"MSR\",\"SiBe\"]'" - }, - "current_revision": { - "name": "current_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "rooms": { - "name": "rooms", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number": { - "name": "room_number", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "room_name": { - "name": "room_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "rooms_project_id_projects_id_fk": { - "name": "rooms_project_id_projects_id_fk", - "tableFrom": "rooms", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "rooms_floor_id_floors_id_fk": { - "name": "rooms_floor_id_floors_id_fk", - "tableFrom": "rooms", - "columnsFrom": [ - "floor_id" - ], - "tableTo": "floors", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/src/db/migrations/meta/0021_snapshot.json b/src/db/migrations/meta/0021_snapshot.json deleted file mode 100644 index f0531ff..0000000 --- a/src/db/migrations/meta/0021_snapshot.json +++ /dev/null @@ -1,1885 +0,0 @@ -{ - "id": "3cdbf3c2-6946-4520-a6d0-d39c3e144a66", - "prevId": "05a90b58-39b0-4e33-95fd-331b4dd38df0", - "version": "6", - "dialect": "sqlite", - "tables": { - "circuit_device_rows": { - "name": "circuit_device_rows", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "linked_project_device_id": { - "name": "linked_project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "legacy_consumer_id": { - "name": "legacy_consumer_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "level": { - "name": "level", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number_snapshot": { - "name": "room_number_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_name_snapshot": { - "name": "room_name_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "overridden_fields": { - "name": "overridden_fields", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "circuit_device_rows_circuit_id_circuits_id_fk": { - "name": "circuit_device_rows_circuit_id_circuits_id_fk", - "tableFrom": "circuit_device_rows", - "columnsFrom": [ - "circuit_id" - ], - "tableTo": "circuits", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { - "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", - "tableFrom": "circuit_device_rows", - "columnsFrom": [ - "linked_project_device_id" - ], - "tableTo": "project_devices", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - }, - "circuit_device_rows_room_id_rooms_id_fk": { - "name": "circuit_device_rows_room_id_rooms_id_fk", - "tableFrom": "circuit_device_rows", - "columnsFrom": [ - "room_id" - ], - "tableTo": "rooms", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_lists": { - "name": "circuit_lists", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_lists_distribution_board_id_unique": { - "name": "circuit_lists_distribution_board_id_unique", - "columns": [ - "distribution_board_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_lists_project_id_projects_id_fk": { - "name": "circuit_lists_project_id_projects_id_fk", - "tableFrom": "circuit_lists", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "circuit_lists_distribution_board_id_distribution_boards_id_fk": { - "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "circuit_lists", - "columnsFrom": [ - "distribution_board_id" - ], - "tableTo": "distribution_boards", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_sections": { - "name": "circuit_sections", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "prefix": { - "name": "prefix", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": { - "circuit_sections_list_key_unique": { - "name": "circuit_sections_list_key_unique", - "columns": [ - "circuit_list_id", - "key" - ], - "isUnique": true - }, - "circuit_sections_list_prefix_unique": { - "name": "circuit_sections_list_prefix_unique", - "columns": [ - "circuit_list_id", - "prefix" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_sections_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_sections", - "columnsFrom": [ - "circuit_list_id" - ], - "tableTo": "circuit_lists", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuits": { - "name": "circuits", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_length": { - "name": "cable_length", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_assignment": { - "name": "rcd_assignment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "terminal_designation": { - "name": "terminal_designation", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage": { - "name": "voltage", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "control_requirement": { - "name": "control_requirement", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_reserve": { - "name": "is_reserve", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuits_list_equipment_identifier_unique": { - "name": "circuits_list_equipment_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuits_circuit_list_id_circuit_lists_id_fk": { - "name": "circuits_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuits", - "columnsFrom": [ - "circuit_list_id" - ], - "tableTo": "circuit_lists", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "circuits_section_id_circuit_sections_id_fk": { - "name": "circuits_section_id_circuit_sections_id_fk", - "tableFrom": "circuits", - "columnsFrom": [ - "section_id" - ], - "tableTo": "circuit_sections", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "consumers": { - "name": "consumers", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "project_device_id": { - "name": "project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_linked_to_device": { - "name": "is_linked_to_device", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_number": { - "name": "circuit_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "device_type": { - "name": "device_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trade_or_cost_group": { - "name": "trade_or_cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "group_name": { - "name": "group_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "comment": { - "name": "comment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "consumers_project_id_projects_id_fk": { - "name": "consumers_project_id_projects_id_fk", - "tableFrom": "consumers", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "consumers_distribution_board_id_distribution_boards_id_fk": { - "name": "consumers_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "consumers", - "columnsFrom": [ - "distribution_board_id" - ], - "tableTo": "distribution_boards", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - }, - "consumers_circuit_list_id_circuit_lists_id_fk": { - "name": "consumers_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "consumers", - "columnsFrom": [ - "circuit_list_id" - ], - "tableTo": "circuit_lists", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - }, - "consumers_project_device_id_project_devices_id_fk": { - "name": "consumers_project_device_id_project_devices_id_fk", - "tableFrom": "consumers", - "columnsFrom": [ - "project_device_id" - ], - "tableTo": "project_devices", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - }, - "consumers_room_id_rooms_id_fk": { - "name": "consumers_room_id_rooms_id_fk", - "tableFrom": "consumers", - "columnsFrom": [ - "room_id" - ], - "tableTo": "rooms", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_boards": { - "name": "distribution_boards", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "supply_type": { - "name": "supply_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_boards_project_id_projects_id_fk": { - "name": "distribution_boards_project_id_projects_id_fk", - "tableFrom": "distribution_boards", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "distribution_boards_floor_id_floors_id_fk": { - "name": "distribution_boards_floor_id_floors_id_fk", - "tableFrom": "distribution_boards", - "columnsFrom": [ - "floor_id" - ], - "tableTo": "floors", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "floors": { - "name": "floors", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "floors_project_id_projects_id_fk": { - "name": "floors_project_id_projects_id_fk", - "tableFrom": "floors", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "global_devices": { - "name": "global_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_circuit_migrations": { - "name": "legacy_consumer_circuit_migrations", - "columns": { - "consumer_id": { - "name": "consumer_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_device_row_id": { - "name": "circuit_device_row_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "columnsFrom": [ - "circuit_id" - ], - "tableTo": "circuits", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "columnsFrom": [ - "circuit_device_row_id" - ], - "tableTo": "circuit_device_rows", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "columnsFrom": [ - "circuit_list_id" - ], - "tableTo": "circuit_lists", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_migration_reports": { - "name": "legacy_consumer_migration_reports", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "legacy_consumer_count": { - "name": "legacy_consumer_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_circuit_count": { - "name": "created_circuit_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_device_row_count": { - "name": "created_device_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_grouped_count": { - "name": "duplicate_grouped_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifier_count": { - "name": "generated_identifier_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "unassigned_row_count": { - "name": "unassigned_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "warnings_json": { - "name": "warnings_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifiers_json": { - "name": "generated_identifiers_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_groups_json": { - "name": "duplicate_groups_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "legacy_consumer_migration_reports_list_unique": { - "name": "legacy_consumer_migration_reports_list_unique", - "columns": [ - "circuit_list_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_migration_reports", - "columnsFrom": [ - "circuit_list_id" - ], - "tableTo": "circuit_lists", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_change_sets": { - "name": "project_change_sets", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_revision_id": { - "name": "project_revision_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "command_type": { - "name": "command_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_schema_version": { - "name": "payload_schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "forward_payload_json": { - "name": "forward_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "inverse_payload_json": { - "name": "inverse_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_change_sets_revision_unique": { - "name": "project_change_sets_revision_unique", - "columns": [ - "project_revision_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_change_sets_project_revision_id_project_revisions_id_fk": { - "name": "project_change_sets_project_revision_id_project_revisions_id_fk", - "tableFrom": "project_change_sets", - "columnsFrom": [ - "project_revision_id" - ], - "tableTo": "project_revisions", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_devices": { - "name": "project_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'single_phase'" - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "project_devices_project_id_projects_id_fk": { - "name": "project_devices_project_id_projects_id_fk", - "tableFrom": "project_devices", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_history_stack_entries": { - "name": "project_history_stack_entries", - "columns": { - "change_set_id": { - "name": "change_set_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "stack": { - "name": "stack", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "position": { - "name": "position", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_history_stack_entries_position_unique": { - "name": "project_history_stack_entries_position_unique", - "columns": [ - "project_id", - "stack", - "position" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { - "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", - "tableFrom": "project_history_stack_entries", - "columnsFrom": [ - "change_set_id" - ], - "tableTo": "project_change_sets", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "project_history_stack_entries_project_id_projects_id_fk": { - "name": "project_history_stack_entries_project_id_projects_id_fk", - "tableFrom": "project_history_stack_entries", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": { - "project_history_stack_entries_stack_check": { - "name": "project_history_stack_entries_stack_check", - "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" - } - } - }, - "project_revisions": { - "name": "project_revisions", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "revision_number": { - "name": "revision_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "actor_id": { - "name": "actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "source": { - "name": "source", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_revisions_project_number_unique": { - "name": "project_revisions_project_number_unique", - "columns": [ - "project_id", - "revision_number" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_revisions_project_id_projects_id_fk": { - "name": "project_revisions_project_id_projects_id_fk", - "tableFrom": "project_revisions", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_snapshots": { - "name": "project_snapshots", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_revision": { - "name": "source_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "schema_version": { - "name": "schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "kind": { - "name": "kind", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'named'" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "payload_json": { - "name": "payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_sha256": { - "name": "payload_sha256", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_by_actor_id": { - "name": "created_by_actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_snapshots_project_created_idx": { - "name": "project_snapshots_project_created_idx", - "columns": [ - "project_id", - "created_at_iso" - ], - "isUnique": false - }, - "project_snapshots_project_kind_revision_idx": { - "name": "project_snapshots_project_kind_revision_idx", - "columns": [ - "project_id", - "kind", - "source_revision" - ], - "isUnique": false - }, - "project_snapshots_project_name_unique": { - "name": "project_snapshots_project_name_unique", - "columns": [ - "project_id", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_snapshots_project_id_projects_id_fk": { - "name": "project_snapshots_project_id_projects_id_fk", - "tableFrom": "project_snapshots", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "projects": { - "name": "projects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "internal_project_number": { - "name": "internal_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "external_project_number": { - "name": "external_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "building_owner": { - "name": "building_owner", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "single_phase_voltage_v": { - "name": "single_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 230 - }, - "three_phase_voltage_v": { - "name": "three_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 400 - }, - "enabled_distribution_board_supply_types": { - "name": "enabled_distribution_board_supply_types", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'[\"AV\",\"SV\",\"EV\",\"USV\",\"MSR\",\"SiBe\"]'" - }, - "current_revision": { - "name": "current_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "rooms": { - "name": "rooms", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number": { - "name": "room_number", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "room_name": { - "name": "room_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "rooms_project_id_projects_id_fk": { - "name": "rooms_project_id_projects_id_fk", - "tableFrom": "rooms", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "rooms_floor_id_floors_id_fk": { - "name": "rooms_floor_id_floors_id_fk", - "tableFrom": "rooms", - "columnsFrom": [ - "floor_id" - ], - "tableTo": "floors", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/src/db/migrations/meta/0022_snapshot.json b/src/db/migrations/meta/0022_snapshot.json deleted file mode 100644 index a561905..0000000 --- a/src/db/migrations/meta/0022_snapshot.json +++ /dev/null @@ -1,1885 +0,0 @@ -{ - "id": "c9eabd70-45c9-43c3-ae63-f0685aeeb1f1", - "prevId": "3cdbf3c2-6946-4520-a6d0-d39c3e144a66", - "version": "6", - "dialect": "sqlite", - "tables": { - "circuit_device_rows": { - "name": "circuit_device_rows", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "linked_project_device_id": { - "name": "linked_project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "legacy_consumer_id": { - "name": "legacy_consumer_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "level": { - "name": "level", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number_snapshot": { - "name": "room_number_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_name_snapshot": { - "name": "room_name_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "overridden_fields": { - "name": "overridden_fields", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "circuit_device_rows_circuit_id_circuits_id_fk": { - "name": "circuit_device_rows_circuit_id_circuits_id_fk", - "tableFrom": "circuit_device_rows", - "columnsFrom": [ - "circuit_id" - ], - "tableTo": "circuits", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { - "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", - "tableFrom": "circuit_device_rows", - "columnsFrom": [ - "linked_project_device_id" - ], - "tableTo": "project_devices", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - }, - "circuit_device_rows_room_id_rooms_id_fk": { - "name": "circuit_device_rows_room_id_rooms_id_fk", - "tableFrom": "circuit_device_rows", - "columnsFrom": [ - "room_id" - ], - "tableTo": "rooms", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_lists": { - "name": "circuit_lists", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_lists_distribution_board_id_unique": { - "name": "circuit_lists_distribution_board_id_unique", - "columns": [ - "distribution_board_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_lists_project_id_projects_id_fk": { - "name": "circuit_lists_project_id_projects_id_fk", - "tableFrom": "circuit_lists", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "circuit_lists_distribution_board_id_distribution_boards_id_fk": { - "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "circuit_lists", - "columnsFrom": [ - "distribution_board_id" - ], - "tableTo": "distribution_boards", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_sections": { - "name": "circuit_sections", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "prefix": { - "name": "prefix", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": { - "circuit_sections_list_key_unique": { - "name": "circuit_sections_list_key_unique", - "columns": [ - "circuit_list_id", - "key" - ], - "isUnique": true - }, - "circuit_sections_list_prefix_unique": { - "name": "circuit_sections_list_prefix_unique", - "columns": [ - "circuit_list_id", - "prefix" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_sections_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_sections", - "columnsFrom": [ - "circuit_list_id" - ], - "tableTo": "circuit_lists", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuits": { - "name": "circuits", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_length": { - "name": "cable_length", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_assignment": { - "name": "rcd_assignment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "terminal_designation": { - "name": "terminal_designation", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage": { - "name": "voltage", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "control_requirement": { - "name": "control_requirement", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_reserve": { - "name": "is_reserve", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuits_list_equipment_identifier_unique": { - "name": "circuits_list_equipment_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuits_circuit_list_id_circuit_lists_id_fk": { - "name": "circuits_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuits", - "columnsFrom": [ - "circuit_list_id" - ], - "tableTo": "circuit_lists", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "circuits_section_id_circuit_sections_id_fk": { - "name": "circuits_section_id_circuit_sections_id_fk", - "tableFrom": "circuits", - "columnsFrom": [ - "section_id" - ], - "tableTo": "circuit_sections", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "consumers": { - "name": "consumers", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "project_device_id": { - "name": "project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_linked_to_device": { - "name": "is_linked_to_device", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_number": { - "name": "circuit_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "device_type": { - "name": "device_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trade_or_cost_group": { - "name": "trade_or_cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "group_name": { - "name": "group_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "comment": { - "name": "comment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "consumers_project_id_projects_id_fk": { - "name": "consumers_project_id_projects_id_fk", - "tableFrom": "consumers", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "consumers_distribution_board_id_distribution_boards_id_fk": { - "name": "consumers_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "consumers", - "columnsFrom": [ - "distribution_board_id" - ], - "tableTo": "distribution_boards", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - }, - "consumers_circuit_list_id_circuit_lists_id_fk": { - "name": "consumers_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "consumers", - "columnsFrom": [ - "circuit_list_id" - ], - "tableTo": "circuit_lists", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - }, - "consumers_project_device_id_project_devices_id_fk": { - "name": "consumers_project_device_id_project_devices_id_fk", - "tableFrom": "consumers", - "columnsFrom": [ - "project_device_id" - ], - "tableTo": "project_devices", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - }, - "consumers_room_id_rooms_id_fk": { - "name": "consumers_room_id_rooms_id_fk", - "tableFrom": "consumers", - "columnsFrom": [ - "room_id" - ], - "tableTo": "rooms", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_boards": { - "name": "distribution_boards", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "supply_type": { - "name": "supply_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_boards_project_id_projects_id_fk": { - "name": "distribution_boards_project_id_projects_id_fk", - "tableFrom": "distribution_boards", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "distribution_boards_floor_id_floors_id_fk": { - "name": "distribution_boards_floor_id_floors_id_fk", - "tableFrom": "distribution_boards", - "columnsFrom": [ - "floor_id" - ], - "tableTo": "floors", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "floors": { - "name": "floors", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "floors_project_id_projects_id_fk": { - "name": "floors_project_id_projects_id_fk", - "tableFrom": "floors", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "global_devices": { - "name": "global_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_circuit_migrations": { - "name": "legacy_consumer_circuit_migrations", - "columns": { - "consumer_id": { - "name": "consumer_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_device_row_id": { - "name": "circuit_device_row_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "columnsFrom": [ - "circuit_id" - ], - "tableTo": "circuits", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "columnsFrom": [ - "circuit_device_row_id" - ], - "tableTo": "circuit_device_rows", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "columnsFrom": [ - "circuit_list_id" - ], - "tableTo": "circuit_lists", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_migration_reports": { - "name": "legacy_consumer_migration_reports", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "legacy_consumer_count": { - "name": "legacy_consumer_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_circuit_count": { - "name": "created_circuit_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_device_row_count": { - "name": "created_device_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_grouped_count": { - "name": "duplicate_grouped_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifier_count": { - "name": "generated_identifier_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "unassigned_row_count": { - "name": "unassigned_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "warnings_json": { - "name": "warnings_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifiers_json": { - "name": "generated_identifiers_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_groups_json": { - "name": "duplicate_groups_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "legacy_consumer_migration_reports_list_unique": { - "name": "legacy_consumer_migration_reports_list_unique", - "columns": [ - "circuit_list_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_migration_reports", - "columnsFrom": [ - "circuit_list_id" - ], - "tableTo": "circuit_lists", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_change_sets": { - "name": "project_change_sets", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_revision_id": { - "name": "project_revision_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "command_type": { - "name": "command_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_schema_version": { - "name": "payload_schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "forward_payload_json": { - "name": "forward_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "inverse_payload_json": { - "name": "inverse_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_change_sets_revision_unique": { - "name": "project_change_sets_revision_unique", - "columns": [ - "project_revision_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_change_sets_project_revision_id_project_revisions_id_fk": { - "name": "project_change_sets_project_revision_id_project_revisions_id_fk", - "tableFrom": "project_change_sets", - "columnsFrom": [ - "project_revision_id" - ], - "tableTo": "project_revisions", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_devices": { - "name": "project_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'single_phase'" - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "project_devices_project_id_projects_id_fk": { - "name": "project_devices_project_id_projects_id_fk", - "tableFrom": "project_devices", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_history_stack_entries": { - "name": "project_history_stack_entries", - "columns": { - "change_set_id": { - "name": "change_set_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "stack": { - "name": "stack", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "position": { - "name": "position", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_history_stack_entries_position_unique": { - "name": "project_history_stack_entries_position_unique", - "columns": [ - "project_id", - "stack", - "position" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { - "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", - "tableFrom": "project_history_stack_entries", - "columnsFrom": [ - "change_set_id" - ], - "tableTo": "project_change_sets", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "project_history_stack_entries_project_id_projects_id_fk": { - "name": "project_history_stack_entries_project_id_projects_id_fk", - "tableFrom": "project_history_stack_entries", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": { - "project_history_stack_entries_stack_check": { - "name": "project_history_stack_entries_stack_check", - "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" - } - } - }, - "project_revisions": { - "name": "project_revisions", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "revision_number": { - "name": "revision_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "actor_id": { - "name": "actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "source": { - "name": "source", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_revisions_project_number_unique": { - "name": "project_revisions_project_number_unique", - "columns": [ - "project_id", - "revision_number" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_revisions_project_id_projects_id_fk": { - "name": "project_revisions_project_id_projects_id_fk", - "tableFrom": "project_revisions", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_snapshots": { - "name": "project_snapshots", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_revision": { - "name": "source_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "schema_version": { - "name": "schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "kind": { - "name": "kind", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'named'" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "payload_json": { - "name": "payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_sha256": { - "name": "payload_sha256", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_by_actor_id": { - "name": "created_by_actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_snapshots_project_created_idx": { - "name": "project_snapshots_project_created_idx", - "columns": [ - "project_id", - "created_at_iso" - ], - "isUnique": false - }, - "project_snapshots_project_kind_revision_idx": { - "name": "project_snapshots_project_kind_revision_idx", - "columns": [ - "project_id", - "kind", - "source_revision" - ], - "isUnique": false - }, - "project_snapshots_project_name_unique": { - "name": "project_snapshots_project_name_unique", - "columns": [ - "project_id", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_snapshots_project_id_projects_id_fk": { - "name": "project_snapshots_project_id_projects_id_fk", - "tableFrom": "project_snapshots", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "projects": { - "name": "projects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "internal_project_number": { - "name": "internal_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "external_project_number": { - "name": "external_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "building_owner": { - "name": "building_owner", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "single_phase_voltage_v": { - "name": "single_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 230 - }, - "three_phase_voltage_v": { - "name": "three_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 400 - }, - "enabled_distribution_board_supply_types": { - "name": "enabled_distribution_board_supply_types", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'[\"AV\",\"SV\",\"EV\",\"USV\",\"MSR\",\"SiBe\"]'" - }, - "current_revision": { - "name": "current_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "rooms": { - "name": "rooms", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number": { - "name": "room_number", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "room_name": { - "name": "room_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "rooms_project_id_projects_id_fk": { - "name": "rooms_project_id_projects_id_fk", - "tableFrom": "rooms", - "columnsFrom": [ - "project_id" - ], - "tableTo": "projects", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - }, - "rooms_floor_id_floors_id_fk": { - "name": "rooms_floor_id_floors_id_fk", - "tableFrom": "rooms", - "columnsFrom": [ - "floor_id" - ], - "tableTo": "floors", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "set null" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/src/db/migrations/meta/0023_snapshot.json b/src/db/migrations/meta/0023_snapshot.json deleted file mode 100644 index 4602820..0000000 --- a/src/db/migrations/meta/0023_snapshot.json +++ /dev/null @@ -1,1893 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "f99355d0-b023-424b-870e-3ea8423497eb", - "prevId": "c9eabd70-45c9-43c3-ae63-f0685aeeb1f1", - "tables": { - "circuit_device_rows": { - "name": "circuit_device_rows", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "linked_project_device_id": { - "name": "linked_project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "legacy_consumer_id": { - "name": "legacy_consumer_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "level": { - "name": "level", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number_snapshot": { - "name": "room_number_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_name_snapshot": { - "name": "room_name_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "overridden_fields": { - "name": "overridden_fields", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "circuit_device_rows_circuit_id_circuits_id_fk": { - "name": "circuit_device_rows_circuit_id_circuits_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { - "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "project_devices", - "columnsFrom": [ - "linked_project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "circuit_device_rows_room_id_rooms_id_fk": { - "name": "circuit_device_rows_room_id_rooms_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_lists": { - "name": "circuit_lists", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_lists_distribution_board_id_unique": { - "name": "circuit_lists_distribution_board_id_unique", - "columns": [ - "distribution_board_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_lists_project_id_projects_id_fk": { - "name": "circuit_lists_project_id_projects_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_lists_distribution_board_id_distribution_boards_id_fk": { - "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_sections": { - "name": "circuit_sections", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "prefix": { - "name": "prefix", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": { - "circuit_sections_list_key_unique": { - "name": "circuit_sections_list_key_unique", - "columns": [ - "circuit_list_id", - "key" - ], - "isUnique": true - }, - "circuit_sections_list_prefix_unique": { - "name": "circuit_sections_list_prefix_unique", - "columns": [ - "circuit_list_id", - "prefix" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_sections_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_sections", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuits": { - "name": "circuits", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_length": { - "name": "cable_length", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_assignment": { - "name": "rcd_assignment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "terminal_designation": { - "name": "terminal_designation", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage": { - "name": "voltage", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "control_requirement": { - "name": "control_requirement", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_reserve": { - "name": "is_reserve", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuits_list_equipment_identifier_unique": { - "name": "circuits_list_equipment_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuits_circuit_list_id_circuit_lists_id_fk": { - "name": "circuits_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuits_section_id_circuit_sections_id_fk": { - "name": "circuits_section_id_circuit_sections_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_sections", - "columnsFrom": [ - "section_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "consumers": { - "name": "consumers", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "project_device_id": { - "name": "project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_linked_to_device": { - "name": "is_linked_to_device", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_number": { - "name": "circuit_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "device_type": { - "name": "device_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trade_or_cost_group": { - "name": "trade_or_cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "group_name": { - "name": "group_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "comment": { - "name": "comment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "consumers_project_id_projects_id_fk": { - "name": "consumers_project_id_projects_id_fk", - "tableFrom": "consumers", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "consumers_distribution_board_id_distribution_boards_id_fk": { - "name": "consumers_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "consumers", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_circuit_list_id_circuit_lists_id_fk": { - "name": "consumers_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "consumers", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_project_device_id_project_devices_id_fk": { - "name": "consumers_project_device_id_project_devices_id_fk", - "tableFrom": "consumers", - "tableTo": "project_devices", - "columnsFrom": [ - "project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_room_id_rooms_id_fk": { - "name": "consumers_room_id_rooms_id_fk", - "tableFrom": "consumers", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_boards": { - "name": "distribution_boards", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "supply_type": { - "name": "supply_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_boards_project_id_projects_id_fk": { - "name": "distribution_boards_project_id_projects_id_fk", - "tableFrom": "distribution_boards", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "distribution_boards_floor_id_floors_id_fk": { - "name": "distribution_boards_floor_id_floors_id_fk", - "tableFrom": "distribution_boards", - "tableTo": "floors", - "columnsFrom": [ - "floor_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "floors": { - "name": "floors", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "floors_project_id_projects_id_fk": { - "name": "floors_project_id_projects_id_fk", - "tableFrom": "floors", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "global_devices": { - "name": "global_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_circuit_migrations": { - "name": "legacy_consumer_circuit_migrations", - "columns": { - "consumer_id": { - "name": "consumer_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_device_row_id": { - "name": "circuit_device_row_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuit_device_rows", - "columnsFrom": [ - "circuit_device_row_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_migration_reports": { - "name": "legacy_consumer_migration_reports", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "legacy_consumer_count": { - "name": "legacy_consumer_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_circuit_count": { - "name": "created_circuit_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_device_row_count": { - "name": "created_device_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_grouped_count": { - "name": "duplicate_grouped_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifier_count": { - "name": "generated_identifier_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "unassigned_row_count": { - "name": "unassigned_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "warnings_json": { - "name": "warnings_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifiers_json": { - "name": "generated_identifiers_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_groups_json": { - "name": "duplicate_groups_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "legacy_consumer_migration_reports_list_unique": { - "name": "legacy_consumer_migration_reports_list_unique", - "columns": [ - "circuit_list_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_migration_reports", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_change_sets": { - "name": "project_change_sets", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_revision_id": { - "name": "project_revision_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "command_type": { - "name": "command_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_schema_version": { - "name": "payload_schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "forward_payload_json": { - "name": "forward_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "inverse_payload_json": { - "name": "inverse_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_change_sets_revision_unique": { - "name": "project_change_sets_revision_unique", - "columns": [ - "project_revision_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_change_sets_project_revision_id_project_revisions_id_fk": { - "name": "project_change_sets_project_revision_id_project_revisions_id_fk", - "tableFrom": "project_change_sets", - "tableTo": "project_revisions", - "columnsFrom": [ - "project_revision_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_devices": { - "name": "project_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'single_phase'" - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "project_devices_project_id_projects_id_fk": { - "name": "project_devices_project_id_projects_id_fk", - "tableFrom": "project_devices", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_history_stack_entries": { - "name": "project_history_stack_entries", - "columns": { - "change_set_id": { - "name": "change_set_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "stack": { - "name": "stack", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "position": { - "name": "position", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_history_stack_entries_position_unique": { - "name": "project_history_stack_entries_position_unique", - "columns": [ - "project_id", - "stack", - "position" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { - "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "project_change_sets", - "columnsFrom": [ - "change_set_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "project_history_stack_entries_project_id_projects_id_fk": { - "name": "project_history_stack_entries_project_id_projects_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": { - "project_history_stack_entries_stack_check": { - "name": "project_history_stack_entries_stack_check", - "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" - } - } - }, - "project_revisions": { - "name": "project_revisions", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "revision_number": { - "name": "revision_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "actor_id": { - "name": "actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "source": { - "name": "source", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_revisions_project_number_unique": { - "name": "project_revisions_project_number_unique", - "columns": [ - "project_id", - "revision_number" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_revisions_project_id_projects_id_fk": { - "name": "project_revisions_project_id_projects_id_fk", - "tableFrom": "project_revisions", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_snapshots": { - "name": "project_snapshots", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_revision": { - "name": "source_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "schema_version": { - "name": "schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "kind": { - "name": "kind", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'named'" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "payload_json": { - "name": "payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_sha256": { - "name": "payload_sha256", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_by_actor_id": { - "name": "created_by_actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_snapshots_project_created_idx": { - "name": "project_snapshots_project_created_idx", - "columns": [ - "project_id", - "created_at_iso" - ], - "isUnique": false - }, - "project_snapshots_project_kind_revision_idx": { - "name": "project_snapshots_project_kind_revision_idx", - "columns": [ - "project_id", - "kind", - "source_revision" - ], - "isUnique": false - }, - "project_snapshots_project_name_unique": { - "name": "project_snapshots_project_name_unique", - "columns": [ - "project_id", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_snapshots_project_id_projects_id_fk": { - "name": "project_snapshots_project_id_projects_id_fk", - "tableFrom": "project_snapshots", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "projects": { - "name": "projects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "internal_project_number": { - "name": "internal_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "external_project_number": { - "name": "external_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "building_owner": { - "name": "building_owner", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "single_phase_voltage_v": { - "name": "single_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 230 - }, - "three_phase_voltage_v": { - "name": "three_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 400 - }, - "enabled_distribution_board_supply_types": { - "name": "enabled_distribution_board_supply_types", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'[\"AV\",\"SV\",\"EV\",\"USV\",\"MSR\",\"SiBe\"]'" - }, - "current_revision": { - "name": "current_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "rooms": { - "name": "rooms", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number": { - "name": "room_number", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "room_name": { - "name": "room_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "rooms_project_id_projects_id_fk": { - "name": "rooms_project_id_projects_id_fk", - "tableFrom": "rooms", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "rooms_floor_id_floors_id_fk": { - "name": "rooms_floor_id_floors_id_fk", - "tableFrom": "rooms", - "tableTo": "floors", - "columnsFrom": [ - "floor_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/src/db/migrations/meta/0024_snapshot.json b/src/db/migrations/meta/0024_snapshot.json deleted file mode 100644 index bd411ce..0000000 --- a/src/db/migrations/meta/0024_snapshot.json +++ /dev/null @@ -1,2242 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "31166943-989c-4199-a8a3-a896dab14995", - "prevId": "f99355d0-b023-424b-870e-3ea8423497eb", - "tables": { - "circuit_device_rows": { - "name": "circuit_device_rows", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "linked_project_device_id": { - "name": "linked_project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "legacy_consumer_id": { - "name": "legacy_consumer_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "level": { - "name": "level", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number_snapshot": { - "name": "room_number_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_name_snapshot": { - "name": "room_name_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "overridden_fields": { - "name": "overridden_fields", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "circuit_device_rows_circuit_id_circuits_id_fk": { - "name": "circuit_device_rows_circuit_id_circuits_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { - "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "project_devices", - "columnsFrom": [ - "linked_project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "circuit_device_rows_room_id_rooms_id_fk": { - "name": "circuit_device_rows_room_id_rooms_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_list_equipment_identifiers": { - "name": "circuit_list_equipment_identifiers", - "columns": { - "owner_type": { - "name": "owner_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "owner_id": { - "name": "owner_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_list_equipment_identifiers_owner_unique": { - "name": "circuit_list_equipment_identifiers_owner_unique", - "columns": [ - "owner_type", - "owner_id" - ], - "isUnique": true - }, - "circuit_list_equipment_identifiers_list_identifier_unique": { - "name": "circuit_list_equipment_identifiers_list_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_list_equipment_identifiers", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_lists": { - "name": "circuit_lists", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_lists_distribution_board_id_unique": { - "name": "circuit_lists_distribution_board_id_unique", - "columns": [ - "distribution_board_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_lists_project_id_projects_id_fk": { - "name": "circuit_lists_project_id_projects_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_lists_distribution_board_id_distribution_boards_id_fk": { - "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_protection_devices": { - "name": "circuit_protection_devices", - "columns": { - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "rated_current_a": { - "name": "rated_current_a", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "fuse_utilization_category": { - "name": "fuse_utilization_category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trip_characteristic": { - "name": "trip_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_type": { - "name": "rcd_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rated_residual_current_ma": { - "name": "rated_residual_current_ma", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "circuit_protection_devices_circuit_id_circuits_id_fk": { - "name": "circuit_protection_devices_circuit_id_circuits_id_fk", - "tableFrom": "circuit_protection_devices", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_sections": { - "name": "circuit_sections", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "prefix": { - "name": "prefix", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "group_number": { - "name": "group_number", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuit_sections_list_key_unique": { - "name": "circuit_sections_list_key_unique", - "columns": [ - "circuit_list_id", - "key" - ], - "isUnique": true - }, - "circuit_sections_list_prefix_unique": { - "name": "circuit_sections_list_prefix_unique", - "columns": [ - "circuit_list_id", - "prefix" - ], - "isUnique": true - }, - "circuit_sections_list_category_group_unique": { - "name": "circuit_sections_list_category_group_unique", - "columns": [ - "circuit_list_id", - "category", - "group_number" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_sections_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_sections", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuits": { - "name": "circuits", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_length": { - "name": "cable_length", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_assignment": { - "name": "rcd_assignment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "terminal_designation": { - "name": "terminal_designation", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage": { - "name": "voltage", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "control_requirement": { - "name": "control_requirement", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_reserve": { - "name": "is_reserve", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuits_list_equipment_identifier_unique": { - "name": "circuits_list_equipment_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuits_circuit_list_id_circuit_lists_id_fk": { - "name": "circuits_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuits_section_id_circuit_sections_id_fk": { - "name": "circuits_section_id_circuit_sections_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_sections", - "columnsFrom": [ - "section_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "consumers": { - "name": "consumers", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "project_device_id": { - "name": "project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_linked_to_device": { - "name": "is_linked_to_device", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_number": { - "name": "circuit_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "device_type": { - "name": "device_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trade_or_cost_group": { - "name": "trade_or_cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "group_name": { - "name": "group_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_type": { - "name": "protection_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_rated_current": { - "name": "protection_rated_current", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "protection_characteristic": { - "name": "protection_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "comment": { - "name": "comment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "consumers_project_id_projects_id_fk": { - "name": "consumers_project_id_projects_id_fk", - "tableFrom": "consumers", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "consumers_distribution_board_id_distribution_boards_id_fk": { - "name": "consumers_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "consumers", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_circuit_list_id_circuit_lists_id_fk": { - "name": "consumers_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "consumers", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_project_device_id_project_devices_id_fk": { - "name": "consumers_project_device_id_project_devices_id_fk", - "tableFrom": "consumers", - "tableTo": "project_devices", - "columnsFrom": [ - "project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "consumers_room_id_rooms_id_fk": { - "name": "consumers_room_id_rooms_id_fk", - "tableFrom": "consumers", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_board_component_protection_devices": { - "name": "distribution_board_component_protection_devices", - "columns": { - "component_id": { - "name": "component_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "rated_current_a": { - "name": "rated_current_a", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "fuse_utilization_category": { - "name": "fuse_utilization_category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trip_characteristic": { - "name": "trip_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_type": { - "name": "rcd_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rated_residual_current_ma": { - "name": "rated_residual_current_ma", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk": { - "name": "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk", - "tableFrom": "distribution_board_component_protection_devices", - "tableTo": "distribution_board_components", - "columnsFrom": [ - "component_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_board_components": { - "name": "distribution_board_components", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "placement": { - "name": "placement", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": { - "distribution_board_components_list_identifier_unique": { - "name": "distribution_board_components_list_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - }, - "distribution_board_components_section_role_unique": { - "name": "distribution_board_components_section_role_unique", - "columns": [ - "section_id", - "role" - ], - "isUnique": true - } - }, - "foreignKeys": { - "distribution_board_components_circuit_list_id_circuit_lists_id_fk": { - "name": "distribution_board_components_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "distribution_board_components", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "distribution_board_components_section_id_circuit_sections_id_fk": { - "name": "distribution_board_components_section_id_circuit_sections_id_fk", - "tableFrom": "distribution_board_components", - "tableTo": "circuit_sections", - "columnsFrom": [ - "section_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_boards": { - "name": "distribution_boards", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "supply_type": { - "name": "supply_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_boards_project_id_projects_id_fk": { - "name": "distribution_boards_project_id_projects_id_fk", - "tableFrom": "distribution_boards", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "distribution_boards_floor_id_floors_id_fk": { - "name": "distribution_boards_floor_id_floors_id_fk", - "tableFrom": "distribution_boards", - "tableTo": "floors", - "columnsFrom": [ - "floor_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "floors": { - "name": "floors", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "floors_project_id_projects_id_fk": { - "name": "floors_project_id_projects_id_fk", - "tableFrom": "floors", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "global_devices": { - "name": "global_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_circuit_migrations": { - "name": "legacy_consumer_circuit_migrations", - "columns": { - "consumer_id": { - "name": "consumer_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_device_row_id": { - "name": "circuit_device_row_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuit_device_rows", - "columnsFrom": [ - "circuit_device_row_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_circuit_migrations", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "legacy_consumer_migration_reports": { - "name": "legacy_consumer_migration_reports", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "legacy_consumer_count": { - "name": "legacy_consumer_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_circuit_count": { - "name": "created_circuit_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_device_row_count": { - "name": "created_device_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_grouped_count": { - "name": "duplicate_grouped_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifier_count": { - "name": "generated_identifier_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "unassigned_row_count": { - "name": "unassigned_row_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "warnings_json": { - "name": "warnings_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "generated_identifiers_json": { - "name": "generated_identifiers_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "duplicate_groups_json": { - "name": "duplicate_groups_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "legacy_consumer_migration_reports_list_unique": { - "name": "legacy_consumer_migration_reports_list_unique", - "columns": [ - "circuit_list_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk": { - "name": "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "legacy_consumer_migration_reports", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_change_sets": { - "name": "project_change_sets", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_revision_id": { - "name": "project_revision_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "command_type": { - "name": "command_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_schema_version": { - "name": "payload_schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "forward_payload_json": { - "name": "forward_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "inverse_payload_json": { - "name": "inverse_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_change_sets_revision_unique": { - "name": "project_change_sets_revision_unique", - "columns": [ - "project_revision_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_change_sets_project_revision_id_project_revisions_id_fk": { - "name": "project_change_sets_project_revision_id_project_revisions_id_fk", - "tableFrom": "project_change_sets", - "tableTo": "project_revisions", - "columnsFrom": [ - "project_revision_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_devices": { - "name": "project_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'single_phase'" - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "project_devices_project_id_projects_id_fk": { - "name": "project_devices_project_id_projects_id_fk", - "tableFrom": "project_devices", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_history_stack_entries": { - "name": "project_history_stack_entries", - "columns": { - "change_set_id": { - "name": "change_set_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "stack": { - "name": "stack", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "position": { - "name": "position", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_history_stack_entries_position_unique": { - "name": "project_history_stack_entries_position_unique", - "columns": [ - "project_id", - "stack", - "position" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { - "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "project_change_sets", - "columnsFrom": [ - "change_set_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "project_history_stack_entries_project_id_projects_id_fk": { - "name": "project_history_stack_entries_project_id_projects_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": { - "project_history_stack_entries_stack_check": { - "name": "project_history_stack_entries_stack_check", - "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" - } - } - }, - "project_revisions": { - "name": "project_revisions", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "revision_number": { - "name": "revision_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "actor_id": { - "name": "actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "source": { - "name": "source", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_revisions_project_number_unique": { - "name": "project_revisions_project_number_unique", - "columns": [ - "project_id", - "revision_number" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_revisions_project_id_projects_id_fk": { - "name": "project_revisions_project_id_projects_id_fk", - "tableFrom": "project_revisions", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_snapshots": { - "name": "project_snapshots", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_revision": { - "name": "source_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "schema_version": { - "name": "schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "kind": { - "name": "kind", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'named'" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "payload_json": { - "name": "payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_sha256": { - "name": "payload_sha256", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_by_actor_id": { - "name": "created_by_actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_snapshots_project_created_idx": { - "name": "project_snapshots_project_created_idx", - "columns": [ - "project_id", - "created_at_iso" - ], - "isUnique": false - }, - "project_snapshots_project_kind_revision_idx": { - "name": "project_snapshots_project_kind_revision_idx", - "columns": [ - "project_id", - "kind", - "source_revision" - ], - "isUnique": false - }, - "project_snapshots_project_name_unique": { - "name": "project_snapshots_project_name_unique", - "columns": [ - "project_id", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_snapshots_project_id_projects_id_fk": { - "name": "project_snapshots_project_id_projects_id_fk", - "tableFrom": "project_snapshots", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "projects": { - "name": "projects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "internal_project_number": { - "name": "internal_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "external_project_number": { - "name": "external_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "building_owner": { - "name": "building_owner", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "single_phase_voltage_v": { - "name": "single_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 230 - }, - "three_phase_voltage_v": { - "name": "three_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 400 - }, - "enabled_distribution_board_supply_types": { - "name": "enabled_distribution_board_supply_types", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'[\"AV\",\"SV\",\"EV\",\"USV\",\"MSR\",\"SiBe\"]'" - }, - "current_revision": { - "name": "current_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "rooms": { - "name": "rooms", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number": { - "name": "room_number", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "room_name": { - "name": "room_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "rooms_project_id_projects_id_fk": { - "name": "rooms_project_id_projects_id_fk", - "tableFrom": "rooms", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "rooms_floor_id_floors_id_fk": { - "name": "rooms_floor_id_floors_id_fk", - "tableFrom": "rooms", - "tableTo": "floors", - "columnsFrom": [ - "floor_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index afa07cf..44a9552 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -5,176 +5,8 @@ { "idx": 0, "version": "6", - "when": 1777565414148, - "tag": "0000_bizarre_colossus", - "breakpoints": true - }, - { - "idx": 1, - "version": "6", - "when": 1777577000000, - "tag": "0001_global_devices", - "breakpoints": true - }, - { - "idx": 2, - "version": "6", - "when": 1777580000000, - "tag": "0002_project_voltage_defaults", - "breakpoints": true - }, - { - "idx": 3, - "version": "6", - "when": 1777589000000, - "tag": "0003_project_floors_rooms", - "breakpoints": true - }, - { - "idx": 4, - "version": "6", - "when": 1777594000000, - "tag": "0004_circuit_lists_and_entry_fields", - "breakpoints": true - }, - { - "idx": 5, - "version": "6", - "when": 1777597000000, - "tag": "0005_project_devices", - "breakpoints": true - }, - { - "idx": 6, - "version": "6", - "when": 1777652000000, - "tag": "0006_device_display_name", - "breakpoints": true - }, - { - "idx": 7, - "version": "6", - "when": 1777680000000, - "tag": "0007_consumer_device_link", - "breakpoints": true - }, - { - "idx": 8, - "version": "6", - "when": 1777800000000, - "tag": "0008_circuit_first_model", - "breakpoints": true - }, - { - "idx": 9, - "version": "6", - "when": 1784743200000, - "tag": "0009_project_device_circuit_fields", - "breakpoints": true - }, - { - "idx": 10, - "version": "6", - "when": 1784746800000, - "tag": "0010_circuit_control_requirement", - "breakpoints": true - }, - { - "idx": 11, - "version": "6", - "when": 1784832541202, - "tag": "0011_project_device_canonical_fields", - "breakpoints": true - }, - { - "idx": 12, - "version": "6", - "when": 1784833957929, - "tag": "0012_project_revision_foundation", - "breakpoints": true - }, - { - "idx": 13, - "version": "6", - "when": 1784835703230, - "tag": "0013_project_history_stacks", - "breakpoints": true - }, - { - "idx": 14, - "version": "6", - "when": 1785009799979, - "tag": "0014_project_snapshots", - "breakpoints": true - }, - { - "idx": 15, - "version": "6", - "when": 1785014383032, - "tag": "0015_modern_madame_masque", - "breakpoints": true - }, - { - "idx": 16, - "version": "6", - "when": 1785254079435, - "tag": "0016_dashing_darkstar", - "breakpoints": true - }, - { - "idx": 17, - "version": "6", - "when": 1785307189539, - "tag": "0017_vengeful_romulus", - "breakpoints": true - }, - { - "idx": 18, - "version": "6", - "when": 1785308458789, - "tag": "0018_fancy_argent", - "breakpoints": true - }, - { - "idx": 19, - "version": "6", - "when": 1785310907349, - "tag": "0019_normalize_project_voltages", - "breakpoints": true - }, - { - "idx": 20, - "version": "6", - "when": 1785314248007, - "tag": "0020_normalize_phase_types", - "breakpoints": true - }, - { - "idx": 21, - "version": "6", - "when": 1785314558555, - "tag": "0021_fill_missing_phase_types", - "breakpoints": true - }, - { - "idx": 22, - "version": "6", - "when": 1785314626476, - "tag": "0022_default_remaining_phase_types", - "breakpoints": true - }, - { - "idx": 23, - "version": "6", - "when": 1785343645615, - "tag": "0023_cheerful_night_thrasher", - "breakpoints": true - }, - { - "idx": 24, - "version": "6", - "when": 1785431250141, - "tag": "0024_damp_mister_sinister", + "when": 1785499119431, + "tag": "0000_whole_surge", "breakpoints": true } ] diff --git a/src/db/repositories/circuit-device-row-move-project-command.repository.ts b/src/db/repositories/circuit-device-row-move-project-command.repository.ts index 0325d73..2c5bfc9 100644 --- a/src/db/repositories/circuit-device-row-move-project-command.repository.ts +++ b/src/db/repositories/circuit-device-row-move-project-command.repository.ts @@ -382,9 +382,6 @@ export class CircuitDeviceRowMoveProjectCommandRepository equipmentIdentifier: snapshot.equipmentIdentifier, displayName: snapshot.displayName, sortOrder: snapshot.sortOrder, - protectionType: snapshot.protectionType, - protectionRatedCurrent: snapshot.protectionRatedCurrent, - protectionCharacteristic: snapshot.protectionCharacteristic, cableType: snapshot.cableType, cableCrossSection: snapshot.cableCrossSection, cableLength: snapshot.cableLength, @@ -423,11 +420,6 @@ export class CircuitDeviceRowMoveProjectCommandRepository snapshot.equipmentIdentifier || current.displayName !== snapshot.displayName || current.sortOrder !== snapshot.sortOrder || - current.protectionType !== snapshot.protectionType || - current.protectionRatedCurrent !== - snapshot.protectionRatedCurrent || - current.protectionCharacteristic !== - snapshot.protectionCharacteristic || current.cableType !== snapshot.cableType || current.cableCrossSection !== snapshot.cableCrossSection || current.cableLength !== snapshot.cableLength || diff --git a/src/db/repositories/circuit-device-row-structure-project-command.repository.ts b/src/db/repositories/circuit-device-row-structure-project-command.repository.ts index 40f0bf9..4d9532d 100644 --- a/src/db/repositories/circuit-device-row-structure-project-command.repository.ts +++ b/src/db/repositories/circuit-device-row-structure-project-command.repository.ts @@ -77,11 +77,6 @@ export class CircuitDeviceRowStructureProjectCommandRepository source: ExecuteCircuitDeviceRowStructureCommandInput["source"], row: CircuitDeviceRowSnapshot ) { - if (source === "user" && row.legacyConsumerId !== null) { - throw new Error( - "User commands cannot assign legacy consumer ids." - ); - } if (source === "user" && !isElectricalPhaseType(row.phaseType)) { throw new Error("Device-row phase type is invalid."); } diff --git a/src/db/repositories/circuit-device-row-structure.persistence.ts b/src/db/repositories/circuit-device-row-structure.persistence.ts index 09aee9f..fffaff4 100644 --- a/src/db/repositories/circuit-device-row-structure.persistence.ts +++ b/src/db/repositories/circuit-device-row-structure.persistence.ts @@ -49,7 +49,6 @@ export function toCircuitDeviceRowSnapshot( id: row.id, circuitId: row.circuitId, linkedProjectDeviceId: row.linkedProjectDeviceId, - legacyConsumerId: row.legacyConsumerId, sortOrder: row.sortOrder, name: row.name, displayName: row.displayName, diff --git a/src/db/repositories/circuit-device-row.persistence.ts b/src/db/repositories/circuit-device-row.persistence.ts index 47ae7ee..e1f9222 100644 --- a/src/db/repositories/circuit-device-row.persistence.ts +++ b/src/db/repositories/circuit-device-row.persistence.ts @@ -44,7 +44,6 @@ export interface CircuitDeviceRowPatchInput { export interface CircuitDeviceRowCreateInput extends CircuitDeviceRowUpdateInput { circuitId: string; linkedProjectDeviceId?: string; - legacyConsumerId?: string; sortOrder: number; } @@ -110,7 +109,6 @@ export function toCircuitDeviceRowCreateValues( return { id, circuitId: input.circuitId, - legacyConsumerId: input.legacyConsumerId ?? null, sortOrder: input.sortOrder, ...toCircuitDeviceRowUpdateValues(input), }; diff --git a/src/db/repositories/circuit-device-row.repository.ts b/src/db/repositories/circuit-device-row.repository.ts index dc6118d..6bdb550 100644 --- a/src/db/repositories/circuit-device-row.repository.ts +++ b/src/db/repositories/circuit-device-row.repository.ts @@ -25,7 +25,6 @@ export class CircuitDeviceRowRepository { id: circuitDeviceRows.id, circuitId: circuitDeviceRows.circuitId, linkedProjectDeviceId: circuitDeviceRows.linkedProjectDeviceId, - legacyConsumerId: circuitDeviceRows.legacyConsumerId, sortOrder: circuitDeviceRows.sortOrder, name: circuitDeviceRows.name, displayName: circuitDeviceRows.displayName, diff --git a/src/db/repositories/circuit-structure-project-command.repository.ts b/src/db/repositories/circuit-structure-project-command.repository.ts index 11e6fd6..8e76e99 100644 --- a/src/db/repositories/circuit-structure-project-command.repository.ts +++ b/src/db/repositories/circuit-structure-project-command.repository.ts @@ -133,11 +133,6 @@ export class CircuitStructureProjectCommandRepository throw new Error("Circuit device-row id already exists."); } for (const row of snapshot.deviceRows) { - if (source === "user" && row.legacyConsumerId !== null) { - throw new Error( - "User commands cannot assign legacy consumer ids." - ); - } if (source === "user" && !isElectricalPhaseType(row.phaseType)) { throw new Error("Device-row phase type is invalid."); } @@ -158,9 +153,6 @@ export class CircuitStructureProjectCommandRepository equipmentIdentifier: snapshot.equipmentIdentifier, displayName: snapshot.displayName, sortOrder: snapshot.sortOrder, - protectionType: snapshot.protectionType, - protectionRatedCurrent: snapshot.protectionRatedCurrent, - protectionCharacteristic: snapshot.protectionCharacteristic, cableType: snapshot.cableType, cableCrossSection: snapshot.cableCrossSection, cableLength: snapshot.cableLength, @@ -238,9 +230,6 @@ export class CircuitStructureProjectCommandRepository equipmentIdentifier: circuit.equipmentIdentifier, displayName: circuit.displayName, sortOrder: circuit.sortOrder, - protectionType: circuit.protectionType, - protectionRatedCurrent: circuit.protectionRatedCurrent, - protectionCharacteristic: circuit.protectionCharacteristic, cableType: circuit.cableType, cableCrossSection: circuit.cableCrossSection, cableLength: circuit.cableLength, diff --git a/src/db/repositories/circuit.persistence.ts b/src/db/repositories/circuit.persistence.ts index f14bc2c..96bde5d 100644 --- a/src/db/repositories/circuit.persistence.ts +++ b/src/db/repositories/circuit.persistence.ts @@ -4,9 +4,6 @@ export interface CircuitCreatePersistenceInput { equipmentIdentifier: string; displayName?: string; sortOrder: number; - protectionType?: string; - protectionRatedCurrent?: number; - protectionCharacteristic?: string; cableType?: string; cableCrossSection?: string; cableLength?: number; @@ -24,9 +21,6 @@ export interface CircuitPatchPersistenceInput { equipmentIdentifier?: string; displayName?: string | null; sortOrder?: number; - protectionType?: string | null; - protectionRatedCurrent?: number | null; - protectionCharacteristic?: string | null; cableType?: string | null; cableCrossSection?: string | null; cableLength?: number | null; @@ -47,9 +41,6 @@ export function toCircuitCreateValues(id: string, input: CircuitCreatePersistenc equipmentIdentifier: input.equipmentIdentifier, displayName: input.displayName ?? null, sortOrder: input.sortOrder, - protectionType: input.protectionType ?? null, - protectionRatedCurrent: input.protectionRatedCurrent ?? null, - protectionCharacteristic: input.protectionCharacteristic ?? null, cableType: input.cableType ?? null, cableCrossSection: input.cableCrossSection ?? null, cableLength: input.cableLength ?? null, @@ -74,13 +65,6 @@ export function toCircuitPatchValues(input: CircuitPatchPersistenceInput) { } if (has("displayName")) values.displayName = input.displayName ?? null; if (input.sortOrder !== undefined) values.sortOrder = input.sortOrder; - if (has("protectionType")) values.protectionType = input.protectionType ?? null; - if (has("protectionRatedCurrent")) { - values.protectionRatedCurrent = input.protectionRatedCurrent ?? null; - } - if (has("protectionCharacteristic")) { - values.protectionCharacteristic = input.protectionCharacteristic ?? null; - } if (has("cableType")) values.cableType = input.cableType ?? null; if (has("cableCrossSection")) { values.cableCrossSection = input.cableCrossSection ?? null; diff --git a/src/db/repositories/distribution-board-structure-project-command.repository.ts b/src/db/repositories/distribution-board-structure-project-command.repository.ts index 9e7ae6f..29a8f21 100644 --- a/src/db/repositories/distribution-board-structure-project-command.repository.ts +++ b/src/db/repositories/distribution-board-structure-project-command.repository.ts @@ -12,7 +12,6 @@ import { import { assertDistributionBoardUpdateProjectCommand, createDistributionBoardUpdateProjectCommand, - legacyDistributionBoardUpdateCommandSchemaVersion, type DistributionBoardUpdateField, type DistributionBoardUpdatePatch, type DistributionBoardUpdateProjectCommand, @@ -332,19 +331,10 @@ export class DistributionBoardStructureProjectCommandRepository getDistributionBoardFieldValue(current, change.field), ]) ) as DistributionBoardUpdatePatch; - const currentInverse = createDistributionBoardUpdateProjectCommand( + const inverse = createDistributionBoardUpdateProjectCommand( current.id, inversePatch ); - const inverse = - input.command.schemaVersion === - legacyDistributionBoardUpdateCommandSchemaVersion - ? ({ - ...currentInverse, - schemaVersion: - legacyDistributionBoardUpdateCommandSchemaVersion, - } as DistributionBoardUpdateProjectCommand) - : currentInverse; const updated = database .update(distributionBoards) .set(patch) diff --git a/src/db/repositories/legacy-consumer-migration.repository.ts b/src/db/repositories/legacy-consumer-migration.repository.ts deleted file mode 100644 index 2c3fa35..0000000 --- a/src/db/repositories/legacy-consumer-migration.repository.ts +++ /dev/null @@ -1,142 +0,0 @@ -import crypto from "node:crypto"; -import { eq, inArray, isNull } from "drizzle-orm"; -import type { - LegacyMigrationCircuitInput, - LegacyMigrationReportInput, -} from "../../domain/ports/legacy-consumer-migration.store.js"; -import type { AppDatabase } from "../database-context.js"; -import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; -import { circuits } from "../schema/circuits.js"; -import { consumers } from "../schema/consumers.js"; -import { legacyConsumerCircuitMigrations } from "../schema/legacy-consumer-circuit-migrations.js"; -import { legacyConsumerMigrationReports } from "../schema/legacy-consumer-migration-report.js"; -import { toCircuitDeviceRowCreateValues } from "./circuit-device-row.persistence.js"; -import { toCircuitCreateValues } from "./circuit.persistence.js"; - -export class LegacyConsumerMigrationRepository { - constructor(private readonly database: AppDatabase) {} - - async listSourceConsumersByCircuitList(circuitListId: string) { - return this.database - .select() - .from(consumers) - .where(eq(consumers.circuitListId, circuitListId)); - } - - 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) { - const rows = await this.database - .select({ consumerId: legacyConsumerCircuitMigrations.consumerId }) - .from(legacyConsumerCircuitMigrations) - .where(eq(legacyConsumerCircuitMigrations.circuitListId, circuitListId)); - return rows.map((row) => row.consumerId); - } - - persistCircuitListMigration(input: { - circuitListId: string; - circuits: LegacyMigrationCircuitInput[]; - report: LegacyMigrationReportInput; - }) { - if (input.circuits.some((entry) => entry.circuit.circuitListId !== input.circuitListId)) { - throw new Error("All migrated circuits must belong to the target circuit list."); - } - - const consumerIds = input.circuits.flatMap((entry) => - entry.deviceRows.map((row) => row.legacyConsumerId) - ); - if (new Set(consumerIds).size !== consumerIds.length) { - throw new Error("A legacy consumer may only be migrated once per operation."); - } - - const preparedCircuits = input.circuits.map((entry) => { - const circuitId = crypto.randomUUID(); - return { - circuitId, - circuitValues: toCircuitCreateValues(circuitId, entry.circuit), - rows: entry.deviceRows.map((row) => { - const rowId = crypto.randomUUID(); - return { - consumerId: row.legacyConsumerId, - rowId, - rowValues: toCircuitDeviceRowCreateValues(rowId, { - ...row, - circuitId, - }), - }; - }), - }; - }); - const createdAtIso = new Date().toISOString(); - - this.database.transaction((tx) => { - if (consumerIds.length > 0) { - const existingMappings = tx - .select({ consumerId: legacyConsumerCircuitMigrations.consumerId }) - .from(legacyConsumerCircuitMigrations) - .where(inArray(legacyConsumerCircuitMigrations.consumerId, consumerIds)) - .all(); - if (existingMappings.length > 0) { - throw new Error("A legacy consumer was migrated before this operation completed."); - } - } - - for (const preparedCircuit of preparedCircuits) { - tx.insert(circuits).values(preparedCircuit.circuitValues).run(); - for (const row of preparedCircuit.rows) { - tx.insert(circuitDeviceRows).values(row.rowValues).run(); - tx - .insert(legacyConsumerCircuitMigrations) - .values({ - consumerId: row.consumerId, - circuitId: preparedCircuit.circuitId, - circuitDeviceRowId: row.rowId, - circuitListId: input.circuitListId, - createdAtIso, - }) - .run(); - } - } - - const reportValues = { - ...input.report, - createdAtIso, - }; - const existingReport = tx - .select({ id: legacyConsumerMigrationReports.id }) - .from(legacyConsumerMigrationReports) - .where(eq(legacyConsumerMigrationReports.circuitListId, input.circuitListId)) - .limit(1) - .all(); - if (existingReport.length > 0) { - tx - .update(legacyConsumerMigrationReports) - .set(reportValues) - .where(eq(legacyConsumerMigrationReports.id, existingReport[0].id)) - .run(); - } else { - tx - .insert(legacyConsumerMigrationReports) - .values({ - id: crypto.randomUUID(), - circuitListId: input.circuitListId, - ...reportValues, - }) - .run(); - } - }); - } -} diff --git a/src/db/repositories/project-device-structure-project-command.repository.ts b/src/db/repositories/project-device-structure-project-command.repository.ts index fcbdb2c..c75f7f5 100644 --- a/src/db/repositories/project-device-structure-project-command.repository.ts +++ b/src/db/repositories/project-device-structure-project-command.repository.ts @@ -34,7 +34,6 @@ const circuitDeviceRowSnapshotFields = [ "id", "circuitId", "linkedProjectDeviceId", - "legacyConsumerId", "sortOrder", "name", "displayName", diff --git a/src/db/repositories/project-location-structure-project-command.repository.ts b/src/db/repositories/project-location-structure-project-command.repository.ts index 2368def..2a3466a 100644 --- a/src/db/repositories/project-location-structure-project-command.repository.ts +++ b/src/db/repositories/project-location-structure-project-command.repository.ts @@ -22,7 +22,6 @@ import type { } from "../../domain/ports/project-location-structure-project-command.store.js"; import type { AppDatabase } from "../database-context.js"; import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; -import { consumers } from "../schema/consumers.js"; import { floors } from "../schema/floors.js"; import { distributionBoards } from "../schema/distribution-boards.js"; import { projects } from "../schema/projects.js"; @@ -216,13 +215,7 @@ export class ProjectLocationStructureProjectCommandRepository .where(eq(circuitDeviceRows.roomId, expected.id)) .limit(1) .get(); - const referencedLegacyConsumer = database - .select({ id: consumers.id }) - .from(consumers) - .where(eq(consumers.roomId, expected.id)) - .limit(1) - .get(); - if (referencedDeviceRow || referencedLegacyConsumer) { + if (referencedDeviceRow) { throw new Error( "A referenced project room cannot be removed by history." ); diff --git a/src/db/repositories/project-settings-project-command.repository.ts b/src/db/repositories/project-settings-project-command.repository.ts index a5a3621..9ff2405 100644 --- a/src/db/repositories/project-settings-project-command.repository.ts +++ b/src/db/repositories/project-settings-project-command.repository.ts @@ -2,9 +2,6 @@ import { and, eq, isNotNull } from "drizzle-orm"; import { assertProjectSettingsUpdateProjectCommand, createProjectSettingsUpdateProjectCommand, - legacyProjectSettingsUpdateCommandSchemaVersion, - normalizeProjectSettingsValues, - previousProjectSettingsUpdateCommandSchemaVersion, type ProjectSettingsValues, } from "../../domain/models/project-settings-project-command.model.js"; import type { @@ -43,49 +40,22 @@ export class ProjectSettingsProjectCommandRepository if (!current) { throw new Error("Project not found."); } - const target = normalizeProjectSettingsValues(input.command, current); + const target = input.command.payload; if (projectSettingsEqual(current, target)) { throw new Error("Project settings did not change."); } - const inverse = - input.command.schemaVersion === - legacyProjectSettingsUpdateCommandSchemaVersion - ? { - schemaVersion: legacyProjectSettingsUpdateCommandSchemaVersion, - type: input.command.type, - payload: { - singlePhaseVoltageV: current.singlePhaseVoltageV, - threePhaseVoltageV: current.threePhaseVoltageV, - }, - } - : input.command.schemaVersion === - previousProjectSettingsUpdateCommandSchemaVersion - ? { - schemaVersion: - previousProjectSettingsUpdateCommandSchemaVersion, - type: input.command.type, - payload: { - name: current.name, - internalProjectNumber: current.internalProjectNumber, - externalProjectNumber: current.externalProjectNumber, - buildingOwner: current.buildingOwner, - description: current.description, - singlePhaseVoltageV: current.singlePhaseVoltageV, - threePhaseVoltageV: current.threePhaseVoltageV, - }, - } - : createProjectSettingsUpdateProjectCommand({ - name: current.name, - internalProjectNumber: current.internalProjectNumber, - externalProjectNumber: current.externalProjectNumber, - buildingOwner: current.buildingOwner, - description: current.description, - singlePhaseVoltageV: current.singlePhaseVoltageV, - threePhaseVoltageV: current.threePhaseVoltageV, - enabledDistributionBoardSupplyTypes: - current.enabledDistributionBoardSupplyTypes, - }); + const inverse = createProjectSettingsUpdateProjectCommand({ + name: current.name, + internalProjectNumber: current.internalProjectNumber, + externalProjectNumber: current.externalProjectNumber, + buildingOwner: current.buildingOwner, + description: current.description, + singlePhaseVoltageV: current.singlePhaseVoltageV, + threePhaseVoltageV: current.threePhaseVoltageV, + enabledDistributionBoardSupplyTypes: + current.enabledDistributionBoardSupplyTypes, + }); assertDisabledSupplyTypesAreUnused(tx, input.projectId, target); const updated = tx .update(projects) diff --git a/src/db/repositories/project-snapshot.repository.ts b/src/db/repositories/project-snapshot.repository.ts index 7d645cc..5274fee 100644 --- a/src/db/repositories/project-snapshot.repository.ts +++ b/src/db/repositories/project-snapshot.repository.ts @@ -4,14 +4,8 @@ import { ProjectRevisionConflictError } from "../../domain/errors/project-revisi import { ProjectSnapshotNameConflictError } from "../../domain/errors/project-snapshot-name-conflict.error.js"; import { createProjectStateRestoreCommand } from "../../domain/models/project-state-restore-command.model.js"; import { - distributionBoardProjectStateSnapshotSchemaVersion, deserializeProjectStateSnapshot, - legacyProjectStateSnapshotSchemaVersion, - previousProjectStateSnapshotSchemaVersion, projectStateSnapshotSchemaVersion, - simultaneityFactorProjectStateSnapshotSchemaVersion, - supplyTypesProjectStateSnapshotSchemaVersion, - voltageProjectStateSnapshotSchemaVersion, } from "../../domain/models/project-state-snapshot.model.js"; import type { CreateNamedProjectSnapshotInput, @@ -144,19 +138,7 @@ export class ProjectSnapshotRepository implements ProjectSnapshotStore { if (actualChecksum !== stored.payloadSha256) { throw new Error("Project snapshot checksum verification failed."); } - if ( - stored.schemaVersion !== legacyProjectStateSnapshotSchemaVersion && - stored.schemaVersion !== previousProjectStateSnapshotSchemaVersion && - stored.schemaVersion !== - distributionBoardProjectStateSnapshotSchemaVersion && - stored.schemaVersion !== - supplyTypesProjectStateSnapshotSchemaVersion && - stored.schemaVersion !== - voltageProjectStateSnapshotSchemaVersion && - stored.schemaVersion !== - simultaneityFactorProjectStateSnapshotSchemaVersion && - stored.schemaVersion !== projectStateSnapshotSchemaVersion - ) { + if (stored.schemaVersion !== projectStateSnapshotSchemaVersion) { throw new Error("Project snapshot schema version is not supported."); } const targetState = deserializeProjectStateSnapshot(stored.payloadJson); diff --git a/src/db/repositories/project-state-restore-command.repository.ts b/src/db/repositories/project-state-restore-command.repository.ts index 2d42589..aa0f05d 100644 --- a/src/db/repositories/project-state-restore-command.repository.ts +++ b/src/db/repositories/project-state-restore-command.repository.ts @@ -18,13 +18,10 @@ import { circuitLists } from "../schema/circuit-lists.js"; import { circuitProtectionDevices } from "../schema/circuit-protection-devices.js"; import { circuitSections } from "../schema/circuit-sections.js"; import { circuits } from "../schema/circuits.js"; -import { consumers } from "../schema/consumers.js"; import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js"; import { distributionBoardComponents } from "../schema/distribution-board-components.js"; import { distributionBoards } from "../schema/distribution-boards.js"; import { floors } from "../schema/floors.js"; -import { legacyConsumerCircuitMigrations } from "../schema/legacy-consumer-circuit-migrations.js"; -import { legacyConsumerMigrationReports } from "../schema/legacy-consumer-migration-report.js"; import { projectDevices } from "../schema/project-devices.js"; import { projectChangeSets } from "../schema/project-change-sets.js"; import { projects } from "../schema/projects.js"; @@ -186,10 +183,6 @@ function replaceProjectState( database: AppDatabase, state: ProjectStateSnapshot ) { - const upgradeOnlyData = captureUpgradeOnlyData( - database, - state.project.id - ); database .delete(distributionBoards) .where(eq(distributionBoards.projectId, state.project.id)) @@ -257,136 +250,6 @@ function replaceProjectState( circuitDeviceRows, state.circuits.flatMap((circuit) => circuit.deviceRows) ); - restoreCompatibleUpgradeOnlyData(database, state, upgradeOnlyData); -} - -function captureUpgradeOnlyData( - database: AppDatabase, - projectId: string -) { - const consumerLinks = database - .select({ - id: consumers.id, - distributionBoardId: consumers.distributionBoardId, - circuitListId: consumers.circuitListId, - projectDeviceId: consumers.projectDeviceId, - roomId: consumers.roomId, - }) - .from(consumers) - .where(eq(consumers.projectId, projectId)) - .all(); - const migrationMappings = database - .select({ - consumerId: legacyConsumerCircuitMigrations.consumerId, - circuitId: legacyConsumerCircuitMigrations.circuitId, - circuitDeviceRowId: - legacyConsumerCircuitMigrations.circuitDeviceRowId, - circuitListId: legacyConsumerCircuitMigrations.circuitListId, - createdAtIso: legacyConsumerCircuitMigrations.createdAtIso, - }) - .from(legacyConsumerCircuitMigrations) - .innerJoin( - circuitLists, - eq( - circuitLists.id, - legacyConsumerCircuitMigrations.circuitListId - ) - ) - .where(eq(circuitLists.projectId, projectId)) - .all(); - const migrationReports = database - .select({ - id: legacyConsumerMigrationReports.id, - circuitListId: legacyConsumerMigrationReports.circuitListId, - legacyConsumerCount: - legacyConsumerMigrationReports.legacyConsumerCount, - createdCircuitCount: - legacyConsumerMigrationReports.createdCircuitCount, - createdDeviceRowCount: - legacyConsumerMigrationReports.createdDeviceRowCount, - duplicateGroupedCount: - legacyConsumerMigrationReports.duplicateGroupedCount, - generatedIdentifierCount: - legacyConsumerMigrationReports.generatedIdentifierCount, - unassignedRowCount: - legacyConsumerMigrationReports.unassignedRowCount, - warningsJson: legacyConsumerMigrationReports.warningsJson, - generatedIdentifiersJson: - legacyConsumerMigrationReports.generatedIdentifiersJson, - duplicateGroupsJson: - legacyConsumerMigrationReports.duplicateGroupsJson, - createdAtIso: legacyConsumerMigrationReports.createdAtIso, - }) - .from(legacyConsumerMigrationReports) - .innerJoin( - circuitLists, - eq(circuitLists.id, legacyConsumerMigrationReports.circuitListId) - ) - .where(eq(circuitLists.projectId, projectId)) - .all(); - return { consumerLinks, migrationMappings, migrationReports }; -} - -function restoreCompatibleUpgradeOnlyData( - database: AppDatabase, - state: ProjectStateSnapshot, - data: ReturnType -) { - const boardIds = new Set( - state.distributionBoards.map((entry) => entry.id) - ); - const listIds = new Set(state.circuitLists.map((entry) => entry.id)); - const deviceIds = new Set( - state.projectDevices.map((entry) => entry.id) - ); - const roomIds = new Set(state.rooms.map((entry) => entry.id)); - const circuitIds = new Set(state.circuits.map((entry) => entry.id)); - const rowIds = new Set( - state.circuits.flatMap((circuit) => - circuit.deviceRows.map((entry) => entry.id) - ) - ); - - for (const link of data.consumerLinks) { - database - .update(consumers) - .set({ - distributionBoardId: - link.distributionBoardId && - boardIds.has(link.distributionBoardId) - ? link.distributionBoardId - : null, - circuitListId: - link.circuitListId && listIds.has(link.circuitListId) - ? link.circuitListId - : null, - projectDeviceId: - link.projectDeviceId && deviceIds.has(link.projectDeviceId) - ? link.projectDeviceId - : null, - roomId: - link.roomId && roomIds.has(link.roomId) ? link.roomId : null, - }) - .where(eq(consumers.id, link.id)) - .run(); - } - insertMany( - database, - legacyConsumerCircuitMigrations, - data.migrationMappings.filter( - (entry) => - listIds.has(entry.circuitListId) && - circuitIds.has(entry.circuitId) && - rowIds.has(entry.circuitDeviceRowId) - ) - ); - insertMany( - database, - legacyConsumerMigrationReports, - data.migrationReports.filter((entry) => - listIds.has(entry.circuitListId) - ) - ); } function insertMany[0]>( diff --git a/src/db/repositories/project-state-snapshot.persistence.ts b/src/db/repositories/project-state-snapshot.persistence.ts index ef20776..f82702c 100644 --- a/src/db/repositories/project-state-snapshot.persistence.ts +++ b/src/db/repositories/project-state-snapshot.persistence.ts @@ -145,9 +145,6 @@ export function readProjectStateSnapshot( equipmentIdentifier: circuits.equipmentIdentifier, displayName: circuits.displayName, sortOrder: circuits.sortOrder, - protectionType: circuits.protectionType, - protectionRatedCurrent: circuits.protectionRatedCurrent, - protectionCharacteristic: circuits.protectionCharacteristic, cableType: circuits.cableType, cableCrossSection: circuits.cableCrossSection, cableLength: circuits.cableLength, @@ -173,7 +170,6 @@ export function readProjectStateSnapshot( id: circuitDeviceRows.id, circuitId: circuitDeviceRows.circuitId, linkedProjectDeviceId: circuitDeviceRows.linkedProjectDeviceId, - legacyConsumerId: circuitDeviceRows.legacyConsumerId, sortOrder: circuitDeviceRows.sortOrder, name: circuitDeviceRows.name, displayName: circuitDeviceRows.displayName, diff --git a/src/db/schema/circuit-device-rows.ts b/src/db/schema/circuit-device-rows.ts index 6fefd18..805b590 100644 --- a/src/db/schema/circuit-device-rows.ts +++ b/src/db/schema/circuit-device-rows.ts @@ -11,7 +11,6 @@ export const circuitDeviceRows = sqliteTable("circuit_device_rows", { linkedProjectDeviceId: text("linked_project_device_id").references(() => projectDevices.id, { onDelete: "set null", }), - legacyConsumerId: text("legacy_consumer_id"), sortOrder: integer("sort_order").notNull().default(0), name: text("name").notNull(), displayName: text("display_name").notNull(), diff --git a/src/db/schema/circuits.ts b/src/db/schema/circuits.ts index 27b4683..c0c733c 100644 --- a/src/db/schema/circuits.ts +++ b/src/db/schema/circuits.ts @@ -15,9 +15,6 @@ export const circuits = sqliteTable( equipmentIdentifier: text("equipment_identifier").notNull(), displayName: text("display_name"), sortOrder: integer("sort_order").notNull().default(0), - protectionType: text("protection_type"), - protectionRatedCurrent: real("protection_rated_current"), - protectionCharacteristic: text("protection_characteristic"), cableType: text("cable_type"), cableCrossSection: text("cable_cross_section"), cableLength: real("cable_length"), diff --git a/src/db/schema/consumers.ts b/src/db/schema/consumers.ts deleted file mode 100644 index a7ee83d..0000000 --- a/src/db/schema/consumers.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core"; -import { circuitLists } from "./circuit-lists.js"; -import { distributionBoards } from "./distribution-boards.js"; -import { projectDevices } from "./project-devices.js"; -import { projects } from "./projects.js"; -import { rooms } from "./rooms.js"; - -export const consumers = sqliteTable("consumers", { - id: text("id").primaryKey(), - projectId: text("project_id") - .notNull() - .references(() => projects.id, { onDelete: "cascade" }), - distributionBoardId: text("distribution_board_id").references(() => distributionBoards.id, { - onDelete: "set null", - }), - circuitListId: text("circuit_list_id").references(() => circuitLists.id, { - onDelete: "set null", - }), - projectDeviceId: text("project_device_id").references(() => projectDevices.id, { - onDelete: "set null", - }), - isLinkedToDevice: integer("is_linked_to_device").notNull().default(0), - roomId: text("room_id").references(() => rooms.id, { - onDelete: "set null", - }), - circuitNumber: text("circuit_number"), - description: text("description"), - name: text("name").notNull(), - category: text("category"), - deviceType: text("device_type"), - phaseType: text("phase_type"), - tradeOrCostGroup: text("trade_or_cost_group"), - group: text("group_name"), - protectionType: text("protection_type"), - protectionRatedCurrent: real("protection_rated_current"), - protectionCharacteristic: text("protection_characteristic"), - cableType: text("cable_type"), - cableCrossSection: text("cable_cross_section"), - comment: text("comment"), - quantity: integer("quantity").notNull(), - installedPowerPerUnitKw: real("installed_power_per_unit_kw").notNull(), - demandFactor: real("demand_factor").notNull(), - voltageV: real("voltage_v"), - phaseCount: integer("phase_count"), - powerFactor: real("power_factor"), - note: text("note"), -}); diff --git a/src/db/schema/legacy-consumer-circuit-migrations.ts b/src/db/schema/legacy-consumer-circuit-migrations.ts deleted file mode 100644 index 04f6bbb..0000000 --- a/src/db/schema/legacy-consumer-circuit-migrations.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { sqliteTable, text } from "drizzle-orm/sqlite-core"; -import { circuitDeviceRows } from "./circuit-device-rows.js"; -import { circuitLists } from "./circuit-lists.js"; -import { circuits } from "./circuits.js"; - -export const legacyConsumerCircuitMigrations = sqliteTable("legacy_consumer_circuit_migrations", { - consumerId: text("consumer_id").primaryKey(), - circuitId: text("circuit_id") - .notNull() - .references(() => circuits.id, { onDelete: "cascade" }), - circuitDeviceRowId: text("circuit_device_row_id") - .notNull() - .references(() => circuitDeviceRows.id, { onDelete: "cascade" }), - circuitListId: text("circuit_list_id") - .notNull() - .references(() => circuitLists.id, { onDelete: "cascade" }), - createdAtIso: text("created_at_iso").notNull(), -}); - diff --git a/src/db/schema/legacy-consumer-migration-report.ts b/src/db/schema/legacy-consumer-migration-report.ts deleted file mode 100644 index 0084ef9..0000000 --- a/src/db/schema/legacy-consumer-migration-report.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { integer, sqliteTable, text, unique } from "drizzle-orm/sqlite-core"; -import { circuitLists } from "./circuit-lists.js"; - -export const legacyConsumerMigrationReports = sqliteTable( - "legacy_consumer_migration_reports", - { - id: text("id").primaryKey(), - circuitListId: text("circuit_list_id") - .notNull() - .references(() => circuitLists.id, { onDelete: "cascade" }), - legacyConsumerCount: integer("legacy_consumer_count").notNull(), - createdCircuitCount: integer("created_circuit_count").notNull(), - createdDeviceRowCount: integer("created_device_row_count").notNull(), - duplicateGroupedCount: integer("duplicate_grouped_count").notNull(), - generatedIdentifierCount: integer("generated_identifier_count").notNull(), - unassignedRowCount: integer("unassigned_row_count").notNull(), - warningsJson: text("warnings_json").notNull(), - generatedIdentifiersJson: text("generated_identifiers_json").notNull(), - duplicateGroupsJson: text("duplicate_groups_json").notNull(), - createdAtIso: text("created_at_iso").notNull(), - }, - (table) => [unique("legacy_consumer_migration_reports_list_unique").on(table.circuitListId)] -); - diff --git a/src/domain/models/circuit-device-row-structure-project-command.model.ts b/src/domain/models/circuit-device-row-structure-project-command.model.ts index 57efbdc..a897b6b 100644 --- a/src/domain/models/circuit-device-row-structure-project-command.model.ts +++ b/src/domain/models/circuit-device-row-structure-project-command.model.ts @@ -10,7 +10,6 @@ export interface CircuitDeviceRowSnapshot { id: string; circuitId: string; linkedProjectDeviceId: string | null; - legacyConsumerId: string | null; sortOrder: number; name: string; displayName: string; @@ -101,7 +100,6 @@ export function assertCircuitDeviceRowInsertProjectCommand( assertNonEmptyString(row.id, "row.id"); assertNonEmptyString(row.circuitId, "row.circuitId"); assertNullableString(row.linkedProjectDeviceId, "row.linkedProjectDeviceId"); - assertNullableString(row.legacyConsumerId, "row.legacyConsumerId"); assertFiniteNumber(row.sortOrder, "row.sortOrder"); assertNonEmptyString(row.name, "row.name"); assertNonEmptyString(row.displayName, "row.displayName"); diff --git a/src/domain/models/circuit-device-row.model.ts b/src/domain/models/circuit-device-row.model.ts index f3adfa8..2b6c11c 100644 --- a/src/domain/models/circuit-device-row.model.ts +++ b/src/domain/models/circuit-device-row.model.ts @@ -2,7 +2,6 @@ export interface CircuitDeviceRow { id: string; circuitId: string; linkedProjectDeviceId?: string; - legacyConsumerId?: string; sortOrder: number; name: string; displayName: string; diff --git a/src/domain/models/circuit-project-command.model.ts b/src/domain/models/circuit-project-command.model.ts index 4cdaaa3..af70d2f 100644 --- a/src/domain/models/circuit-project-command.model.ts +++ b/src/domain/models/circuit-project-command.model.ts @@ -8,9 +8,6 @@ export interface CircuitUpdateValues { equipmentIdentifier: string; displayName: string | null; sortOrder: number; - protectionType: string | null; - protectionRatedCurrent: number | null; - protectionCharacteristic: string | null; cableType: string | null; cableCrossSection: string | null; cableLength: number | null; @@ -118,7 +115,6 @@ function assertCircuitUpdateFieldValue( return; } if ( - field === "protectionRatedCurrent" || field === "cableLength" || field === "voltage" ) { @@ -147,9 +143,6 @@ function isCircuitUpdateField(value: string): value is CircuitUpdateField { "equipmentIdentifier", "displayName", "sortOrder", - "protectionType", - "protectionRatedCurrent", - "protectionCharacteristic", "cableType", "cableCrossSection", "cableLength", diff --git a/src/domain/models/circuit-structure-project-command.model.ts b/src/domain/models/circuit-structure-project-command.model.ts index 9cbde65..5260c4b 100644 --- a/src/domain/models/circuit-structure-project-command.model.ts +++ b/src/domain/models/circuit-structure-project-command.model.ts @@ -21,9 +21,6 @@ export interface CircuitSnapshot { equipmentIdentifier: string; displayName: string | null; sortOrder: number; - protectionType: string | null; - protectionRatedCurrent: number | null; - protectionCharacteristic: string | null; cableType: string | null; cableCrossSection: string | null; cableLength: number | null; @@ -115,8 +112,6 @@ export function assertCircuitInsertProjectCommand( assertNullableString(circuit.displayName, "circuit.displayName"); assertFiniteNumber(circuit.sortOrder, "circuit.sortOrder"); for (const field of [ - "protectionType", - "protectionCharacteristic", "cableType", "cableCrossSection", "rcdAssignment", @@ -127,10 +122,6 @@ export function assertCircuitInsertProjectCommand( ] as const) { assertNullableString(circuit[field], `circuit.${field}`); } - assertNullableNonNegativeNumber( - circuit.protectionRatedCurrent, - "circuit.protectionRatedCurrent" - ); assertNullableNonNegativeNumber( circuit.cableLength, "circuit.cableLength" diff --git a/src/domain/models/circuit-tree.model.ts b/src/domain/models/circuit-tree.model.ts index dee49ce..37d68ca 100644 --- a/src/domain/models/circuit-tree.model.ts +++ b/src/domain/models/circuit-tree.model.ts @@ -1,7 +1,6 @@ export interface CircuitTreeDeviceRow { id: string; linkedProjectDeviceId?: string; - legacyConsumerId?: string; sortOrder: number; name: string; displayName: string; @@ -29,9 +28,6 @@ export interface CircuitTreeCircuit { equipmentIdentifier: string; displayName?: string; sortOrder: number; - protectionType?: string; - protectionRatedCurrent?: number; - protectionCharacteristic?: string; cableType?: string; cableCrossSection?: string; cableLength?: number; @@ -73,17 +69,6 @@ export interface CircuitTreeResponse { footerComponents: CircuitTreeComponent[]; } -export interface LegacyMigrationReport { - circuitListId: string; - legacyConsumerCount: number; - createdCircuitCount: number; - createdDeviceRowCount: number; - groupedDuplicateCircuitNumbers: Array<{ normalizedCircuitNumber: string; count: number }>; - generatedIdentifiers: string[]; - unassignedRows: Array<{ consumerId: string; reason: string }>; - warnings: string[]; -} - import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js"; import type { DistributionBoardComponentPlacement, diff --git a/src/domain/models/circuit.model.ts b/src/domain/models/circuit.model.ts index a0bca18..3efe12c 100644 --- a/src/domain/models/circuit.model.ts +++ b/src/domain/models/circuit.model.ts @@ -5,9 +5,6 @@ export interface Circuit { equipmentIdentifier: string; displayName?: string; sortOrder: number; - protectionType?: string; - protectionRatedCurrent?: number; - protectionCharacteristic?: string; cableType?: string; cableCrossSection?: string; cableLength?: number; diff --git a/src/domain/models/distribution-board-project-command.model.ts b/src/domain/models/distribution-board-project-command.model.ts index 8f6d4fc..a41be72 100644 --- a/src/domain/models/distribution-board-project-command.model.ts +++ b/src/domain/models/distribution-board-project-command.model.ts @@ -6,8 +6,7 @@ import type { SerializedProjectCommand } from "./project-command.model.js"; export const distributionBoardUpdateCommandType = "distribution-board.update" as const; -export const legacyDistributionBoardUpdateCommandSchemaVersion = 1 as const; -export const distributionBoardUpdateCommandSchemaVersion = 2 as const; +export const distributionBoardUpdateCommandSchemaVersion = 1 as const; export interface DistributionBoardUpdateValues { floorId: string | null; @@ -33,27 +32,17 @@ export interface DistributionBoardUpdateCommandPayload { changes: DistributionBoardUpdateFieldChange[]; } -interface CurrentDistributionBoardUpdateProjectCommand +export interface DistributionBoardUpdateProjectCommand extends SerializedProjectCommand { schemaVersion: typeof distributionBoardUpdateCommandSchemaVersion; type: typeof distributionBoardUpdateCommandType; } -interface LegacyDistributionBoardUpdateProjectCommand - extends SerializedProjectCommand { - schemaVersion: typeof legacyDistributionBoardUpdateCommandSchemaVersion; - type: typeof distributionBoardUpdateCommandType; -} - -export type DistributionBoardUpdateProjectCommand = - | CurrentDistributionBoardUpdateProjectCommand - | LegacyDistributionBoardUpdateProjectCommand; - export function createDistributionBoardUpdateProjectCommand( distributionBoardId: string, patch: DistributionBoardUpdatePatch -): CurrentDistributionBoardUpdateProjectCommand { - const command: CurrentDistributionBoardUpdateProjectCommand = { +): DistributionBoardUpdateProjectCommand { + const command: DistributionBoardUpdateProjectCommand = { schemaVersion: distributionBoardUpdateCommandSchemaVersion, type: distributionBoardUpdateCommandType, payload: { @@ -72,9 +61,7 @@ export function assertDistributionBoardUpdateProjectCommand( command: SerializedProjectCommand ): asserts command is DistributionBoardUpdateProjectCommand { if ( - (command.schemaVersion !== - legacyDistributionBoardUpdateCommandSchemaVersion && - command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion) || + command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion || command.type !== distributionBoardUpdateCommandType || !isPlainObject(command.payload) ) { @@ -97,9 +84,7 @@ export function assertDistributionBoardUpdateProjectCommand( !isPlainObject(change) || (change.field !== "floorId" && change.field !== "supplyType" && - (command.schemaVersion === - legacyDistributionBoardUpdateCommandSchemaVersion || - change.field !== "simultaneityFactor")) || + change.field !== "simultaneityFactor") || seen.has(change.field) ) { throw new Error( diff --git a/src/domain/models/distribution-board-structure-project-command.model.ts b/src/domain/models/distribution-board-structure-project-command.model.ts index 6553399..f461734 100644 --- a/src/domain/models/distribution-board-structure-project-command.model.ts +++ b/src/domain/models/distribution-board-structure-project-command.model.ts @@ -18,36 +18,7 @@ export const distributionBoardInsertCommandType = "distribution-board.insert" as const; export const distributionBoardDeleteCommandType = "distribution-board.delete" as const; -export const legacyDistributionBoardStructureCommandSchemaVersion = 1 as const; -export const previousDistributionBoardStructureCommandSchemaVersion = 2 as const; -export const distributionBoardStructureCommandSchemaVersion = 3 as const; - -const legacyDefaultCircuitSectionDefinitions = [ - { - key: "lighting", - displayName: "Lighting", - prefix: "-1F", - sortOrder: 10, - }, - { - key: "single_phase", - displayName: "Single-phase circuits", - prefix: "-2F", - sortOrder: 20, - }, - { - key: "three_phase", - displayName: "Three-phase circuits", - prefix: "-3F", - sortOrder: 30, - }, - { - key: "unassigned", - displayName: "Unassigned", - prefix: "-UF", - sortOrder: 90, - }, -] as const; +export const distributionBoardStructureCommandSchemaVersion = 1 as const; export const defaultCircuitSectionDefinitions = [ { @@ -76,31 +47,6 @@ export const defaultCircuitSectionDefinitions = [ }, ] as const; -interface LegacyDistributionBoardStructureSnapshot { - distributionBoard: { - id: string; - projectId: string; - name: string; - }; - circuitList: DistributionBoardStructureSnapshot["circuitList"]; - sections: LegacyCircuitSectionSnapshot[]; -} - -interface PreviousDistributionBoardStructureSnapshot { - distributionBoard: DistributionBoardStructureSnapshot["distributionBoard"]; - circuitList: DistributionBoardStructureSnapshot["circuitList"]; - sections: LegacyCircuitSectionSnapshot[]; -} - -interface LegacyCircuitSectionSnapshot { - id: string; - circuitListId: string; - key: string; - displayName: string; - prefix: string; - sortOrder: number; -} - export interface DistributionBoardStructureSnapshot { distributionBoard: { id: string; @@ -140,22 +86,15 @@ export interface DistributionBoardStructureSnapshot { export interface NormalizedDistributionBoardStructureSnapshot { distributionBoard: DistributionBoardStructureSnapshot["distributionBoard"]; circuitList: DistributionBoardStructureSnapshot["circuitList"]; - sections: Array< - LegacyCircuitSectionSnapshot & { - category: CircuitGroupCategory | null; - groupNumber: number | null; - } - >; + sections: DistributionBoardStructureSnapshot["sections"]; components: DistributionBoardStructureSnapshot["components"]; } -interface DistributionBoardStructureCommandPayload< - TStructure = DistributionBoardStructureSnapshot, -> { - structure: TStructure; +interface DistributionBoardStructureCommandPayload { + structure: DistributionBoardStructureSnapshot; } -interface CurrentDistributionBoardStructureProjectCommand +interface DistributionBoardStructureProjectCommandBase extends SerializedProjectCommand { schemaVersion: typeof distributionBoardStructureCommandSchemaVersion; type: @@ -163,41 +102,19 @@ interface CurrentDistributionBoardStructureProjectCommand | typeof distributionBoardDeleteCommandType; } -interface LegacyDistributionBoardStructureProjectCommand - extends SerializedProjectCommand< - DistributionBoardStructureCommandPayload - > { - schemaVersion: typeof legacyDistributionBoardStructureCommandSchemaVersion; - type: - | typeof distributionBoardInsertCommandType - | typeof distributionBoardDeleteCommandType; -} - -interface PreviousDistributionBoardStructureProjectCommand - extends SerializedProjectCommand< - DistributionBoardStructureCommandPayload - > { - schemaVersion: typeof previousDistributionBoardStructureCommandSchemaVersion; - type: - | typeof distributionBoardInsertCommandType - | typeof distributionBoardDeleteCommandType; -} - export interface DistributionBoardInsertProjectCommand - extends CurrentDistributionBoardStructureProjectCommand { + extends DistributionBoardStructureProjectCommandBase { type: typeof distributionBoardInsertCommandType; } export interface DistributionBoardDeleteProjectCommand - extends CurrentDistributionBoardStructureProjectCommand { + extends DistributionBoardStructureProjectCommandBase { type: typeof distributionBoardDeleteCommandType; } export type DistributionBoardStructureProjectCommand = | DistributionBoardInsertProjectCommand - | DistributionBoardDeleteProjectCommand - | PreviousDistributionBoardStructureProjectCommand - | LegacyDistributionBoardStructureProjectCommand; + | DistributionBoardDeleteProjectCommand; export function createDistributionBoardStructureSnapshot( projectId: string, @@ -296,35 +213,6 @@ export function assertDistributionBoardDeleteProjectCommand( export function normalizeDistributionBoardStructureProjectCommand( command: DistributionBoardStructureProjectCommand ): NormalizedDistributionBoardStructureSnapshot { - if ( - command.schemaVersion === - legacyDistributionBoardStructureCommandSchemaVersion - ) { - return { - distributionBoard: { - ...command.payload.structure.distributionBoard, - floorId: null, - supplyType: null, - }, - circuitList: command.payload.structure.circuitList, - sections: command.payload.structure.sections.map( - normalizeLegacySection - ), - components: [], - }; - } - if ( - command.schemaVersion === - previousDistributionBoardStructureCommandSchemaVersion - ) { - return { - ...command.payload.structure, - sections: command.payload.structure.sections.map( - normalizeLegacySection - ), - components: [], - }; - } return command.payload.structure; } @@ -340,26 +228,10 @@ export function invertDistributionBoardStructureProjectCommand( } as DistributionBoardStructureProjectCommand; } -function normalizeLegacySection( - section: LegacyCircuitSectionSnapshot -): NormalizedDistributionBoardStructureSnapshot["sections"][number] { - const category = - section.key === "lighting" || - section.key === "single_phase" || - section.key === "three_phase" - ? section.key - : null; - return { - ...section, - category, - groupNumber: category === null ? null : 1, - }; -} - export function assertDistributionBoardStructureSnapshot( structure: unknown ): asserts structure is DistributionBoardStructureSnapshot { - assertDistributionBoardStructureSnapshotVersion(structure, "current"); + assertDistributionBoardStructureSnapshotVersion(structure); } function assertDistributionBoardStructureProjectCommand( @@ -375,63 +247,36 @@ function assertDistributionBoardStructureProjectCommand( ) { throw new Error("Unsupported distribution-board structure command."); } - if ( - command.schemaVersion === - legacyDistributionBoardStructureCommandSchemaVersion - ) { - assertDistributionBoardStructureSnapshotVersion( - command.payload.structure, - "legacy" - ); - return; - } - if ( - command.schemaVersion === - previousDistributionBoardStructureCommandSchemaVersion - ) { - assertDistributionBoardStructureSnapshotVersion( - command.payload.structure, - "previous" - ); - return; - } if ( command.schemaVersion !== distributionBoardStructureCommandSchemaVersion ) { throw new Error("Unsupported distribution-board structure command."); } - assertDistributionBoardStructureSnapshotVersion( - command.payload.structure, - "current" - ); + assertDistributionBoardStructureSnapshotVersion(command.payload.structure); } function assertDistributionBoardStructureSnapshotVersion( - structure: unknown, - version: "legacy" | "previous" | "current" + structure: unknown ) { - const current = version === "current"; if ( !isPlainObject(structure) || - Object.keys(structure).length !== (current ? 4 : 3) + Object.keys(structure).length !== 4 ) { throw new Error("Distribution-board structure is invalid."); } const { distributionBoard, circuitList, sections, components } = structure; - const expectedSections = current - ? defaultCircuitSectionDefinitions - : legacyDefaultCircuitSectionDefinitions; + const expectedSections = defaultCircuitSectionDefinitions; if ( !isPlainObject(distributionBoard) || - Object.keys(distributionBoard).length !== - (version === "legacy" ? 3 : 5) || + Object.keys(distributionBoard).length !== 5 || !isPlainObject(circuitList) || Object.keys(circuitList).length !== 4 || !Array.isArray(sections) || sections.length !== expectedSections.length || - (current && (!Array.isArray(components) || components.length !== 2)) + !Array.isArray(components) || + components.length !== 2 ) { throw new Error("Distribution-board structure is incomplete."); } @@ -441,10 +286,8 @@ function assertDistributionBoardStructureSnapshotVersion( `distributionBoard.${field}` ); } - if (version !== "legacy") { - assertNullableId(distributionBoard.floorId, "distributionBoard.floorId"); - assertNullableSupplyType(distributionBoard.supplyType); - } + assertNullableId(distributionBoard.floorId, "distributionBoard.floorId"); + assertNullableSupplyType(distributionBoard.supplyType); for (const [field, value] of Object.entries(circuitList)) { assertNonEmptyString(value, `circuitList.${field}`); } @@ -462,7 +305,7 @@ function assertDistributionBoardStructureSnapshotVersion( const expected = expectedSections[index]; if ( !isPlainObject(section) || - Object.keys(section).length !== (current ? 8 : 6) + Object.keys(section).length !== 8 ) { throw new Error("Default circuit section is invalid."); } @@ -477,10 +320,8 @@ function assertDistributionBoardStructureSnapshotVersion( section.displayName !== expected.displayName || section.prefix !== expected.prefix || section.sortOrder !== expected.sortOrder || - (current && - (!("category" in expected) || - section.category !== expected.category || - section.groupNumber !== expected.groupNumber)) + section.category !== expected.category || + section.groupNumber !== expected.groupNumber ) { throw new Error( "Distribution-board structure has invalid default sections." @@ -488,9 +329,7 @@ function assertDistributionBoardStructureSnapshotVersion( } } - if (current) { - assertDefaultComponents(components, circuitList.id); - } + assertDefaultComponents(components, circuitList.id); } function assertDefaultComponents( diff --git a/src/domain/models/distribution-board-subtree-snapshot.model.ts b/src/domain/models/distribution-board-subtree-snapshot.model.ts index ba55059..be896a0 100644 --- a/src/domain/models/distribution-board-subtree-snapshot.model.ts +++ b/src/domain/models/distribution-board-subtree-snapshot.model.ts @@ -156,7 +156,6 @@ export function cloneDistributionBoardSubtree( ...row, id: createId(), circuitId, - legacyConsumerId: null, })), protectionDevice: circuit.protectionDevice === null || diff --git a/src/domain/models/project-settings-project-command.model.ts b/src/domain/models/project-settings-project-command.model.ts index c14a3cd..37bb132 100644 --- a/src/domain/models/project-settings-project-command.model.ts +++ b/src/domain/models/project-settings-project-command.model.ts @@ -6,54 +6,29 @@ import { export const projectSettingsUpdateCommandType = "project.update-settings" as const; -export const legacyProjectSettingsUpdateCommandSchemaVersion = 1 as const; -export const previousProjectSettingsUpdateCommandSchemaVersion = 2 as const; -export const projectSettingsUpdateCommandSchemaVersion = 3 as const; +export const projectSettingsUpdateCommandSchemaVersion = 1 as const; -export interface LegacyProjectSettingsValues { - singlePhaseVoltageV: number; - threePhaseVoltageV: number; -} - -export interface PreviousProjectSettingsValues extends LegacyProjectSettingsValues { +export interface ProjectSettingsValues { name: string; internalProjectNumber: string | null; externalProjectNumber: string | null; buildingOwner: string | null; description: string | null; -} - -export interface ProjectSettingsValues extends PreviousProjectSettingsValues { + singlePhaseVoltageV: number; + threePhaseVoltageV: number; enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[]; } -export interface LegacyProjectSettingsUpdateProjectCommand - extends SerializedProjectCommand { - schemaVersion: typeof legacyProjectSettingsUpdateCommandSchemaVersion; - type: typeof projectSettingsUpdateCommandType; -} - -export interface CurrentProjectSettingsUpdateProjectCommand +export interface ProjectSettingsUpdateProjectCommand extends SerializedProjectCommand { schemaVersion: typeof projectSettingsUpdateCommandSchemaVersion; type: typeof projectSettingsUpdateCommandType; } -export interface PreviousProjectSettingsUpdateProjectCommand - extends SerializedProjectCommand { - schemaVersion: typeof previousProjectSettingsUpdateCommandSchemaVersion; - type: typeof projectSettingsUpdateCommandType; -} - -export type ProjectSettingsUpdateProjectCommand = - | LegacyProjectSettingsUpdateProjectCommand - | PreviousProjectSettingsUpdateProjectCommand - | CurrentProjectSettingsUpdateProjectCommand; - export function createProjectSettingsUpdateProjectCommand( values: ProjectSettingsValues -): CurrentProjectSettingsUpdateProjectCommand { - const command: CurrentProjectSettingsUpdateProjectCommand = { +): ProjectSettingsUpdateProjectCommand { + const command: ProjectSettingsUpdateProjectCommand = { schemaVersion: projectSettingsUpdateCommandSchemaVersion, type: projectSettingsUpdateCommandType, payload: { ...values }, @@ -67,11 +42,7 @@ export function assertProjectSettingsUpdateProjectCommand( ): asserts command is ProjectSettingsUpdateProjectCommand { if ( command.type !== projectSettingsUpdateCommandType || - (command.schemaVersion !== - legacyProjectSettingsUpdateCommandSchemaVersion && - command.schemaVersion !== - previousProjectSettingsUpdateCommandSchemaVersion && - command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion) + command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion ) { throw new Error("Unsupported project settings update command."); } @@ -80,14 +51,6 @@ export function assertProjectSettingsUpdateProjectCommand( "Project settings update command contains invalid values." ); } - if (command.schemaVersion === legacyProjectSettingsUpdateCommandSchemaVersion) { - assertLegacyValues(command.payload); - return; - } - const expectedKeyCount = - command.schemaVersion === previousProjectSettingsUpdateCommandSchemaVersion - ? 7 - : 8; if ( !isTrimmedString(command.payload.name, 1, 200) || !isNullableTrimmedString(command.payload.internalProjectNumber, 100) || @@ -96,14 +59,13 @@ export function assertProjectSettingsUpdateProjectCommand( !isNullableTrimmedString(command.payload.description, 2000) || !isPositiveFiniteNumber(command.payload.singlePhaseVoltageV) || !isPositiveFiniteNumber(command.payload.threePhaseVoltageV) || - Object.keys(command.payload).length !== expectedKeyCount + Object.keys(command.payload).length !== 8 ) { throw new Error( "Project settings update command contains invalid values." ); } if ( - command.schemaVersion === projectSettingsUpdateCommandSchemaVersion && !isValidSupplyTypes( command.payload.enabledDistributionBoardSupplyTypes ) @@ -114,28 +76,6 @@ export function assertProjectSettingsUpdateProjectCommand( } } -export function normalizeProjectSettingsValues( - command: ProjectSettingsUpdateProjectCommand, - current: ProjectSettingsValues -): ProjectSettingsValues { - if (command.schemaVersion === legacyProjectSettingsUpdateCommandSchemaVersion) { - return { - ...current, - ...command.payload, - }; - } - if ( - command.schemaVersion === previousProjectSettingsUpdateCommandSchemaVersion - ) { - return { - ...command.payload, - enabledDistributionBoardSupplyTypes: - current.enabledDistributionBoardSupplyTypes, - }; - } - return command.payload; -} - function isValidSupplyTypes( value: unknown ): value is DistributionBoardSupplyType[] { @@ -153,20 +93,6 @@ function isValidSupplyTypes( ); } -function assertLegacyValues( - payload: Record -): asserts payload is Record & LegacyProjectSettingsValues { - if ( - !isPositiveFiniteNumber(payload.singlePhaseVoltageV) || - !isPositiveFiniteNumber(payload.threePhaseVoltageV) || - Object.keys(payload).length !== 2 - ) { - throw new Error( - "Project settings update command contains invalid voltages." - ); - } -} - function isPositiveFiniteNumber(value: unknown): value is number { return ( typeof value === "number" && diff --git a/src/domain/models/project-state-snapshot.model.ts b/src/domain/models/project-state-snapshot.model.ts index 80eb6d1..53e2ed2 100644 --- a/src/domain/models/project-state-snapshot.model.ts +++ b/src/domain/models/project-state-snapshot.model.ts @@ -1,11 +1,7 @@ import { z } from "zod"; -import { - defaultDistributionBoardSupplyTypes, - distributionBoardSupplyTypes, -} from "../../shared/constants/distribution-board.js"; +import { distributionBoardSupplyTypes } from "../../shared/constants/distribution-board.js"; import { circuitGroupCategories, - type CircuitGroupCategory, } from "../../shared/constants/circuit-group.js"; import { distributionBoardComponentPlacements, @@ -21,61 +17,38 @@ import { import { resolveCircuitPhaseType, resolveProjectVoltage, - normalizeKnownElectricalPhaseType, } from "../services/project-voltage.service.js"; -export const legacyProjectStateSnapshotSchemaVersion = 1 as const; -export const previousProjectStateSnapshotSchemaVersion = 2 as const; -export const distributionBoardProjectStateSnapshotSchemaVersion = 3 as const; -export const supplyTypesProjectStateSnapshotSchemaVersion = 4 as const; -export const voltageProjectStateSnapshotSchemaVersion = 5 as const; -export const simultaneityFactorProjectStateSnapshotSchemaVersion = 6 as const; -export const projectStateSnapshotSchemaVersion = 7 as const; +export const projectStateSnapshotSchemaVersion = 1 as const; const idSchema = z.string().trim().min(1); const nullableStringSchema = z.string().nullable(); const finiteNumberSchema = z.number().finite(); -const legacyProjectSchema = z +const projectSchema = z .object({ id: idSchema, name: z.string().trim().min(1), + internalProjectNumber: z.string().trim().min(1).max(100).nullable(), + externalProjectNumber: z.string().trim().min(1).max(100).nullable(), + buildingOwner: z.string().trim().min(1).max(200).nullable(), + description: z.string().trim().min(1).max(2000).nullable(), singlePhaseVoltageV: finiteNumberSchema.positive(), threePhaseVoltageV: finiteNumberSchema.positive(), + enabledDistributionBoardSupplyTypes: z + .array(z.enum(distributionBoardSupplyTypes)) + .min(1) + .refine((values) => new Set(values).size === values.length), }) .strict(); -const projectSchema = legacyProjectSchema.extend({ - internalProjectNumber: z.string().trim().min(1).max(100).nullable(), - externalProjectNumber: z.string().trim().min(1).max(100).nullable(), - buildingOwner: z.string().trim().min(1).max(200).nullable(), - description: z.string().trim().min(1).max(2000).nullable(), -}); - -const currentProjectSchema = projectSchema.extend({ - enabledDistributionBoardSupplyTypes: z - .array(z.enum(distributionBoardSupplyTypes)) - .min(1) - .refine((values) => new Set(values).size === values.length), -}); - -const legacyDistributionBoardSchema = z +const distributionBoardSchema = z .object({ id: idSchema, projectId: idSchema, name: z.string().trim().min(1), - }) - .strict(); - -const distributionBoardSchema = legacyDistributionBoardSchema - .extend({ floorId: idSchema.nullable(), supplyType: z.enum(distributionBoardSupplyTypes).nullable(), - }) - .strict(); - -const currentDistributionBoardSchema = distributionBoardSchema - .extend({ simultaneityFactor: finiteNumberSchema.min(0).max(1), }) .strict(); @@ -89,7 +62,7 @@ const circuitListSchema = z }) .strict(); -const legacyCircuitSectionSchema = z +const circuitSectionSchema = z .object({ id: idSchema, circuitListId: idSchema, @@ -97,11 +70,6 @@ const legacyCircuitSectionSchema = z displayName: z.string().trim().min(1), prefix: z.string().trim().min(1), sortOrder: finiteNumberSchema, - }) - .strict(); - -const circuitSectionSchema = legacyCircuitSectionSchema - .extend({ category: z.enum(circuitGroupCategories).nullable(), groupNumber: z.number().int().positive().nullable(), }) @@ -121,11 +89,10 @@ const circuitDeviceRowSchema = z id: idSchema, circuitId: idSchema, linkedProjectDeviceId: idSchema.nullable(), - legacyConsumerId: z.string().nullable(), sortOrder: finiteNumberSchema, name: z.string().trim().min(1), displayName: z.string().trim().min(1), - phaseType: nullableStringSchema, + phaseType: z.enum(["single_phase", "three_phase"]).nullable(), connectionKind: nullableStringSchema, costGroup: nullableStringSchema, category: nullableStringSchema, @@ -150,9 +117,6 @@ const circuitSchema = z equipmentIdentifier: z.string().trim().min(1), displayName: nullableStringSchema, sortOrder: finiteNumberSchema, - protectionType: nullableStringSchema, - protectionRatedCurrent: finiteNumberSchema.nonnegative().nullable(), - protectionCharacteristic: nullableStringSchema, cableType: nullableStringSchema, cableCrossSection: nullableStringSchema, cableLength: finiteNumberSchema.nonnegative().nullable(), @@ -280,18 +244,13 @@ const componentProtectionDeviceSchema = z .strict() .superRefine(validatePersistedProtectionDevice); -const legacyProjectStateSnapshotContents = { +const projectStateSnapshotContents = { circuitLists: z.array(circuitListSchema), - circuitSections: z.array(legacyCircuitSectionSchema), + circuitSections: z.array(circuitSectionSchema), circuits: z.array(circuitSchema), projectDevices: z.array(projectDeviceSchema), floors: z.array(floorSchema), rooms: z.array(roomSchema), -}; - -const currentProjectStateSnapshotContents = { - ...legacyProjectStateSnapshotContents, - circuitSections: z.array(circuitSectionSchema), distributionBoardComponents: z.array(distributionBoardComponentSchema), circuitProtectionDevices: z.array(circuitProtectionDeviceSchema), distributionBoardComponentProtectionDevices: z.array( @@ -299,64 +258,14 @@ const currentProjectStateSnapshotContents = { ), }; -const legacyProjectStateSnapshotSchema = z +export const projectStateSnapshotSchema = z .object({ - schemaVersion: z.literal(legacyProjectStateSnapshotSchemaVersion), - project: legacyProjectSchema, - distributionBoards: z.array(legacyDistributionBoardSchema), - ...legacyProjectStateSnapshotContents, - }) - .strict(); - -const previousProjectStateSnapshotSchema = z - .object({ - schemaVersion: z.literal(previousProjectStateSnapshotSchemaVersion), - project: projectSchema, - distributionBoards: z.array(legacyDistributionBoardSchema), - ...legacyProjectStateSnapshotContents, - }) - .strict(); - -const distributionBoardProjectStateSnapshotSchema = z - .object({ - schemaVersion: z.literal( - distributionBoardProjectStateSnapshotSchemaVersion - ), - project: projectSchema, - distributionBoards: z.array(distributionBoardSchema), - ...legacyProjectStateSnapshotContents, - }) - .strict(); - -const supplyTypesProjectStateSnapshotSchema = z - .object({ - schemaVersion: z.literal(supplyTypesProjectStateSnapshotSchemaVersion), - project: currentProjectSchema, - distributionBoards: z.array(distributionBoardSchema), - ...legacyProjectStateSnapshotContents, - }) - .strict(); - -const voltageProjectStateSnapshotSchema = - supplyTypesProjectStateSnapshotSchema.extend({ - schemaVersion: z.literal( - voltageProjectStateSnapshotSchemaVersion - ), - }); - -const simultaneityFactorProjectStateSnapshotSchema = - voltageProjectStateSnapshotSchema.extend({ - schemaVersion: z.literal( - simultaneityFactorProjectStateSnapshotSchemaVersion - ), - distributionBoards: z.array(currentDistributionBoardSchema), - }); - -export const projectStateSnapshotSchema = - simultaneityFactorProjectStateSnapshotSchema.extend({ schemaVersion: z.literal(projectStateSnapshotSchemaVersion), - ...currentProjectStateSnapshotContents, - }); + project: projectSchema, + distributionBoards: z.array(distributionBoardSchema), + ...projectStateSnapshotContents, + }) + .strict(); export type ProjectStateSnapshot = z.infer< typeof projectStateSnapshotSchema @@ -365,68 +274,7 @@ export type ProjectStateSnapshot = z.infer< export function parseProjectStateSnapshot( value: unknown ): ProjectStateSnapshot { - const version = isPlainObject(value) ? value.schemaVersion : undefined; - const parsedSnapshot = - version === legacyProjectStateSnapshotSchemaVersion - ? upgradeSimultaneityFactorProjectStateSnapshot( - upgradeVoltageProjectStateSnapshot( - upgradeSupplyTypesProjectStateSnapshot( - upgradeDistributionBoardProjectStateSnapshot( - upgradePreviousProjectStateSnapshot( - upgradeLegacyProjectStateSnapshot( - legacyProjectStateSnapshotSchema.parse(value) - ) - ) - ) - ) - ) - ) - : version === previousProjectStateSnapshotSchemaVersion - ? upgradeSimultaneityFactorProjectStateSnapshot( - upgradeVoltageProjectStateSnapshot( - upgradeSupplyTypesProjectStateSnapshot( - upgradeDistributionBoardProjectStateSnapshot( - upgradePreviousProjectStateSnapshot( - previousProjectStateSnapshotSchema.parse(value) - ) - ) - ) - ) - ) - : version === distributionBoardProjectStateSnapshotSchemaVersion - ? upgradeSimultaneityFactorProjectStateSnapshot( - upgradeVoltageProjectStateSnapshot( - upgradeSupplyTypesProjectStateSnapshot( - upgradeDistributionBoardProjectStateSnapshot( - distributionBoardProjectStateSnapshotSchema.parse( - value - ) - ) - ) - ) - ) - : version === supplyTypesProjectStateSnapshotSchemaVersion - ? upgradeSimultaneityFactorProjectStateSnapshot( - upgradeVoltageProjectStateSnapshot( - upgradeSupplyTypesProjectStateSnapshot( - supplyTypesProjectStateSnapshotSchema.parse(value) - ) - ) - ) - : version === voltageProjectStateSnapshotSchemaVersion - ? upgradeSimultaneityFactorProjectStateSnapshot( - upgradeVoltageProjectStateSnapshot( - voltageProjectStateSnapshotSchema.parse(value) - ) - ) - : version === - simultaneityFactorProjectStateSnapshotSchemaVersion - ? upgradeSimultaneityFactorProjectStateSnapshot( - simultaneityFactorProjectStateSnapshotSchema.parse( - value - ) - ) - : projectStateSnapshotSchema.parse(value); + const parsedSnapshot = projectStateSnapshotSchema.parse(value); const snapshot = normalizeSnapshotPhaseTypes(parsedSnapshot); assertProjectStateSnapshotRelations(snapshot); return snapshot; @@ -446,15 +294,13 @@ function normalizeSnapshotPhaseTypes( circuits: snapshot.circuits.map((circuit) => { const section = sectionById.get(circuit.sectionId); const deviceRows = circuit.deviceRows.map((row) => { - const normalizedPhaseType = - normalizeKnownElectricalPhaseType(row.phaseType); const linkedPhaseType = row.linkedProjectDeviceId ? projectDeviceById.get(row.linkedProjectDeviceId)?.phaseType : undefined; return { ...row, phaseType: - normalizedPhaseType ?? + row.phaseType ?? linkedPhaseType ?? (section?.key === "three_phase" ? "three_phase" @@ -478,132 +324,6 @@ function normalizeSnapshotPhaseTypes( }; } -function upgradeLegacyProjectStateSnapshot( - snapshot: z.infer -): z.infer { - return { - ...snapshot, - schemaVersion: previousProjectStateSnapshotSchemaVersion, - project: { - ...snapshot.project, - internalProjectNumber: null, - externalProjectNumber: null, - buildingOwner: null, - description: null, - }, - }; -} - -function upgradePreviousProjectStateSnapshot( - snapshot: z.infer -): z.infer { - return { - ...snapshot, - schemaVersion: distributionBoardProjectStateSnapshotSchemaVersion, - distributionBoards: snapshot.distributionBoards.map((board) => ({ - ...board, - floorId: null, - supplyType: null, - })), - }; -} - -function upgradeDistributionBoardProjectStateSnapshot( - snapshot: z.infer -): z.infer { - return { - ...snapshot, - schemaVersion: supplyTypesProjectStateSnapshotSchemaVersion, - project: { - ...snapshot.project, - enabledDistributionBoardSupplyTypes: [ - ...defaultDistributionBoardSupplyTypes, - ], - }, - }; -} - -function upgradeSupplyTypesProjectStateSnapshot( - snapshot: z.infer -): z.infer { - const settings = snapshot.project; - const sectionById = new Map( - snapshot.circuitSections.map((section) => [section.id, section]) - ); - return { - ...snapshot, - schemaVersion: voltageProjectStateSnapshotSchemaVersion, - projectDevices: snapshot.projectDevices.map((device) => ({ - ...device, - voltageV: resolveProjectVoltage(device.phaseType, settings), - })), - circuits: snapshot.circuits.map((circuit) => { - const section = sectionById.get(circuit.sectionId); - if (!section) { - return circuit; - } - const phaseType = resolveCircuitPhaseType( - section.key, - circuit.deviceRows.map((row) => row.phaseType) - ); - return { - ...circuit, - voltage: resolveProjectVoltage(phaseType, settings), - }; - }), - }; -} - -function upgradeVoltageProjectStateSnapshot( - snapshot: z.infer -): z.infer { - return { - ...snapshot, - schemaVersion: simultaneityFactorProjectStateSnapshotSchemaVersion, - distributionBoards: snapshot.distributionBoards.map((board) => ({ - ...board, - simultaneityFactor: 1, - })), - }; -} - -function upgradeSimultaneityFactorProjectStateSnapshot( - snapshot: z.infer -): ProjectStateSnapshot { - return { - ...snapshot, - schemaVersion: projectStateSnapshotSchemaVersion, - circuitSections: snapshot.circuitSections.map((section) => { - const category = initialCategoryBySectionKey(section.key); - return { - ...section, - category, - groupNumber: category === null ? null : 1, - }; - }), - distributionBoardComponents: [], - circuitProtectionDevices: [], - distributionBoardComponentProtectionDevices: [], - }; -} - -function initialCategoryBySectionKey( - key: string -): CircuitGroupCategory | null { - if ( - key === "lighting" || - key === "single_phase" || - key === "three_phase" - ) { - return key; - } - return null; -} - -function isPlainObject(value: unknown): value is Record { - return value !== null && typeof value === "object" && !Array.isArray(value); -} - export function serializeProjectStateSnapshot( snapshot: ProjectStateSnapshot ): string { diff --git a/src/domain/models/project-transfer.model.ts b/src/domain/models/project-transfer.model.ts index c6de1f7..47c8804 100644 --- a/src/domain/models/project-transfer.model.ts +++ b/src/domain/models/project-transfer.model.ts @@ -156,7 +156,6 @@ export function remapProjectState( row.linkedProjectDeviceId === null ? null : requiredId(deviceIds, row.linkedProjectDeviceId), - legacyConsumerId: null, roomId: row.roomId === null ? null : requiredId(roomIds, row.roomId), })), diff --git a/src/domain/ports/legacy-consumer-migration.store.ts b/src/domain/ports/legacy-consumer-migration.store.ts deleted file mode 100644 index 13d0904..0000000 --- a/src/domain/ports/legacy-consumer-migration.store.ts +++ /dev/null @@ -1,143 +0,0 @@ -export interface LegacyConsumerMigrationCircuitListReader { - findById( - projectId: string, - circuitListId: string - ): Promise<{ id: string } | null>; -} - -export interface LegacyConsumerMigrationSection { - id: string; - key: string; - prefix: string; -} - -export interface LegacyConsumerMigrationSectionStore { - createDefaults(circuitListId: string): Promise; - listByCircuitList( - circuitListId: string - ): Promise; -} - -export interface LegacyConsumerMigrationCircuitReader { - listByCircuitList(circuitListId: string): Promise< - Array<{ - sectionId: string; - equipmentIdentifier: string; - sortOrder: number; - }> - >; -} - -export interface LegacyConsumerMigrationRoomReader { - listByProject(projectId: string): Promise< - Array<{ - id: string; - roomNumber: string; - roomName: string; - }> - >; -} - -export interface LegacyConsumerSource { - id: string; - projectDeviceId: string | null; - roomId: string | null; - circuitNumber: string | null; - description: string | null; - name: string; - category: string | null; - deviceType: string | null; - phaseType: string | null; - tradeOrCostGroup: string | null; - protectionType: string | null; - protectionRatedCurrent: number | null; - protectionCharacteristic: string | null; - cableType: string | null; - cableCrossSection: string | null; - comment: string | null; - quantity: number; - installedPowerPerUnitKw: number; - demandFactor: number; - voltageV: number | null; - phaseCount: number | null; - powerFactor: number | null; - note: string | null; -} - -export interface LegacyMigrationDeviceRowInput { - linkedProjectDeviceId?: string; - legacyConsumerId: string; - sortOrder: number; - name: string; - displayName: string; - phaseType?: string; - connectionKind?: string; - costGroup?: string; - category?: string; - level?: string; - roomId?: string; - roomNumberSnapshot?: string; - roomNameSnapshot?: string; - quantity: number; - powerPerUnit: number; - simultaneityFactor: number; - cosPhi?: number; - remark?: string; - overriddenFields?: string; -} - -export interface LegacyMigrationCircuitInput { - circuit: { - circuitListId: string; - sectionId: string; - equipmentIdentifier: string; - displayName?: string; - sortOrder: number; - protectionType?: string; - protectionRatedCurrent?: number; - protectionCharacteristic?: string; - cableType?: string; - cableCrossSection?: string; - cableLength?: number; - voltage?: number; - controlRequirement?: string; - remark?: string; - rcdAssignment?: string; - terminalDesignation?: string; - status?: string; - isReserve?: boolean; - }; - deviceRows: LegacyMigrationDeviceRowInput[]; -} - -export interface LegacyMigrationReportInput { - legacyConsumerCount: number; - createdCircuitCount: number; - createdDeviceRowCount: number; - duplicateGroupedCount: number; - generatedIdentifierCount: number; - unassignedRowCount: number; - warningsJson: string; - generatedIdentifiersJson: string; - duplicateGroupsJson: string; -} - -export interface LegacyConsumerMigrationStore { - listSourceConsumersByCircuitList( - circuitListId: string - ): Promise; - listMigratedConsumerIds(circuitListId: string): Promise; - persistCircuitListMigration(input: { - circuitListId: string; - circuits: LegacyMigrationCircuitInput[]; - report: LegacyMigrationReportInput; - }): void; -} - -export interface LegacyConsumerMigrationDependencies { - circuitListReader: LegacyConsumerMigrationCircuitListReader; - sectionStore: LegacyConsumerMigrationSectionStore; - circuitReader: LegacyConsumerMigrationCircuitReader; - roomReader: LegacyConsumerMigrationRoomReader; - migrationStore: LegacyConsumerMigrationStore; -} diff --git a/src/domain/services/legacy-consumer-migration-planner.ts b/src/domain/services/legacy-consumer-migration-planner.ts deleted file mode 100644 index fce7cbb..0000000 --- a/src/domain/services/legacy-consumer-migration-planner.ts +++ /dev/null @@ -1,60 +0,0 @@ -export interface LegacyConsumerForPlanning { - id: string; - circuitNumber: string | null; - category: string | null; - phaseType: string | null; - phaseCount: number | null; -} - -// Accepts only normalized BMK-like legacy circuit numbers used for grouping. -export function normalizeCircuitNumber(value: string | null): string | null { - if (!value) { - return null; - } - const trimmed = value.trim().toUpperCase(); - if (!trimmed) { - return null; - } - if (!/^-\d+F\d+$/.test(trimmed)) { - return null; - } - return trimmed; -} - -// Best-effort fallback when no valid circuit number exists. Keeps migration deterministic -// by preferring explicit category/phase cues over random assignment. -export function inferSectionKeyFromLegacyInput(consumer: LegacyConsumerForPlanning): string | null { - const category = (consumer.category ?? "").toLowerCase(); - if (category.includes("light") || category.includes("beleuchtung")) { - return "lighting"; - } - if (consumer.phaseCount === 3) { - return "three_phase"; - } - if (consumer.phaseCount === 1) { - return "single_phase"; - } - - const phaseType = (consumer.phaseType ?? "").toLowerCase(); - if (phaseType.includes("three") || phaseType.includes("3")) { - return "three_phase"; - } - if (phaseType.includes("single") || phaseType.includes("1")) { - return "single_phase"; - } - return null; -} - -// Prefix-based section inference for already normalized equipment identifiers. -export function inferSectionKeyFromEquipmentIdentifier(equipmentIdentifier: string): string | null { - if (equipmentIdentifier.startsWith("-1F")) { - return "lighting"; - } - if (equipmentIdentifier.startsWith("-2F")) { - return "single_phase"; - } - if (equipmentIdentifier.startsWith("-3F")) { - return "three_phase"; - } - return null; -} diff --git a/src/domain/services/legacy-consumer-migration.service.ts b/src/domain/services/legacy-consumer-migration.service.ts deleted file mode 100644 index c717d00..0000000 --- a/src/domain/services/legacy-consumer-migration.service.ts +++ /dev/null @@ -1,269 +0,0 @@ -import type { LegacyMigrationReport } from "../models/circuit-tree.model.js"; -import type { - LegacyConsumerMigrationDependencies, - LegacyConsumerSource, - LegacyMigrationCircuitInput, -} from "../ports/legacy-consumer-migration.store.js"; -import { - inferSectionKeyFromEquipmentIdentifier, - inferSectionKeyFromLegacyInput, - normalizeCircuitNumber, -} from "./legacy-consumer-migration-planner.js"; - -function parseEquipmentSequence( - equipmentIdentifier: string, - prefix: string -): number | null { - if (!equipmentIdentifier.startsWith(prefix)) { - return null; - } - const suffix = equipmentIdentifier.slice(prefix.length); - if (!/^\d+$/.test(suffix)) { - return null; - } - return Number(suffix); -} - -export class LegacyConsumerMigrationService { - constructor( - private readonly dependencies: LegacyConsumerMigrationDependencies - ) {} - - async migrateCircuitList( - projectId: string, - circuitListId: string - ): Promise { - // Migration is additive: legacy consumers are preserved, and circuit-first entities are created - // with mapping records so transition remains auditable and reversible. - const list = await this.dependencies.circuitListReader.findById( - projectId, - circuitListId - ); - if (!list) { - throw new Error("Circuit list not found in project."); - } - - await this.dependencies.sectionStore.createDefaults(circuitListId); - const sections = - await this.dependencies.sectionStore.listByCircuitList(circuitListId); - const sectionByKey = new Map(sections.map((section) => [section.key, section])); - const unassignedSection = sectionByKey.get("unassigned"); - if (!unassignedSection) { - throw new Error("Unassigned section is required."); - } - - const existingCircuits = - await this.dependencies.circuitReader.listByCircuitList(circuitListId); - const usedEquipmentIdentifiers = new Set( - existingCircuits.map((circuit) => circuit.equipmentIdentifier.toUpperCase()) - ); - - const legacyConsumers = - await this.dependencies.migrationStore.listSourceConsumersByCircuitList( - circuitListId - ); - const rooms = await this.dependencies.roomReader.listByProject(projectId); - const roomById = new Map(rooms.map((room) => [room.id, room])); - - const report: LegacyMigrationReport = { - circuitListId, - legacyConsumerCount: legacyConsumers.length, - createdCircuitCount: 0, - createdDeviceRowCount: 0, - groupedDuplicateCircuitNumbers: [], - generatedIdentifiers: [], - unassignedRows: [], - warnings: [], - }; - - // Idempotency guard: skip consumers already mapped in previous migration run. - const migratedConsumerIds = new Set( - await this.dependencies.migrationStore.listMigratedConsumerIds( - circuitListId - ) - ); - const consumersToMigrate = legacyConsumers.filter((consumer) => !migratedConsumerIds.has(consumer.id)); - - // Legacy rows are grouped by normalized circuit number so duplicates become - // multiple device rows within one circuit instead of duplicate circuits. - const byNormalizedCircuitNumber = new Map(); - const withoutNormalizedCircuitNumber: LegacyConsumerSource[] = []; - for (const consumer of consumersToMigrate) { - const normalized = normalizeCircuitNumber(consumer.circuitNumber ?? null); - if (!normalized) { - withoutNormalizedCircuitNumber.push(consumer); - continue; - } - if (!byNormalizedCircuitNumber.has(normalized)) { - byNormalizedCircuitNumber.set(normalized, []); - } - byNormalizedCircuitNumber.get(normalized)!.push(consumer); - } - - // Duplicate normalized circuit numbers are expected and represented as one circuit - // with multiple circuit_device_rows. - report.groupedDuplicateCircuitNumbers = [...byNormalizedCircuitNumber.entries()] - .filter(([, grouped]) => grouped.length > 1) - .map(([normalizedCircuitNumber, grouped]) => ({ normalizedCircuitNumber, count: grouped.length })); - - const groups: Array<{ - equipmentIdentifier: string | null; - consumers: LegacyConsumerSource[]; - inferredSectionKey: string | null; - isGeneratedIdentifier: boolean; - }> = []; - - // Stable normalized circuit numbers keep existing intent where available. - for (const [normalizedCircuitNumber, grouped] of byNormalizedCircuitNumber.entries()) { - groups.push({ - equipmentIdentifier: normalizedCircuitNumber, - consumers: grouped, - inferredSectionKey: inferSectionKeyFromEquipmentIdentifier(normalizedCircuitNumber), - isGeneratedIdentifier: false, - }); - } - - // Missing/invalid circuit numbers are migrated as single-row groups with - // generated identifiers and best-effort section inference. - for (const consumer of withoutNormalizedCircuitNumber) { - groups.push({ - equipmentIdentifier: null, - consumers: [consumer], - inferredSectionKey: inferSectionKeyFromLegacyInput(consumer), - isGeneratedIdentifier: true, - }); - } - - let nextSortOrder = existingCircuits.length ? Math.max(...existingCircuits.map((circuit) => circuit.sortOrder)) + 10 : 10; - - const maxBySectionPrefix = new Map(); - for (const circuit of existingCircuits) { - const section = sections.find((entry) => entry.id === circuit.sectionId); - if (!section) { - continue; - } - const sequence = parseEquipmentSequence(circuit.equipmentIdentifier.toUpperCase(), section.prefix.toUpperCase()); - if (sequence === null) { - continue; - } - const current = maxBySectionPrefix.get(section.prefix.toUpperCase()) ?? 0; - maxBySectionPrefix.set(section.prefix.toUpperCase(), Math.max(current, sequence)); - } - - const migrationCircuits: LegacyMigrationCircuitInput[] = []; - for (const group of groups) { - const representative = group.consumers[0]; - let section = group.inferredSectionKey ? sectionByKey.get(group.inferredSectionKey) : null; - if (!section) { - section = unassignedSection; - } - - let equipmentIdentifier = group.equipmentIdentifier; - if (!equipmentIdentifier || usedEquipmentIdentifiers.has(equipmentIdentifier.toUpperCase())) { - const prefix = section.prefix.toUpperCase(); - const current = maxBySectionPrefix.get(prefix) ?? 0; - const generatedSequence = current + 1; - maxBySectionPrefix.set(prefix, generatedSequence); - equipmentIdentifier = `${section.prefix}${generatedSequence}`; - report.generatedIdentifiers.push(equipmentIdentifier); - } - - if (!group.inferredSectionKey && group.isGeneratedIdentifier) { - for (const consumer of group.consumers) { - report.unassignedRows.push({ - consumerId: consumer.id, - reason: "Missing or invalid circuit number and no section inferred from phase/category.", - }); - } - } - - usedEquipmentIdentifiers.add(equipmentIdentifier.toUpperCase()); - - const deviceRows: LegacyMigrationCircuitInput["deviceRows"] = []; - const circuitSortOrder = nextSortOrder; - nextSortOrder += 10; - - let rowSortOrder = 10; - for (const consumer of group.consumers) { - const room = consumer.roomId ? roomById.get(consumer.roomId) : undefined; - let noteRemark = consumer.note?.trim() || consumer.comment?.trim() || undefined; - if (consumer.deviceType && consumer.deviceType.trim()) { - const legacyDeviceTypeNote = `Legacy deviceType: ${consumer.deviceType.trim()}`; - noteRemark = noteRemark ? `${noteRemark} | ${legacyDeviceTypeNote}` : legacyDeviceTypeNote; - } - - deviceRows.push({ - linkedProjectDeviceId: consumer.projectDeviceId ?? undefined, - legacyConsumerId: consumer.id, - sortOrder: rowSortOrder, - name: consumer.name, - displayName: consumer.description ?? consumer.name, - phaseType: consumer.phaseType ?? undefined, - connectionKind: undefined, - costGroup: consumer.tradeOrCostGroup ?? undefined, - category: consumer.category ?? undefined, - roomId: consumer.roomId ?? undefined, - roomNumberSnapshot: room?.roomNumber, - roomNameSnapshot: room?.roomName, - quantity: consumer.quantity, - powerPerUnit: consumer.installedPowerPerUnitKw, - simultaneityFactor: consumer.demandFactor, - cosPhi: consumer.powerFactor ?? undefined, - remark: noteRemark, - }); - rowSortOrder += 10; - } - - migrationCircuits.push({ - circuit: { - circuitListId, - sectionId: section.id, - equipmentIdentifier, - displayName: representative.description ?? representative.name, - sortOrder: circuitSortOrder, - protectionType: representative.protectionType ?? undefined, - protectionRatedCurrent: representative.protectionRatedCurrent ?? undefined, - protectionCharacteristic: representative.protectionCharacteristic ?? undefined, - cableType: representative.cableType ?? undefined, - cableCrossSection: representative.cableCrossSection ?? undefined, - voltage: representative.voltageV ?? undefined, - remark: undefined, - }, - deviceRows, - }); - } - report.createdCircuitCount = migrationCircuits.length; - report.createdDeviceRowCount = migrationCircuits.reduce( - (count, entry) => count + entry.deviceRows.length, - 0 - ); - - if (consumersToMigrate.some((consumer) => Boolean(consumer.comment?.trim()))) { - report.warnings.push( - "Legacy comment field was mapped to circuit_device_rows.remark because circuit-level vs row-level intent is ambiguous." - ); - } - - for (const row of report.unassignedRows) { - report.warnings.push(`Consumer ${row.consumerId} was migrated into unassigned section.`); - } - - this.dependencies.migrationStore.persistCircuitListMigration({ - circuitListId, - circuits: migrationCircuits, - report: { - legacyConsumerCount: report.legacyConsumerCount, - createdCircuitCount: report.createdCircuitCount, - createdDeviceRowCount: report.createdDeviceRowCount, - duplicateGroupedCount: report.groupedDuplicateCircuitNumbers.length, - generatedIdentifierCount: report.generatedIdentifiers.length, - unassignedRowCount: report.unassignedRows.length, - warningsJson: JSON.stringify(report.warnings), - generatedIdentifiersJson: JSON.stringify(report.generatedIdentifiers), - duplicateGroupsJson: JSON.stringify(report.groupedDuplicateCircuitNumbers), - }, - }); - - return report; - } -} diff --git a/src/domain/services/project-voltage.service.ts b/src/domain/services/project-voltage.service.ts index 0bb10d0..d0687e0 100644 --- a/src/domain/services/project-voltage.service.ts +++ b/src/domain/services/project-voltage.service.ts @@ -6,34 +6,6 @@ export function isElectricalPhaseType( return value === "single_phase" || value === "three_phase"; } -export function normalizeKnownElectricalPhaseType( - value: string | null -): ElectricalPhaseType | null { - if (value === null) { - return null; - } - const normalized = value - .trim() - .toLocaleLowerCase("de-DE") - .replaceAll("-", "") - .replaceAll("_", ""); - if ( - ["1ph", "1phase", "1phasig", "einphasig", "singlephase"].includes( - normalized - ) - ) { - return "single_phase"; - } - if ( - ["3ph", "3phase", "3phasig", "dreiphasig", "threephase"].includes( - normalized - ) - ) { - return "three_phase"; - } - throw new Error(`Unknown electrical phase type: ${value}`); -} - export interface ProjectVoltageSettings { singlePhaseVoltageV: number; threePhaseVoltageV: number; diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx index fdb6f5e..b214d60 100644 --- a/src/frontend/components/circuit-tree-editor.tsx +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -30,10 +30,8 @@ import { formatPhaseTypeLabel, formatValue, getCircuitSectionLabel, - getLegacyProjectColumnLayoutStorageKey, getProjectColumnLayoutStorageKey, isGridEditorControlTarget, - migrateLegacyColumnLayout, normalizeColumnOrder, parseStoredColumnLayout, } from "../utils/circuit-grid-model"; @@ -191,9 +189,6 @@ type CircuitReorderDropIntent = | { kind: "after-circuit"; sectionId: string; targetCircuitId: string; valid: boolean } | { kind: "section-end"; sectionId: string; valid: boolean }; -const LEGACY_COLUMN_LAYOUT_STORAGE_KEY = - "circuitTreeEditor.columnLayout.v1"; - function normalizeUiError(err: unknown): string { const message = err instanceof Error ? err.message : "Der Vorgang ist fehlgeschlagen."; try { @@ -379,25 +374,11 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const currentLayout = parseStoredColumnLayout( localStorage.getItem(getProjectColumnLayoutStorageKey(projectId)) ); - const legacyLayout = - parseStoredColumnLayout( - localStorage.getItem( - getLegacyProjectColumnLayoutStorageKey(projectId) - ) - ) ?? - parseStoredColumnLayout( - localStorage.getItem(LEGACY_COLUMN_LAYOUT_STORAGE_KEY) - ); - const parsed = - currentLayout ?? - (legacyLayout - ? migrateLegacyColumnLayout(legacyLayout) - : null); - if (!parsed) { + if (!currentLayout) { return; } - setColumnOrder(parsed.order); - setVisibleColumnKeys(parsed.visible); + setColumnOrder(currentLayout.order); + setVisibleColumnKeys(currentLayout.visible); } catch { // ignore invalid local storage and use defaults } finally { diff --git a/src/frontend/components/circuit-tree-preview.tsx b/src/frontend/components/circuit-tree-preview.tsx index 2e5a5d2..cd93739 100644 --- a/src/frontend/components/circuit-tree-preview.tsx +++ b/src/frontend/components/circuit-tree-preview.tsx @@ -114,11 +114,11 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number {formatValue(circuit.circuitTotalPower, "circuitTotalPower")} - {circuit.protectionType ?? "-"} + {circuit.protectionDevice?.type ?? "-"} - {formatValue(circuit.protectionRatedCurrent, "protectionRatedCurrent")} + {formatValue(circuit.protectionDevice?.ratedCurrentA, "protectionRatedCurrent")} - {circuit.protectionCharacteristic ?? "-"} + {circuit.protectionDevice?.tripCharacteristic ?? circuit.protectionDevice?.fuseUtilizationCategory ?? "-"} {circuit.cableType ?? "-"} {circuit.cableCrossSection ?? "-"} {formatValue(circuit.cableLength, "cableLength")} @@ -147,11 +147,11 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number {formatValue(row.cosPhi, "cosPhi")} {formatValue(row.rowTotalPower, "rowTotalPower")} - {circuit.protectionType ?? "-"} + {circuit.protectionDevice?.type ?? "-"} - {formatValue(circuit.protectionRatedCurrent, "protectionRatedCurrent")} + {formatValue(circuit.protectionDevice?.ratedCurrentA, "protectionRatedCurrent")} - {circuit.protectionCharacteristic ?? "-"} + {circuit.protectionDevice?.tripCharacteristic ?? circuit.protectionDevice?.fuseUtilizationCategory ?? "-"} {circuit.cableType ?? "-"} {circuit.cableCrossSection ?? "-"} {formatValue(circuit.cableLength, "cableLength")} @@ -169,11 +169,11 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number {formatValue(circuit.circuitTotalPower, "circuitTotalPower")} - {circuit.protectionType ?? "-"} + {circuit.protectionDevice?.type ?? "-"} - {formatValue(circuit.protectionRatedCurrent, "protectionRatedCurrent")} + {formatValue(circuit.protectionDevice?.ratedCurrentA, "protectionRatedCurrent")} - {circuit.protectionCharacteristic ?? "-"} + {circuit.protectionDevice?.tripCharacteristic ?? circuit.protectionDevice?.fuseUtilizationCategory ?? "-"} {circuit.cableType ?? "-"} {circuit.cableCrossSection ?? "-"} {formatValue(circuit.cableLength, "cableLength")} diff --git a/src/frontend/types.ts b/src/frontend/types.ts index 71c832f..ee58d0d 100644 --- a/src/frontend/types.ts +++ b/src/frontend/types.ts @@ -267,7 +267,6 @@ export interface ProjectDeviceSyncCommandResultDto export interface CircuitTreeDeviceRowDto { id: string; linkedProjectDeviceId?: string; - legacyConsumerId?: string; sortOrder: number; name: string; displayName: string; @@ -316,9 +315,6 @@ export interface CircuitTreeCircuitDto { equipmentIdentifier: string; displayName?: string; sortOrder: number; - protectionType?: string; - protectionRatedCurrent?: number; - protectionCharacteristic?: string; cableType?: string; cableCrossSection?: string; cableLength?: number; @@ -347,17 +343,6 @@ export interface CircuitTreeSectionDto { circuits: CircuitTreeCircuitDto[]; } -export interface CircuitTreeMigrationReportDto { - circuitListId: string; - legacyConsumerCount: number; - createdCircuitCount: number; - createdDeviceRowCount: number; - groupedDuplicateCircuitNumbers: Array<{ normalizedCircuitNumber: string; count: number }>; - generatedIdentifiers: string[]; - unassignedRows: Array<{ consumerId: string; reason: string }>; - warnings: string[]; -} - export interface CircuitTreeResponseDto { circuitListId: string; currentRevision: number; @@ -369,7 +354,6 @@ export interface CircuitTreeResponseDto { headerComponents: CircuitTreeComponentDto[]; sections: CircuitTreeSectionDto[]; footerComponents: CircuitTreeComponentDto[]; - migrationReport?: CircuitTreeMigrationReportDto; } export interface CreateCircuitInputDto { @@ -377,9 +361,6 @@ export interface CreateCircuitInputDto { equipmentIdentifier: string; displayName?: string; sortOrder: number; - protectionType?: string; - protectionRatedCurrent?: number; - protectionCharacteristic?: string; cableType?: string; cableCrossSection?: string; cableLength?: number; diff --git a/src/frontend/utils/circuit-grid-model.ts b/src/frontend/utils/circuit-grid-model.ts index 880dbe9..47e7189 100644 --- a/src/frontend/utils/circuit-grid-model.ts +++ b/src/frontend/utils/circuit-grid-model.ts @@ -130,8 +130,6 @@ export const defaultVisibleColumnKeys = allColumns const projectColumnLayoutStoragePrefix = "circuitTreeEditor.columnLayout.v3"; -const legacyProjectColumnLayoutStoragePrefix = - "circuitTreeEditor.columnLayout.v2"; export interface CircuitColumnLayout { order: CellKey[]; @@ -164,12 +162,6 @@ export function getProjectColumnLayoutStorageKey( return `${projectColumnLayoutStoragePrefix}.${projectId}`; } -export function getLegacyProjectColumnLayoutStorageKey( - projectId: string -): string { - return `${legacyProjectColumnLayoutStoragePrefix}.${projectId}`; -} - export function normalizeColumnOrder(keys: CellKey[]): CellKey[] { const unique = [...new Set(keys)]; const allKeys = allColumns.map((column) => column.key); @@ -233,28 +225,6 @@ export function parseStoredColumnLayout( } } -export function migrateLegacyColumnLayout( - layout: CircuitColumnLayout -): CircuitColumnLayout { - const newlyVisibleKey: CellKey = "protectionSummary"; - const order = layout.order.filter( - (key) => key !== newlyVisibleKey - ) as CellKey[]; - const totalPowerIndex = order.indexOf("rowTotalPower"); - order.splice( - totalPowerIndex >= 0 ? totalPowerIndex + 1 : order.length, - 0, - newlyVisibleKey - ); - - return { - order, - visible: layout.visible.includes(newlyVisibleKey) - ? layout.visible - : [...layout.visible, newlyVisibleKey], - }; -} - const deviceOnlyColumns = new Set([ "quantity", "powerPerUnit", @@ -422,11 +392,7 @@ export function buildCircuitEditPatch( if (key === "protectionSummary" || key === "cableSummary") { return { remark: draft.trim() || null }; } - if ( - key === "protectionRatedCurrent" || - key === "cableLength" || - key === "voltage" - ) { + if (key === "cableLength" || key === "voltage") { return { [key]: parseNumeric(key, draft) ?? null }; } if (key === "isReserve") { @@ -530,23 +496,14 @@ export function getCircuitValue(circuit: CircuitTreeCircuitDto, key: CellKey): G case "protectionType": return protection ? protectionDeviceTypeLabels[protection.type] - : circuit.protectionType; + : undefined; case "protectionRatedCurrent": - return protection?.ratedCurrentA ?? circuit.protectionRatedCurrent; + return protection?.ratedCurrentA; case "protectionCharacteristic": - return protectionCharacteristic || circuit.protectionCharacteristic; + return protectionCharacteristic || undefined; case "protectionSummary": { if (!protection) { - const legacyCurrent = - circuit.protectionRatedCurrent !== undefined && - circuit.protectionRatedCurrent !== null - ? `${circuit.protectionRatedCurrent}A` - : ""; - return [ - circuit.protectionType, - legacyCurrent, - circuit.protectionCharacteristic, - ].filter(Boolean).join(" ").trim() || undefined; + return undefined; } const currentValue = protection.ratedCurrentA; diff --git a/src/frontend/utils/circuit-group-editing.ts b/src/frontend/utils/circuit-group-editing.ts index af28ee3..a8ed14d 100644 --- a/src/frontend/utils/circuit-group-editing.ts +++ b/src/frontend/utils/circuit-group-editing.ts @@ -117,11 +117,6 @@ export function buildCircuitGroupSubtreeSnapshot( equipmentIdentifier: circuit.equipmentIdentifier, displayName: circuit.displayName ?? null, sortOrder: circuit.sortOrder, - protectionType: circuit.protectionType ?? null, - protectionRatedCurrent: - circuit.protectionRatedCurrent ?? null, - protectionCharacteristic: - circuit.protectionCharacteristic ?? null, cableType: circuit.cableType ?? null, cableCrossSection: circuit.cableCrossSection ?? null, cableLength: circuit.cableLength ?? null, @@ -143,7 +138,6 @@ export function buildCircuitGroupSubtreeSnapshot( circuitId: circuit.id, linkedProjectDeviceId: row.linkedProjectDeviceId ?? null, - legacyConsumerId: row.legacyConsumerId ?? null, sortOrder: row.sortOrder, name: row.name, displayName: row.displayName, diff --git a/src/frontend/utils/circuit-structure-command.ts b/src/frontend/utils/circuit-structure-command.ts index 9113d15..582dcd8 100644 --- a/src/frontend/utils/circuit-structure-command.ts +++ b/src/frontend/utils/circuit-structure-command.ts @@ -22,7 +22,6 @@ export function buildCircuitDeviceRowInsertSnapshot(input: { id, circuitId, linkedProjectDeviceId: values.linkedProjectDeviceId ?? null, - legacyConsumerId: null, sortOrder: values.sortOrder ?? defaultSortOrder, name: values.name, displayName: values.displayName, @@ -61,9 +60,6 @@ export function buildCircuitInsertSnapshot(input: { equipmentIdentifier: values.equipmentIdentifier, displayName: values.displayName ?? null, sortOrder: values.sortOrder, - protectionType: values.protectionType ?? null, - protectionRatedCurrent: values.protectionRatedCurrent ?? null, - protectionCharacteristic: values.protectionCharacteristic ?? null, cableType: values.cableType ?? null, cableCrossSection: values.cableCrossSection ?? null, cableLength: values.cableLength ?? null, diff --git a/src/server/controllers/circuit-tree.controller.ts b/src/server/controllers/circuit-tree.controller.ts index 4957e2e..b7f0e0b 100644 --- a/src/server/controllers/circuit-tree.controller.ts +++ b/src/server/controllers/circuit-tree.controller.ts @@ -148,7 +148,6 @@ export async function getCircuitTree(req: Request, res: Response) { const deviceRows = (rowsByCircuitId.get(circuit.id) ?? []).map((row) => ({ id: row.id, linkedProjectDeviceId: row.linkedProjectDeviceId ?? undefined, - legacyConsumerId: row.legacyConsumerId ?? undefined, sortOrder: row.sortOrder, name: row.name, displayName: row.displayName, @@ -178,9 +177,6 @@ export async function getCircuitTree(req: Request, res: Response) { equipmentIdentifier: circuit.equipmentIdentifier, displayName: circuit.displayName ?? undefined, sortOrder: circuit.sortOrder, - protectionType: circuit.protectionType ?? undefined, - protectionRatedCurrent: circuit.protectionRatedCurrent ?? undefined, - protectionCharacteristic: circuit.protectionCharacteristic ?? undefined, cableType: circuit.cableType ?? undefined, cableCrossSection: circuit.cableCrossSection ?? undefined, cableLength: circuit.cableLength ?? undefined, diff --git a/tests/circuit-device-row-move-project-command.repository.test.ts b/tests/circuit-device-row-move-project-command.repository.test.ts index 448914f..0569099 100644 --- a/tests/circuit-device-row-move-project-command.repository.test.ts +++ b/tests/circuit-device-row-move-project-command.repository.test.ts @@ -180,9 +180,6 @@ function createMoveTargetSnapshot( equipmentIdentifier: "-1F4", displayName: "Neuer Stromkreis", sortOrder: 40, - protectionType: null, - protectionRatedCurrent: null, - protectionCharacteristic: null, cableType: null, cableCrossSection: null, cableLength: null, diff --git a/tests/circuit-device-row-structure-project-command.repository.test.ts b/tests/circuit-device-row-structure-project-command.repository.test.ts index c16c6ed..318ef02 100644 --- a/tests/circuit-device-row-structure-project-command.repository.test.ts +++ b/tests/circuit-device-row-structure-project-command.repository.test.ts @@ -133,7 +133,6 @@ function createTestDatabase(): DatabaseContext { id: "row-1", circuitId: "circuit-1", linkedProjectDeviceId: "device-1", - legacyConsumerId: "legacy-1", sortOrder: 10, name: "Leuchte", displayName: "Leuchte lokal", @@ -163,7 +162,6 @@ function createInsertSnapshot( id: "row-new", circuitId: "circuit-reserve", linkedProjectDeviceId: "device-1", - legacyConsumerId: null, sortOrder: 10, name: "Steckdose", displayName: "Steckdose lokal", @@ -423,10 +421,6 @@ describe("circuit device-row structure project-command repository", () => { roomId: "room-foreign", }), createInsertSnapshot({ id: "row-1" }), - createInsertSnapshot({ - id: "row-legacy", - legacyConsumerId: "legacy-user-value", - }), ]) { assert.throws(() => store.execute({ diff --git a/tests/circuit-grid-model.test.ts b/tests/circuit-grid-model.test.ts index b0472b8..6dbfef8 100644 --- a/tests/circuit-grid-model.test.ts +++ b/tests/circuit-grid-model.test.ts @@ -6,14 +6,12 @@ import { buildDeviceRowEditPatch, formatValue, getCircuitSectionLabel, - getLegacyProjectColumnLayoutStorageKey, getProjectColumnLayoutStorageKey, isGridEditorControlTarget, getBlockSortValue, getCellKind, getCircuitValue, getDeviceValue, - migrateLegacyColumnLayout, parseStoredColumnLayout, parseNumeric, } from "../src/frontend/utils/circuit-grid-model.js"; @@ -41,9 +39,11 @@ const circuit: CircuitTreeCircuitDto = { equipmentIdentifier: "-1F1", displayName: "Lighting circuit", sortOrder: 10, - protectionType: "MCB", - protectionRatedCurrent: 16, - protectionCharacteristic: "B", + protectionDevice: { + type: "LS", + ratedCurrentA: 16, + tripCharacteristic: "B", + }, cableType: "NYM-J", cableCrossSection: "1.5 mm²", cableLength: 20, @@ -83,10 +83,6 @@ describe("circuit grid model", () => { getProjectColumnLayoutStorageKey("project-1"), "circuitTreeEditor.columnLayout.v3.project-1" ); - assert.equal( - getLegacyProjectColumnLayoutStorageKey("project-1"), - "circuitTreeEditor.columnLayout.v2.project-1" - ); const layout = parseStoredColumnLayout( JSON.stringify({ order: ["displayName", "equipmentIdentifier"], @@ -106,39 +102,6 @@ describe("circuit grid model", () => { assert.equal(parseStoredColumnLayout("{invalid"), null); }); - it("adds the circuit protection column to legacy project layouts", () => { - const legacyLayout = parseStoredColumnLayout( - JSON.stringify({ - order: [ - "equipmentIdentifier", - "displayName", - "rowTotalPower", - "remark", - ], - visible: [ - "equipmentIdentifier", - "displayName", - "rowTotalPower", - "remark", - ], - }) - ); - assert.ok(legacyLayout); - - const migrated = migrateLegacyColumnLayout(legacyLayout); - assert.equal( - migrated.order.indexOf("protectionSummary"), - migrated.order.indexOf("rowTotalPower") + 1 - ); - assert.deepEqual(migrated.visible, [ - "equipmentIdentifier", - "displayName", - "rowTotalPower", - "remark", - "protectionSummary", - ]); - }); - it("shows stable circuit sections with German labels", () => { assert.equal( getCircuitSectionLabel({ @@ -181,7 +144,7 @@ describe("circuit grid model", () => { it("projects circuit and device values without mixing ownership", () => { assert.equal(getDeviceValue(device, "roomSummary"), "1.01 Office"); assert.equal(getDeviceValue(device, "rowTotalPower"), 0.08); - assert.equal(getCircuitValue(circuit, "protectionSummary"), "MCB 16A B"); + assert.equal(getCircuitValue(circuit, "protectionSummary"), "LS · 16 A · B"); assert.equal( getCircuitValue( { diff --git a/tests/circuit-group-numbering.test.ts b/tests/circuit-group-numbering.test.ts index 88e291b..a63c766 100644 --- a/tests/circuit-group-numbering.test.ts +++ b/tests/circuit-group-numbering.test.ts @@ -424,7 +424,7 @@ describe("circuit group numbering", () => { }); }); - it("rejects legacy, malformed and unsupported identifiers", () => { + it("rejects outdated, malformed and unsupported identifiers", () => { for (const identifier of [ "-1F1", "1F1.1", diff --git a/tests/circuit-group-subtree-project-command.repository.test.ts b/tests/circuit-group-subtree-project-command.repository.test.ts index 2fc5469..05d7dcc 100644 --- a/tests/circuit-group-subtree-project-command.repository.test.ts +++ b/tests/circuit-group-subtree-project-command.repository.test.ts @@ -90,7 +90,6 @@ function createTestDatabase(): DatabaseContext { id: "subtree-row", circuitId: "subtree-circuit", linkedProjectDeviceId: null, - legacyConsumerId: null, sortOrder: 10, name: "Leuchte", displayName: "Leuchte", diff --git a/tests/circuit-group-subtree-snapshot.test.ts b/tests/circuit-group-subtree-snapshot.test.ts index dfe1105..f748e90 100644 --- a/tests/circuit-group-subtree-snapshot.test.ts +++ b/tests/circuit-group-subtree-snapshot.test.ts @@ -71,9 +71,6 @@ function createSnapshot(): CircuitGroupSubtreeSnapshot { equipmentIdentifier: "-1F1.1", displayName: "Beleuchtung", sortOrder: 10, - protectionType: null, - protectionRatedCurrent: null, - protectionCharacteristic: null, cableType: null, cableCrossSection: null, cableLength: null, @@ -89,7 +86,6 @@ function createSnapshot(): CircuitGroupSubtreeSnapshot { id: "row-1", circuitId: "circuit-1", linkedProjectDeviceId: "device-1", - legacyConsumerId: null, sortOrder: 10, name: "Leuchte", displayName: "Leuchte Flur", diff --git a/tests/circuit-power-calculation.test.ts b/tests/circuit-power-calculation.test.ts index 157bce2..2f9c0ec 100644 --- a/tests/circuit-power-calculation.test.ts +++ b/tests/circuit-power-calculation.test.ts @@ -10,7 +10,6 @@ import { import { resolveCircuitPhaseType, resolveProjectVoltage, - normalizeKnownElectricalPhaseType, } from "../src/domain/services/project-voltage.service.js"; describe("circuit power calculation", () => { @@ -74,16 +73,5 @@ describe("project voltage derivation", () => { assert.equal(resolveCircuitPhaseType("unassigned", [null]), "single_phase"); }); - it("normalizes known legacy phase labels without guessing unknown values", () => { - assert.equal(normalizeKnownElectricalPhaseType("1phasig"), "single_phase"); - assert.equal(normalizeKnownElectricalPhaseType("1-phasig"), "single_phase"); - assert.equal(normalizeKnownElectricalPhaseType("3phase"), "three_phase"); - assert.equal(normalizeKnownElectricalPhaseType("3-phasig"), "three_phase"); - assert.equal(normalizeKnownElectricalPhaseType(null), null); - assert.throws( - () => normalizeKnownElectricalPhaseType("zweiphasig"), - /Unknown electrical phase type/ - ); - }); }); diff --git a/tests/circuit-project-command.repository.test.ts b/tests/circuit-project-command.repository.test.ts index a98d7d5..7cad141 100644 --- a/tests/circuit-project-command.repository.test.ts +++ b/tests/circuit-project-command.repository.test.ts @@ -70,7 +70,7 @@ function createTestFixture(): TestFixture { equipmentIdentifier: "-1F1", displayName: "Licht Bestand", sortOrder: 10, - protectionRatedCurrent: 10, + cableLength: 10, voltage: 230, controlRequirement: "none", isReserve: 0, @@ -112,7 +112,7 @@ describe("circuit project-command repository", () => { const store = new CircuitProjectCommandRepository(fixture.context.db); const command = createCircuitUpdateProjectCommand("circuit-1", { displayName: null, - protectionRatedCurrent: 16, + cableLength: 16, voltage: 400, controlRequirement: "DALI", isReserve: true, @@ -128,7 +128,7 @@ describe("circuit project-command repository", () => { const circuit = getCircuit(fixture.context); assert.equal(circuit.displayName, null); - assert.equal(circuit.protectionRatedCurrent, 16); + assert.equal(circuit.cableLength, 16); assert.equal(circuit.voltage, 230); assert.equal(circuit.controlRequirement, "DALI"); assert.equal(circuit.isReserve, 1); @@ -137,7 +137,7 @@ describe("circuit project-command repository", () => { circuitId: "circuit-1", changes: [ { field: "displayName", value: "Licht Bestand" }, - { field: "protectionRatedCurrent", value: 10 }, + { field: "cableLength", value: 10 }, { field: "controlRequirement", value: "none" }, { field: "isReserve", value: false }, ], @@ -152,7 +152,7 @@ describe("circuit project-command repository", () => { deserializeProjectCommand(changeSet.forwardPayloadJson), createCircuitUpdateProjectCommand("circuit-1", { displayName: null, - protectionRatedCurrent: 16, + cableLength: 16, controlRequirement: "DALI", isReserve: true, }) @@ -184,7 +184,7 @@ describe("circuit project-command repository", () => { source: "user", command: createCircuitUpdateProjectCommand("circuit-1", { displayName: "Licht Neu", - protectionRatedCurrent: 16, + cableLength: 16, }), }); @@ -199,7 +199,7 @@ describe("circuit project-command repository", () => { const circuit = getCircuit(fixture.context); assert.equal(circuit.displayName, "Licht Bestand"); - assert.equal(circuit.protectionRatedCurrent, 10); + assert.equal(circuit.cableLength, 10); assert.equal(undone.revision.revisionNumber, 2); assert.deepEqual( fixture.context.db diff --git a/tests/circuit-structure-command.test.ts b/tests/circuit-structure-command.test.ts index 2bee690..d688685 100644 --- a/tests/circuit-structure-command.test.ts +++ b/tests/circuit-structure-command.test.ts @@ -12,7 +12,7 @@ import { } from "../src/frontend/utils/api.js"; describe("circuit structure command adapters", () => { - it("builds a complete row snapshot without legacy identities", () => { + it("builds a complete row snapshot", () => { const row = buildCircuitDeviceRowInsertSnapshot({ id: "row-1", circuitId: "circuit-1", @@ -31,7 +31,6 @@ describe("circuit structure command adapters", () => { id: "row-1", circuitId: "circuit-1", linkedProjectDeviceId: "device-1", - legacyConsumerId: null, sortOrder: 20, name: "Steckdose", displayName: "Steckdose Büro", diff --git a/tests/circuit-structure-project-command.repository.test.ts b/tests/circuit-structure-project-command.repository.test.ts index acfdd02..96e8c10 100644 --- a/tests/circuit-structure-project-command.repository.test.ts +++ b/tests/circuit-structure-project-command.repository.test.ts @@ -117,9 +117,6 @@ function createTestDatabase(): TestFixture { equipmentIdentifier: "-1F1", displayName: "Bestand", sortOrder: 10, - protectionType: "LS", - protectionRatedCurrent: 16, - protectionCharacteristic: "B", cableType: "NYM-J", cableCrossSection: "3x1,5", cableLength: 18.5, @@ -139,7 +136,6 @@ function createTestDatabase(): TestFixture { id: "row-existing-1", circuitId: "circuit-existing", linkedProjectDeviceId: "device-1", - legacyConsumerId: "legacy-1", sortOrder: 10, name: "Leuchte A", displayName: "Leuchte A lokal", @@ -198,7 +194,6 @@ function createRow( id, circuitId, linkedProjectDeviceId: null, - legacyConsumerId: null, sortOrder, name: `Gerät ${id}`, displayName: `Gerät ${id}`, @@ -241,9 +236,6 @@ function createCircuitSnapshot( equipmentIdentifier: "-1F2", displayName: "Neuer Stromkreis", sortOrder: 20, - protectionType: null, - protectionRatedCurrent: null, - protectionCharacteristic: null, cableType: null, cableCrossSection: null, cableLength: null, @@ -569,14 +561,6 @@ describe("circuit structure project-command repository", () => { }), ], }), - createCircuitSnapshot(fixture, { - id: "legacy-row", - deviceRows: [ - createRow("legacy-row", "row-legacy", 10, { - legacyConsumerId: "legacy-user-value", - }), - ], - }), ]; for (const snapshot of invalidSnapshots) { assert.throws(() => diff --git a/tests/distribution-board-component-schema-migration.test.ts b/tests/distribution-board-component-schema-migration.test.ts deleted file mode 100644 index a8132f9..0000000 --- a/tests/distribution-board-component-schema-migration.test.ts +++ /dev/null @@ -1,466 +0,0 @@ -import assert from "node:assert/strict"; -import fs from "node:fs"; -import path from "node:path"; -import { describe, it } from "node:test"; -import Database from "better-sqlite3"; -import { migrate } from "drizzle-orm/better-sqlite3/migrator"; -import { createDatabaseContext } from "../src/db/database-context.js"; - -const migrationPath = path.resolve( - "src", - "db", - "migrations", - "0024_damp_mister_sinister.sql" -); - -function applyComponentSchemaMigration(sqlite: Database.Database): void { - const migrationSql = fs.readFileSync(migrationPath, "utf8"); - for (const statement of migrationSql.split("--> statement-breakpoint")) { - if (statement.trim()) { - sqlite.exec(statement); - } - } -} - -function createLegacySchema(sqlite: Database.Database): void { - sqlite.pragma("foreign_keys = ON"); - sqlite.exec(` - CREATE TABLE circuit_lists ( - id text PRIMARY KEY NOT NULL - ); - CREATE TABLE circuit_sections ( - id text PRIMARY KEY NOT NULL, - circuit_list_id text NOT NULL REFERENCES circuit_lists(id) ON DELETE CASCADE, - key text NOT NULL, - display_name text NOT NULL, - prefix text NOT NULL, - sort_order integer DEFAULT 0 NOT NULL - ); - CREATE TABLE circuits ( - id text PRIMARY KEY NOT NULL, - circuit_list_id text NOT NULL REFERENCES circuit_lists(id) ON DELETE CASCADE, - equipment_identifier text NOT NULL, - protection_type text, - protection_rated_current real, - protection_characteristic text - ); - `); -} - -describe("distribution board component schema migration", () => { - it("creates the additive schema on an empty database", () => { - const context = createDatabaseContext(":memory:"); - try { - migrate(context.db, { - migrationsFolder: path.resolve("src", "db", "migrations"), - }); - - const tables = ( - context.sqlite - .prepare( - `SELECT name - FROM sqlite_master - WHERE type = 'table' - ORDER BY name` - ) - .all() as Array<{ name: string }> - ).map((table) => table.name); - - assert.equal(tables.includes("distribution_board_components"), true); - assert.equal(tables.includes("circuit_protection_devices"), true); - assert.deepEqual( - tables.includes("distribution_board_component_protection_devices"), - true - ); - assert.deepEqual( - tables.includes("circuit_list_equipment_identifiers"), - true - ); - - const sectionColumns = ( - context.sqlite - .prepare("PRAGMA table_info(circuit_sections)") - .all() as Array<{ name: string }> - ).map((column) => column.name); - assert.equal(sectionColumns.includes("category"), true); - assert.equal(sectionColumns.includes("group_number"), true); - } finally { - context.close(); - } - }); - - it("backfills groups and the BMK registry without rewriting existing planning data", () => { - const sqlite = new Database(":memory:"); - try { - createLegacySchema(sqlite); - sqlite.exec(` - INSERT INTO circuit_lists (id) VALUES ('list-1'); - INSERT INTO circuit_sections ( - id, - circuit_list_id, - key, - display_name, - prefix, - sort_order - ) VALUES - ('lighting', 'list-1', 'lighting', 'Beleuchtung', '-1F', 10), - ('single', 'list-1', 'single_phase', '1-phasig', '-2F', 20), - ('three', 'list-1', 'three_phase', '3-phasig', '-3F', 30), - ('unassigned', 'list-1', 'unassigned', 'Nicht zugeordnet', '-9F', 40); - INSERT INTO circuits ( - id, - circuit_list_id, - equipment_identifier, - protection_type, - protection_rated_current, - protection_characteristic - ) VALUES - ('circuit-1', 'list-1', '-1F1', 'legacy fuse', 13, 'legacy'), - ('circuit-2', 'list-1', '-2F4', NULL, NULL, NULL); - `); - - applyComponentSchemaMigration(sqlite); - - assert.deepEqual( - sqlite - .prepare( - `SELECT - id, - category, - group_number AS groupNumber - FROM circuit_sections - ORDER BY sort_order` - ) - .all(), - [ - { id: "lighting", category: "lighting", groupNumber: 1 }, - { id: "single", category: "single_phase", groupNumber: 1 }, - { id: "three", category: "three_phase", groupNumber: 1 }, - { id: "unassigned", category: null, groupNumber: null }, - ] - ); - assert.deepEqual( - sqlite - .prepare( - `SELECT - id, - equipment_identifier AS equipmentIdentifier, - protection_type AS protectionType, - protection_rated_current AS protectionRatedCurrent, - protection_characteristic AS protectionCharacteristic - FROM circuits - ORDER BY id` - ) - .all(), - [ - { - id: "circuit-1", - equipmentIdentifier: "-1F1", - protectionType: "legacy fuse", - protectionRatedCurrent: 13, - protectionCharacteristic: "legacy", - }, - { - id: "circuit-2", - equipmentIdentifier: "-2F4", - protectionType: null, - protectionRatedCurrent: null, - protectionCharacteristic: null, - }, - ] - ); - assert.deepEqual( - sqlite - .prepare( - `SELECT - owner_type AS ownerType, - owner_id AS ownerId, - equipment_identifier AS equipmentIdentifier - FROM circuit_list_equipment_identifiers - ORDER BY owner_id` - ) - .all(), - [ - { - ownerType: "circuit", - ownerId: "circuit-1", - equipmentIdentifier: "-1F1", - }, - { - ownerType: "circuit", - ownerId: "circuit-2", - equipmentIdentifier: "-2F4", - }, - ] - ); - assert.deepEqual( - sqlite - .prepare("SELECT count(*) AS count FROM circuit_protection_devices") - .get(), - { count: 0 } - ); - } finally { - sqlite.close(); - } - }); - - it("keeps the shared BMK registry synchronized and rejects normalized collisions", () => { - const sqlite = new Database(":memory:"); - try { - createLegacySchema(sqlite); - sqlite.exec(` - INSERT INTO circuit_lists (id) VALUES ('list-1'); - INSERT INTO circuit_sections ( - id, - circuit_list_id, - key, - display_name, - prefix - ) VALUES ('lighting', 'list-1', 'lighting', 'Beleuchtung', '-1F'); - INSERT INTO circuits ( - id, - circuit_list_id, - equipment_identifier - ) VALUES ('circuit-1', 'list-1', '-1F1'); - `); - applyComponentSchemaMigration(sqlite); - - sqlite - .prepare( - `INSERT INTO distribution_board_components ( - id, - circuit_list_id, - equipment_identifier, - name, - role, - placement, - sort_order - ) VALUES (?, ?, ?, ?, ?, ?, ?)` - ) - .run( - "main-switch", - "list-1", - "-Q0", - "Hauptschalter", - "main_switch", - "header", - 10 - ); - - assert.throws(() => - sqlite - .prepare( - `INSERT INTO distribution_board_components ( - id, - circuit_list_id, - equipment_identifier, - name, - role, - placement - ) VALUES (?, ?, ?, ?, ?, ?)` - ) - .run( - "duplicate", - "list-1", - " -q0 ", - "Duplikat", - "auxiliary", - "footer" - ) - ); - assert.throws(() => - sqlite - .prepare( - `INSERT INTO distribution_board_components ( - id, - circuit_list_id, - equipment_identifier, - name, - role, - placement - ) VALUES (?, ?, ?, ?, ?, ?)` - ) - .run( - "circuit-duplicate", - "list-1", - "-1f1", - "Duplikat", - "auxiliary", - "footer" - ) - ); - - sqlite - .prepare( - `INSERT INTO circuits ( - id, - circuit_list_id, - equipment_identifier - ) VALUES (?, ?, ?)` - ) - .run("circuit-2", "list-1", "-1F2"); - sqlite - .prepare( - `UPDATE circuits - SET equipment_identifier = ? - WHERE id = ?` - ) - .run("-1F3", "circuit-2"); - - assert.deepEqual( - sqlite - .prepare( - `SELECT equipment_identifier AS equipmentIdentifier - FROM circuit_list_equipment_identifiers - WHERE owner_type = 'circuit' AND owner_id = 'circuit-2'` - ) - .get(), - { equipmentIdentifier: "-1F3" } - ); - - assert.throws(() => - sqlite - .prepare( - `UPDATE circuits - SET equipment_identifier = ? - WHERE id = ?` - ) - .run("-q0", "circuit-2") - ); - assert.deepEqual( - sqlite - .prepare( - `SELECT equipment_identifier AS equipmentIdentifier - FROM circuits - WHERE id = 'circuit-2'` - ) - .get(), - { equipmentIdentifier: "-1F3" } - ); - - sqlite - .prepare("DELETE FROM distribution_board_components WHERE id = ?") - .run("main-switch"); - assert.deepEqual( - sqlite - .prepare( - `SELECT count(*) AS count - FROM circuit_list_equipment_identifiers - WHERE owner_type = 'distribution_board_component'` - ) - .get(), - { count: 0 } - ); - } finally { - sqlite.close(); - } - }); - - it("stores independent one-to-one circuit and component protection state", () => { - const sqlite = new Database(":memory:"); - try { - createLegacySchema(sqlite); - sqlite.exec(` - INSERT INTO circuit_lists (id) VALUES ('list-1'); - INSERT INTO circuit_sections ( - id, - circuit_list_id, - key, - display_name, - prefix - ) VALUES ('lighting', 'list-1', 'lighting', 'Beleuchtung', '-1F'); - INSERT INTO circuits ( - id, - circuit_list_id, - equipment_identifier - ) VALUES ('circuit-1', 'list-1', '-1F1'); - `); - applyComponentSchemaMigration(sqlite); - sqlite.exec(` - INSERT INTO circuit_protection_devices ( - circuit_id, - type, - rated_current_a, - trip_characteristic - ) VALUES ('circuit-1', 'LS', 10, 'B'); - INSERT INTO distribution_board_components ( - id, - circuit_list_id, - section_id, - equipment_identifier, - name, - role, - placement - ) VALUES ( - 'group-rcd', - 'list-1', - 'lighting', - '-1Q1.0', - 'Gruppen-FI', - 'group_residual_current_protection', - 'group' - ); - INSERT INTO distribution_board_component_protection_devices ( - component_id, - type, - rated_current_a, - rcd_type, - rated_residual_current_ma - ) VALUES ('group-rcd', 'FI', 40, 'A', 30); - `); - - assert.deepEqual( - sqlite - .prepare("SELECT * FROM circuit_protection_devices") - .get(), - { - circuit_id: "circuit-1", - type: "LS", - rated_current_a: 10, - fuse_utilization_category: null, - trip_characteristic: "B", - rcd_type: null, - rated_residual_current_ma: null, - } - ); - assert.deepEqual( - sqlite - .prepare( - "SELECT * FROM distribution_board_component_protection_devices" - ) - .get(), - { - component_id: "group-rcd", - type: "FI", - rated_current_a: 40, - fuse_utilization_category: null, - trip_characteristic: null, - rcd_type: "A", - rated_residual_current_ma: 30, - } - ); - - sqlite.prepare("DELETE FROM circuits WHERE id = ?").run("circuit-1"); - sqlite - .prepare("DELETE FROM distribution_board_components WHERE id = ?") - .run("group-rcd"); - assert.deepEqual( - sqlite - .prepare("SELECT count(*) AS count FROM circuit_protection_devices") - .get(), - { count: 0 } - ); - assert.deepEqual( - sqlite - .prepare( - `SELECT count(*) AS count - FROM distribution_board_component_protection_devices` - ) - .get(), - { count: 0 } - ); - } finally { - sqlite.close(); - } - }); -}); diff --git a/tests/distribution-board-structure-project-command.repository.test.ts b/tests/distribution-board-structure-project-command.repository.test.ts index 9933446..685ffe8 100644 --- a/tests/distribution-board-structure-project-command.repository.test.ts +++ b/tests/distribution-board-structure-project-command.repository.test.ts @@ -21,11 +21,9 @@ import { ProjectRevisionConflictError } from "../src/domain/errors/project-revis import { createDistributionBoardInsertProjectCommand, createDistributionBoardStructureSnapshot, - type DistributionBoardStructureProjectCommand, } from "../src/domain/models/distribution-board-structure-project-command.model.js"; import { createDistributionBoardUpdateProjectCommand, - type DistributionBoardUpdateProjectCommand, } from "../src/domain/models/distribution-board-project-command.model.js"; import { copyDistributionBoard, @@ -62,43 +60,6 @@ function getStructureCounts(context: DatabaseContext) { }; } -function legacySections(circuitListId: string) { - return [ - { - id: "legacy-lighting", - circuitListId, - key: "lighting", - displayName: "Lighting", - prefix: "-1F", - sortOrder: 10, - }, - { - id: "legacy-single-phase", - circuitListId, - key: "single_phase", - displayName: "Single-phase circuits", - prefix: "-2F", - sortOrder: 20, - }, - { - id: "legacy-three-phase", - circuitListId, - key: "three_phase", - displayName: "Three-phase circuits", - prefix: "-3F", - sortOrder: 30, - }, - { - id: "legacy-unassigned", - circuitListId, - key: "unassigned", - displayName: "Unassigned", - prefix: "-UF", - sortOrder: 90, - }, - ]; -} - describe("distribution-board structure project command", () => { it("builds the fixed setup and sends the expected frontend revision", async () => { const structure = createDistributionBoardStructureSnapshot( @@ -308,104 +269,6 @@ describe("distribution-board structure project command", () => { } }); - it("applies stored version-one setup commands with unassigned new fields", () => { - const context = createTestDatabase(); - try { - const repository = - new DistributionBoardStructureProjectCommandRepository( - context.db - ); - const current = createDistributionBoardStructureSnapshot( - "project-1", - "UV Alt", - { floorId: null, supplyType: null } - ); - const { - floorId: _floorId, - supplyType: _supplyType, - ...legacyBoard - } = current.distributionBoard; - const legacyCommand = { - schemaVersion: 1, - type: "distribution-board.insert", - payload: { - structure: { - distributionBoard: legacyBoard, - circuitList: current.circuitList, - sections: legacySections(current.circuitList.id), - }, - }, - } as unknown as DistributionBoardStructureProjectCommand; - repository.execute({ - projectId: "project-1", - expectedRevision: 0, - source: "user", - command: legacyCommand, - }); - const inserted = context.db - .select() - .from(distributionBoards) - .get(); - assert.equal(inserted?.floorId, null); - assert.equal(inserted?.supplyType, null); - assert.equal( - context.db.select().from(circuitSections).all().length, - 4 - ); - assert.equal( - context.db.select().from(distributionBoardComponents).all() - .length, - 0 - ); - } finally { - context.close(); - } - }); - - it("keeps stored version-two setup commands component-free", () => { - const context = createTestDatabase(); - try { - const repository = - new DistributionBoardStructureProjectCommandRepository( - context.db - ); - const current = createDistributionBoardStructureSnapshot( - "project-1", - "UV Version 2" - ); - const command = { - schemaVersion: 2, - type: "distribution-board.insert", - payload: { - structure: { - distributionBoard: current.distributionBoard, - circuitList: current.circuitList, - sections: legacySections(current.circuitList.id), - }, - }, - } as DistributionBoardStructureProjectCommand; - - const inserted = repository.execute({ - projectId: "project-1", - expectedRevision: 0, - source: "user", - command, - }); - assert.equal(inserted.inverse.schemaVersion, 2); - assert.equal( - context.db.select().from(circuitSections).all().length, - 4 - ); - assert.equal( - context.db.select().from(distributionBoardComponents).all() - .length, - 0 - ); - } finally { - context.close(); - } - }); - it("updates floor, supply type and simultaneity factor with persisted undo", () => { const context = createTestDatabase(); try { @@ -524,52 +387,6 @@ describe("distribution-board structure project command", () => { } }); - it("keeps stored version-one board updates executable", () => { - const context = createTestDatabase(); - try { - const repository = - new DistributionBoardStructureProjectCommandRepository( - context.db - ); - const structure = createDistributionBoardStructureSnapshot( - "project-1", - "UV Alt", - { supplyType: "AV" } - ); - repository.execute({ - projectId: "project-1", - expectedRevision: 0, - source: "user", - command: - createDistributionBoardInsertProjectCommand(structure), - }); - const legacyUpdate = { - schemaVersion: 1, - type: "distribution-board.update", - payload: { - distributionBoardId: structure.distributionBoard.id, - changes: [{ field: "supplyType", value: "SV" }], - }, - } as DistributionBoardUpdateProjectCommand; - - repository.executeUpdate({ - projectId: "project-1", - expectedRevision: 1, - source: "user", - command: legacyUpdate, - }); - - const board = context.db - .select() - .from(distributionBoards) - .get(); - assert.equal(board?.supplyType, "SV"); - assert.equal(board?.simultaneityFactor, 1); - } finally { - context.close(); - } - }); - it("sends distribution-board writes to the revision-safe API", async () => { const requests: Array<{ url: string; method: string; body: unknown }> = []; const originalFetch = globalThis.fetch; diff --git a/tests/distribution-board-subtree.test.ts b/tests/distribution-board-subtree.test.ts index 3faaa12..33d5318 100644 --- a/tests/distribution-board-subtree.test.ts +++ b/tests/distribution-board-subtree.test.ts @@ -82,9 +82,6 @@ function createSnapshot(): DistributionBoardSubtreeSnapshot { equipmentIdentifier: "-1F1.1", displayName: "Licht", sortOrder: 10, - protectionType: null, - protectionRatedCurrent: null, - protectionCharacteristic: null, cableType: "NYM-J", cableCrossSection: "3x1,5 mm²", cableLength: 10, @@ -100,7 +97,6 @@ function createSnapshot(): DistributionBoardSubtreeSnapshot { id: "row-1", circuitId: "circuit-1", linkedProjectDeviceId: "device-1", - legacyConsumerId: "legacy-1", sortOrder: 10, name: "Leuchte", displayName: "Leuchte", @@ -176,7 +172,6 @@ describe("distribution-board subtree snapshot", () => { "device-1" ); assert.equal(clone.circuits[0].deviceRows[0].roomId, "room-1"); - assert.equal(clone.circuits[0].deviceRows[0].legacyConsumerId, null); assert.equal(clone.circuits[0].protectionDevice?.circuitId, "new-3"); assert.doesNotThrow(() => assertDistributionBoardSubtreeSnapshot(clone)); }); diff --git a/tests/legacy-consumer-migration-planner.test.ts b/tests/legacy-consumer-migration-planner.test.ts deleted file mode 100644 index e541e73..0000000 --- a/tests/legacy-consumer-migration-planner.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; -import { - inferSectionKeyFromEquipmentIdentifier, - inferSectionKeyFromLegacyInput, - normalizeCircuitNumber, -} from "../src/domain/services/legacy-consumer-migration-planner.js"; - -describe("legacy consumer migration planner", () => { - it("normalizes valid circuit numbers and rejects invalid formats", () => { - assert.equal(normalizeCircuitNumber(" -2f12 "), "-2F12"); - assert.equal(normalizeCircuitNumber("2F12"), null); - assert.equal(normalizeCircuitNumber(""), null); - assert.equal(normalizeCircuitNumber(null), null); - }); - - it("infers section from equipment identifier prefix", () => { - assert.equal(inferSectionKeyFromEquipmentIdentifier("-1F4"), "lighting"); - assert.equal(inferSectionKeyFromEquipmentIdentifier("-2F8"), "single_phase"); - assert.equal(inferSectionKeyFromEquipmentIdentifier("-3F1"), "three_phase"); - assert.equal(inferSectionKeyFromEquipmentIdentifier("-9F1"), null); - }); - - it("infers section from category/phase when circuit number is missing", () => { - assert.equal( - inferSectionKeyFromLegacyInput({ - id: "1", - circuitNumber: null, - category: "Beleuchtung", - phaseType: null, - phaseCount: null, - }), - "lighting" - ); - assert.equal( - inferSectionKeyFromLegacyInput({ - id: "2", - circuitNumber: null, - category: null, - phaseType: "three-phase", - phaseCount: null, - }), - "three_phase" - ); - assert.equal( - inferSectionKeyFromLegacyInput({ - id: "3", - circuitNumber: null, - category: null, - phaseType: null, - phaseCount: null, - }), - null - ); - }); -}); - diff --git a/tests/legacy-consumer-migration.repository.test.ts b/tests/legacy-consumer-migration.repository.test.ts deleted file mode 100644 index ae4f874..0000000 --- a/tests/legacy-consumer-migration.repository.test.ts +++ /dev/null @@ -1,245 +0,0 @@ -import path from "node:path"; -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; -import { eq } from "drizzle-orm"; -import { migrate } from "drizzle-orm/better-sqlite3/migrator"; -import { - createDatabaseContext, - type DatabaseContext, -} from "../src/db/database-context.js"; -import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js"; -import { LegacyConsumerMigrationRepository } from "../src/db/repositories/legacy-consumer-migration.repository.js"; -import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js"; -import { circuitSections } from "../src/db/schema/circuit-sections.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 { legacyConsumerMigrationReports } from "../src/db/schema/legacy-consumer-migration-report.js"; -import { projects } from "../src/db/schema/projects.js"; - -function createTestDatabase() { - const context = createDatabaseContext(":memory:"); - migrate(context.db, { - migrationsFolder: path.resolve("src", "db", "migrations"), - }); - context.db.insert(projects).values({ id: "project-1", name: "Test project" }).run(); - const board = new DistributionBoardFixtureRepository( - context.db - ).createWithCircuitListAndDefaultSections("project-1", "UV-01"); - const [section] = context.db - .select() - .from(circuitSections) - .where(eq(circuitSections.circuitListId, board.id)) - .limit(1) - .all(); - return { context, circuitListId: board.id, sectionId: section.id }; -} - -function migrationInput(circuitListId: string, sectionId: string) { - return { - circuitListId, - circuits: [ - { - circuit: { - circuitListId, - sectionId, - equipmentIdentifier: "-1F1", - displayName: "Legacy lighting", - sortOrder: 10, - }, - deviceRows: [ - { - legacyConsumerId: "consumer-1", - sortOrder: 10, - name: "Light 1", - displayName: "Light 1", - quantity: 1, - powerPerUnit: 0.1, - simultaneityFactor: 1, - }, - { - legacyConsumerId: "consumer-2", - sortOrder: 20, - name: "Light 2", - displayName: "Light 2", - quantity: 2, - powerPerUnit: 0.2, - simultaneityFactor: 0.8, - }, - ], - }, - ], - report: { - legacyConsumerCount: 2, - createdCircuitCount: 1, - createdDeviceRowCount: 2, - duplicateGroupedCount: 1, - generatedIdentifierCount: 0, - unassignedRowCount: 0, - warningsJson: "[]", - generatedIdentifiersJson: "[]", - duplicateGroupsJson: - "[{\"normalizedCircuitNumber\":\"-1F1\",\"count\":2}]", - }, - }; -} - -function assertNoMigrationWrites(context: DatabaseContext) { - assert.equal(context.db.select().from(circuits).all().length, 0); - assert.equal(context.db.select().from(circuitDeviceRows).all().length, 0); - assert.equal( - context.db.select().from(legacyConsumerCircuitMigrations).all().length, - 0 - ); - assert.equal( - context.db.select().from(legacyConsumerMigrationReports).all().length, - 0 - ); -} - -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.listSourceConsumersByCircuitList(circuitListId)).map( - (consumer) => consumer.id - ), - ["consumer-1"] - ); - 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", () => { - const { context, circuitListId, sectionId } = createTestDatabase(); - try { - const repository = new LegacyConsumerMigrationRepository(context.db); - - repository.persistCircuitListMigration( - migrationInput(circuitListId, sectionId) - ); - - const persistedCircuits = context.db.select().from(circuits).all(); - const persistedRows = context.db - .select() - .from(circuitDeviceRows) - .all() - .sort((left, right) => left.sortOrder - right.sortOrder); - const persistedMappings = context.db - .select() - .from(legacyConsumerCircuitMigrations) - .all() - .sort((left, right) => left.consumerId.localeCompare(right.consumerId)); - const persistedReports = context.db - .select() - .from(legacyConsumerMigrationReports) - .all(); - - assert.equal(persistedCircuits.length, 1); - assert.equal(persistedRows.length, 2); - assert.equal(persistedMappings.length, 2); - assert.equal(persistedReports.length, 1); - assert.deepEqual( - persistedMappings.map((mapping) => [ - mapping.consumerId, - mapping.circuitId, - mapping.circuitDeviceRowId, - mapping.circuitListId, - ]), - [ - [ - "consumer-1", - persistedCircuits[0].id, - persistedRows[0].id, - circuitListId, - ], - [ - "consumer-2", - persistedCircuits[0].id, - persistedRows[1].id, - circuitListId, - ], - ] - ); - assert.equal(persistedReports[0].createdCircuitCount, 1); - assert.equal(persistedReports[0].createdDeviceRowCount, 2); - } finally { - context.close(); - } - }); - - it("rolls back all migrated entities when the final report write fails", () => { - const { context, circuitListId, sectionId } = createTestDatabase(); - try { - context.sqlite.exec(` - CREATE TRIGGER fail_migration_report - BEFORE INSERT ON legacy_consumer_migration_reports - BEGIN - SELECT RAISE(ABORT, 'forced migration report failure'); - END; - `); - const repository = new LegacyConsumerMigrationRepository(context.db); - - assert.throws( - () => - repository.persistCircuitListMigration( - migrationInput(circuitListId, sectionId) - ), - /forced migration report failure/ - ); - - assertNoMigrationWrites(context); - } finally { - context.close(); - } - }); -}); diff --git a/tests/legacy-consumer-migration.service.test.ts b/tests/legacy-consumer-migration.service.test.ts deleted file mode 100644 index b78aca7..0000000 --- a/tests/legacy-consumer-migration.service.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; -import type { - LegacyConsumerMigrationDependencies, - LegacyMigrationCircuitInput, - LegacyMigrationReportInput, -} from "../src/domain/ports/legacy-consumer-migration.store.js"; -import { LegacyConsumerMigrationService } from "../src/domain/services/legacy-consumer-migration.service.js"; - -describe("legacy consumer migration service", () => { - it("plans the migration through injected readers and persists one complete batch", async () => { - let defaultsCreatedFor: string | null = null; - let persisted: - | { - circuitListId: string; - circuits: LegacyMigrationCircuitInput[]; - report: LegacyMigrationReportInput; - } - | undefined; - const dependencies: LegacyConsumerMigrationDependencies = { - circuitListReader: { - async findById(projectId, circuitListId) { - assert.equal(projectId, "project-1"); - assert.equal(circuitListId, "list-1"); - return { id: circuitListId }; - }, - }, - sectionStore: { - async createDefaults(circuitListId) { - defaultsCreatedFor = circuitListId; - }, - async listByCircuitList() { - return [ - { id: "section-1", key: "single_phase", prefix: "-2F" }, - { id: "section-2", key: "unassigned", prefix: "-XF" }, - ]; - }, - }, - circuitReader: { - async listByCircuitList() { - return [ - { - sectionId: "section-1", - equipmentIdentifier: "-2F2", - sortOrder: 10, - }, - ]; - }, - }, - roomReader: { - async listByProject() { - return [ - { id: "room-1", roomNumber: "1.01", roomName: "Besprechung" }, - ]; - }, - }, - migrationStore: { - async listSourceConsumersByCircuitList() { - return [ - { - id: "consumer-1", - projectDeviceId: null, - roomId: "room-1", - circuitNumber: null, - description: "Steckdosen Besprechung", - name: "Steckdose", - category: null, - deviceType: null, - phaseType: null, - tradeOrCostGroup: "KG 440", - protectionType: null, - protectionRatedCurrent: null, - protectionCharacteristic: null, - cableType: null, - cableCrossSection: null, - comment: null, - quantity: 2, - installedPowerPerUnitKw: 0.2, - demandFactor: 0.8, - voltageV: 230, - phaseCount: 1, - powerFactor: 0.95, - note: "Bestand", - }, - ]; - }, - async listMigratedConsumerIds() { - return []; - }, - persistCircuitListMigration(input) { - persisted = input; - }, - }, - }; - - const report = await new LegacyConsumerMigrationService( - dependencies - ).migrateCircuitList("project-1", "list-1"); - - assert.equal(defaultsCreatedFor, "list-1"); - assert.deepEqual(report.generatedIdentifiers, ["-2F3"]); - assert.equal(report.createdCircuitCount, 1); - assert.equal(report.createdDeviceRowCount, 1); - assert.equal(persisted?.circuitListId, "list-1"); - assert.equal(persisted?.circuits[0].circuit.sectionId, "section-1"); - assert.equal( - persisted?.circuits[0].circuit.equipmentIdentifier, - "-2F3" - ); - assert.deepEqual(persisted?.circuits[0].deviceRows[0], { - linkedProjectDeviceId: undefined, - legacyConsumerId: "consumer-1", - sortOrder: 10, - name: "Steckdose", - displayName: "Steckdosen Besprechung", - phaseType: undefined, - connectionKind: undefined, - costGroup: "KG 440", - category: undefined, - roomId: "room-1", - roomNumberSnapshot: "1.01", - roomNameSnapshot: "Besprechung", - quantity: 2, - powerPerUnit: 0.2, - simultaneityFactor: 0.8, - cosPhi: 0.95, - remark: "Bestand", - }); - }); -}); diff --git a/tests/project-command.model.test.ts b/tests/project-command.model.test.ts index 1eb7c3f..9284f47 100644 --- a/tests/project-command.model.test.ts +++ b/tests/project-command.model.test.ts @@ -60,7 +60,7 @@ describe("serialized project commands", () => { circuitId: "circuit-1", changes: { displayName: "Werkstatt", - protectionRatedCurrent: 16, + cableLength: 16, isReserve: false, remark: null, }, @@ -378,7 +378,6 @@ describe("project-device structure project commands", () => { id: "row-1", circuitId: "circuit-1", linkedProjectDeviceId: null, - legacyConsumerId: null, sortOrder: 10, name: "Luminaire", displayName: "Local lighting", @@ -445,7 +444,6 @@ describe("circuit device-row structure project commands", () => { id: "row-1", circuitId: "circuit-1", linkedProjectDeviceId: null, - legacyConsumerId: null, sortOrder: 10, name: "Leuchte", displayName: "Leuchte", @@ -517,9 +515,6 @@ describe("circuit device-row move project commands", () => { equipmentIdentifier: "-1F3", displayName: "Neuer Stromkreis", sortOrder: 30, - protectionType: null, - protectionRatedCurrent: null, - protectionCharacteristic: null, cableType: null, cableCrossSection: null, cableLength: null, @@ -683,7 +678,6 @@ describe("circuit structure project commands", () => { id: "row-1", circuitId: "circuit-1", linkedProjectDeviceId: null, - legacyConsumerId: null, sortOrder: 10, name: "Leuchte", displayName: "Leuchte", @@ -709,9 +703,6 @@ describe("circuit structure project commands", () => { equipmentIdentifier: "-1F1", displayName: "Beleuchtung", sortOrder: 10, - protectionType: null, - protectionRatedCurrent: null, - protectionCharacteristic: null, cableType: null, cableCrossSection: null, cableLength: null, diff --git a/tests/project-command.service.test.ts b/tests/project-command.service.test.ts index 3a119e0..a96b4aa 100644 --- a/tests/project-command.service.test.ts +++ b/tests/project-command.service.test.ts @@ -555,7 +555,6 @@ describe("project command service", () => { id: "row-2", circuitId: "circuit-1", linkedProjectDeviceId: null, - legacyConsumerId: null, sortOrder: 20, name: "Steckdose", displayName: "Steckdose", @@ -623,9 +622,6 @@ describe("project command service", () => { equipmentIdentifier: "-1F2", displayName: "Reserve", sortOrder: 20, - protectionType: null, - protectionRatedCurrent: null, - protectionCharacteristic: null, cableType: null, cableCrossSection: null, cableLength: null, @@ -745,9 +741,6 @@ describe("project command service", () => { equipmentIdentifier: "-1F2", displayName: "Neuer Stromkreis", sortOrder: 20, - protectionType: null, - protectionRatedCurrent: null, - protectionCharacteristic: null, cableType: null, cableCrossSection: null, cableLength: null, diff --git a/tests/project-device-schema-migration.test.ts b/tests/project-device-schema-migration.test.ts deleted file mode 100644 index 62990c6..0000000 --- a/tests/project-device-schema-migration.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import assert from "node:assert/strict"; -import fs from "node:fs"; -import path from "node:path"; -import { describe, it } from "node:test"; -import Database from "better-sqlite3"; - -describe("project device canonical-field migration", () => { - it("preserves canonical values while removing transitional mirror columns", () => { - const sqlite = new Database(":memory:"); - try { - sqlite.exec(` - CREATE TABLE project_devices ( - id text PRIMARY KEY NOT NULL, - power_per_unit real NOT NULL, - simultaneity_factor real NOT NULL, - phase_type text NOT NULL, - cos_phi real, - remark text, - voltage_v real, - installed_power_per_unit_kw real NOT NULL, - demand_factor real NOT NULL, - phase_count integer, - power_factor real, - note text - ); - INSERT INTO project_devices VALUES ( - 'device-1', - 0.25, - 0.8, - 'three_phase', - 0.95, - 'Canonical remark', - 400, - 0.25, - 0.8, - 3, - 0.95, - 'Canonical remark' - ); - `); - - const migrationSql = fs.readFileSync( - path.resolve( - "src", - "db", - "migrations", - "0011_project_device_canonical_fields.sql" - ), - "utf8" - ); - for (const statement of migrationSql.split("--> statement-breakpoint")) { - if (statement.trim()) { - sqlite.exec(statement); - } - } - - const columns = ( - sqlite.prepare("PRAGMA table_info(project_devices)").all() as Array<{ - name: string; - }> - ).map((column) => column.name); - assert.deepEqual(columns, [ - "id", - "power_per_unit", - "simultaneity_factor", - "phase_type", - "cos_phi", - "remark", - "voltage_v", - ]); - assert.deepEqual( - sqlite - .prepare( - `SELECT - power_per_unit AS powerPerUnit, - simultaneity_factor AS simultaneityFactor, - phase_type AS phaseType, - cos_phi AS cosPhi, - remark, - voltage_v AS voltageV - FROM project_devices - WHERE id = ?` - ) - .get("device-1"), - { - powerPerUnit: 0.25, - simultaneityFactor: 0.8, - phaseType: "three_phase", - cosPhi: 0.95, - remark: "Canonical remark", - voltageV: 400, - } - ); - } finally { - sqlite.close(); - } - }); -}); diff --git a/tests/project-device-schema.test.ts b/tests/project-device-schema.test.ts index 55d078d..7ee2f89 100644 --- a/tests/project-device-schema.test.ts +++ b/tests/project-device-schema.test.ts @@ -30,8 +30,8 @@ describe("project device circuit-first schema", () => { it("requires the circuit-first power and phase fields", () => { const result = createProjectDeviceSchema.safeParse({ - name: "Legacy device", - displayName: "Legacy device", + name: "Test device", + displayName: "Test device", quantity: 1, installedPowerPerUnitKw: 0.1, demandFactor: 1, diff --git a/tests/project-device-sync.service.test.ts b/tests/project-device-sync.service.test.ts index e5a2191..f3fa03f 100644 --- a/tests/project-device-sync.service.test.ts +++ b/tests/project-device-sync.service.test.ts @@ -27,7 +27,6 @@ function linkedRow() { id: "row1", circuitId: "c1", linkedProjectDeviceId: "pd1", - legacyConsumerId: null, sortOrder: 10, name: "Old luminaire", displayName: "Local display name", diff --git a/tests/project-location-structure-project-command.repository.test.ts b/tests/project-location-structure-project-command.repository.test.ts index 7f7a61e..19f278b 100644 --- a/tests/project-location-structure-project-command.repository.test.ts +++ b/tests/project-location-structure-project-command.repository.test.ts @@ -9,7 +9,6 @@ import { } from "../src/db/database-context.js"; import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js"; import { ProjectLocationStructureProjectCommandRepository } from "../src/db/repositories/project-location-structure-project-command.repository.js"; -import { consumers } from "../src/db/schema/consumers.js"; import { distributionBoards } from "../src/db/schema/distribution-boards.js"; import { floors } from "../src/db/schema/floors.js"; import { projectRevisions } from "../src/db/schema/project-revisions.js"; @@ -423,53 +422,6 @@ describe("project-location structure project command", () => { floorContext.close(); } - const roomContext = createTestDatabase(); - try { - const repository = - new ProjectLocationStructureProjectCommandRepository( - roomContext.db - ); - const room = createProjectRoomSnapshot("project-1", { - roomNumber: "001", - roomName: "Technik", - }); - const inserted = repository.execute({ - projectId: "project-1", - expectedRevision: 0, - source: "user", - command: createProjectRoomInsertProjectCommand(room), - }); - roomContext.db - .insert(consumers) - .values({ - id: "legacy-consumer-1", - projectId: "project-1", - roomId: room.id, - name: "Bestand", - quantity: 1, - installedPowerPerUnitKw: 1, - demandFactor: 1, - }) - .run(); - const undoStep = new ProjectHistoryRepository( - roomContext.db - ).getNextCommand("project-1", "undo"); - assert.ok(undoStep); - assert.throws( - () => - repository.execute({ - projectId: "project-1", - expectedRevision: 1, - source: "undo", - historyTargetChangeSetId: undoStep.changeSetId, - command: inserted.inverse, - }), - /referenced project room/ - ); - } finally { - roomContext.close(); - } - const rollbackContext = createTestDatabase(); try { rollbackContext.sqlite.exec(` diff --git a/tests/project-settings-project-command.repository.test.ts b/tests/project-settings-project-command.repository.test.ts index dc3aada..393ba4e 100644 --- a/tests/project-settings-project-command.repository.test.ts +++ b/tests/project-settings-project-command.repository.test.ts @@ -365,37 +365,6 @@ describe("project settings project-command repository", () => { } }); - it("applies persisted version-one voltage commands without clearing metadata", () => { - const context = createTestDatabase(); - try { - const repository = new ProjectSettingsProjectCommandRepository( - context.db - ); - repository.executeUpdate({ - projectId: "project-1", - expectedRevision: 0, - source: "user", - command: { - schemaVersion: 1, - type: "project.update-settings", - payload: { - singlePhaseVoltageV: 240, - threePhaseVoltageV: 415, - }, - }, - }); - assert.deepEqual(getSettings(context), { - ...settings({ - singlePhaseVoltageV: 240, - threePhaseVoltageV: 415, - }), - currentRevision: 1, - }); - } finally { - context.close(); - } - }); - it("persists the project supply-type selection and rejects disabling an in-use type", () => { const context = createTestDatabase(); try { diff --git a/tests/project-snapshot.repository.test.ts b/tests/project-snapshot.repository.test.ts index d70dace..b062c68 100644 --- a/tests/project-snapshot.repository.test.ts +++ b/tests/project-snapshot.repository.test.ts @@ -172,18 +172,6 @@ function createTestDatabase(): DatabaseContext { return context; } -function removeVersionSevenFields(state: Record) { - delete state.distributionBoardComponents; - delete state.circuitProtectionDevices; - delete state.distributionBoardComponentProtectionDevices; - for (const section of state.circuitSections as Array< - Record - >) { - delete section.category; - delete section.groupNumber; - } -} - describe("project snapshot repository", () => { it("captures, hashes and lists a complete named logical snapshot", () => { const context = createTestDatabase(); @@ -354,41 +342,23 @@ describe("project snapshot repository", () => { ) as Record & { project: Record; }; - currentPayload.schemaVersion = 1; - removeVersionSevenFields(currentPayload); - delete currentPayload.project.internalProjectNumber; - delete currentPayload.project.externalProjectNumber; - delete currentPayload.project.buildingOwner; - delete currentPayload.project.description; - delete currentPayload.project.enabledDistributionBoardSupplyTypes; - for (const board of currentPayload.distributionBoards as Array< - Record - >) { - delete board.floorId; - delete board.supplyType; - delete board.simultaneityFactor; - } - const legacyPayloadJson = JSON.stringify(currentPayload); + currentPayload.schemaVersion = 99; + const unsupportedPayloadJson = JSON.stringify(currentPayload); context.db .update(projectSnapshots) .set({ - schemaVersion: 1, - payloadJson: legacyPayloadJson, + schemaVersion: 99, + payloadJson: unsupportedPayloadJson, payloadSha256: crypto .createHash("sha256") - .update(legacyPayloadJson) + .update(unsupportedPayloadJson) .digest("hex"), }) .where(eq(projectSnapshots.id, snapshot.id)) .run(); - const legacyPrepared = repository.prepareRestore( - "project-1", - snapshot.id - ); - assert.ok(legacyPrepared); - assert.equal( - legacyPrepared.command.payload.targetState.project.buildingOwner, - null + assert.throws( + () => repository.prepareRestore("project-1", snapshot.id), + /schema version is not supported/ ); context.db .update(projectSnapshots) @@ -474,10 +444,6 @@ describe("portable project transfer", () => { copied.state.distributionBoards[0].simultaneityFactor, 0.65 ); - assert.equal( - copied.state.circuits[0].deviceRows[0].legacyConsumerId, - null - ); } finally { context.close(); } @@ -512,52 +478,6 @@ describe("portable project transfer", () => { } }); - it("imports a checksum-valid version-two project transfer", () => { - const context = createTestDatabase(); - try { - const repository = new ProjectTransferRepository(context.db); - const current = repository.exportProject("project-1"); - assert.ok(current); - const legacyState = structuredClone( - current.projectState - ) as unknown as Record & { - distributionBoards: Array>; - }; - legacyState.schemaVersion = 2; - removeVersionSevenFields(legacyState); - delete ( - legacyState.project as Record - ).enabledDistributionBoardSupplyTypes; - for (const board of legacyState.distributionBoards) { - delete board.floorId; - delete board.supplyType; - delete board.simultaneityFactor; - } - const legacyPayloadJson = JSON.stringify(legacyState); - const duplicate = repository.importDuplicate({ - ...current, - projectState: legacyState, - payloadSha256: crypto - .createHash("sha256") - .update(legacyPayloadJson) - .digest("hex"), - }); - const copied = readProjectStateSnapshot( - context.db, - duplicate.projectId - ); - assert.ok(copied); - assert.equal(copied.state.distributionBoards[0].floorId, null); - assert.equal(copied.state.distributionBoards[0].supplyType, null); - assert.equal( - copied.state.distributionBoards[0].simultaneityFactor, - 1 - ); - } finally { - context.close(); - } - }); - it("rolls back a duplicate when a late project-state insert fails", () => { const context = createTestDatabase(); try { diff --git a/tests/project-state-restore-command.repository.test.ts b/tests/project-state-restore-command.repository.test.ts index b44d3c0..fff87e0 100644 --- a/tests/project-state-restore-command.repository.test.ts +++ b/tests/project-state-restore-command.repository.test.ts @@ -37,12 +37,9 @@ import { circuitLists } from "../src/db/schema/circuit-lists.js"; import { circuitProtectionDevices } from "../src/db/schema/circuit-protection-devices.js"; import { circuitSections } from "../src/db/schema/circuit-sections.js"; import { circuits } from "../src/db/schema/circuits.js"; -import { consumers } from "../src/db/schema/consumers.js"; import { distributionBoardComponentProtectionDevices } from "../src/db/schema/distribution-board-component-protection-devices.js"; import { distributionBoardComponents } from "../src/db/schema/distribution-board-components.js"; import { floors } from "../src/db/schema/floors.js"; -import { legacyConsumerCircuitMigrations } from "../src/db/schema/legacy-consumer-circuit-migrations.js"; -import { legacyConsumerMigrationReports } from "../src/db/schema/legacy-consumer-migration-report.js"; import { projectDevices } from "../src/db/schema/project-devices.js"; import { projectRevisions } from "../src/db/schema/project-revisions.js"; import { projectSnapshots } from "../src/db/schema/project-snapshots.js"; @@ -282,59 +279,6 @@ function changeCompleteProjectState(context: DatabaseContext) { ).createWithCircuitListAndDefaultSections("project-1", "UV-02"); } -function seedUpgradeOnlyData(context: DatabaseContext) { - const circuit = context.db - .select() - .from(circuits) - .where(eq(circuits.id, "circuit-1")) - .get(); - assert.ok(circuit); - context.db - .insert(consumers) - .values({ - id: "consumer-1", - projectId: "project-1", - distributionBoardId: - context.db.select().from(circuitLists).get()! - .distributionBoardId, - circuitListId: circuit.circuitListId, - projectDeviceId: "device-1", - roomId: "room-1", - name: "Legacy-Pumpe", - quantity: 1, - installedPowerPerUnitKw: 2.5, - demandFactor: 0.8, - }) - .run(); - context.db - .insert(legacyConsumerCircuitMigrations) - .values({ - consumerId: "consumer-1", - circuitId: "circuit-1", - circuitDeviceRowId: "row-1", - circuitListId: circuit.circuitListId, - createdAtIso: "2026-07-25T00:00:00.000Z", - }) - .run(); - context.db - .insert(legacyConsumerMigrationReports) - .values({ - id: "report-1", - circuitListId: circuit.circuitListId, - legacyConsumerCount: 1, - createdCircuitCount: 1, - createdDeviceRowCount: 1, - duplicateGroupedCount: 0, - generatedIdentifierCount: 0, - unassignedRowCount: 0, - warningsJson: "[]", - generatedIdentifiersJson: "[]", - duplicateGroupsJson: "[]", - createdAtIso: "2026-07-25T00:00:00.000Z", - }) - .run(); -} - describe("project state restore command", () => { it("undoes a remapped cross-project import using the persisted target hash", () => { const context = createTestDatabase(); @@ -360,7 +304,7 @@ describe("project state restore command", () => { description: "Projektimport wiederherstellen", command: prepared.command, }); - const legacyExpectedHash = hashProjectStatePayload( + const serializedExpectedHash = hashProjectStatePayload( serializeProjectStateSnapshot( prepared.command.payload.targetState ) @@ -372,7 +316,7 @@ describe("project state restore command", () => { .get(); assert.ok(storedChangeSet); const inverse = JSON.parse(storedChangeSet.inversePayloadJson); - inverse.payload.expectedStateSha256 = legacyExpectedHash; + inverse.payload.expectedStateSha256 = serializedExpectedHash; context.db .update(projectChangeSets) .set({ inversePayloadJson: JSON.stringify(inverse) }) @@ -397,7 +341,6 @@ describe("project state restore command", () => { const context = createTestDatabase(); try { const snapshots = new ProjectSnapshotRepository(context.db); - seedUpgradeOnlyData(context); const snapshot = snapshots.createNamed({ projectId: "project-1", expectedRevision: 0, @@ -443,28 +386,6 @@ describe("project state restore command", () => { context.db.select().from(projectSnapshots).all().length, 1 ); - assert.equal( - context.db - .select() - .from(legacyConsumerCircuitMigrations) - .all().length, - 1 - ); - assert.equal( - context.db - .select() - .from(legacyConsumerMigrationReports) - .all().length, - 1 - ); - assert.equal( - context.db - .select({ roomId: consumers.roomId }) - .from(consumers) - .where(eq(consumers.id, "consumer-1")) - .get()?.roomId, - "room-1" - ); const undone = service.undo({ projectId: "project-1", diff --git a/tests/project-state-snapshot.test.ts b/tests/project-state-snapshot.test.ts index 66aeb72..e408263 100644 --- a/tests/project-state-snapshot.test.ts +++ b/tests/project-state-snapshot.test.ts @@ -42,17 +42,6 @@ function minimalSnapshot() { }; } -function legacyMinimalSnapshot() { - const { - distributionBoardComponents: _distributionBoardComponents, - circuitProtectionDevices: _circuitProtectionDevices, - distributionBoardComponentProtectionDevices: - _distributionBoardComponentProtectionDevices, - ...snapshot - } = minimalSnapshot(); - return snapshot; -} - function protectedSnapshot() { return { ...minimalSnapshot(), @@ -94,9 +83,6 @@ function protectedSnapshot() { equipmentIdentifier: "-1F1.1", displayName: "Reserve", sortOrder: 10, - protectionType: null, - protectionRatedCurrent: null, - protectionCharacteristic: null, cableType: null, cableCrossSection: null, cableLength: null, @@ -148,234 +134,26 @@ function protectedSnapshot() { } describe("project state snapshot model", () => { - it("round-trips a versioned logical project state", () => { + it("round-trips the current logical project state", () => { const snapshot = parseProjectStateSnapshot(minimalSnapshot()); assert.deepEqual( - deserializeProjectStateSnapshot( - serializeProjectStateSnapshot(snapshot) - ), + deserializeProjectStateSnapshot(serializeProjectStateSnapshot(snapshot)), snapshot ); }); - it("upgrades version-one snapshots with empty project metadata", () => { - const legacy = { - ...legacyMinimalSnapshot(), - schemaVersion: 1 as const, - project: { - id: "project-1", - name: "Altprojekt", - singlePhaseVoltageV: 230, - threePhaseVoltageV: 400, - }, - }; - const snapshot = parseProjectStateSnapshot(legacy); - assert.equal(snapshot.schemaVersion, projectStateSnapshotSchemaVersion); - assert.deepEqual(snapshot.project, { - ...legacy.project, - internalProjectNumber: null, - externalProjectNumber: null, - buildingOwner: null, - description: null, - enabledDistributionBoardSupplyTypes: [ - "AV", - "SV", - "EV", - "USV", - "MSR", - "SiBe", - ], - }); - }); - - it("upgrades version-two distribution boards without inferred assignments", () => { - const previous = { - ...legacyMinimalSnapshot(), - schemaVersion: 2 as const, - project: { - ...minimalSnapshot().project, - enabledDistributionBoardSupplyTypes: undefined, - }, - distributionBoards: [ - { - id: "board-1", - projectId: "project-1", - name: "UV 1", - }, - ], - }; - delete previous.project.enabledDistributionBoardSupplyTypes; - const snapshot = parseProjectStateSnapshot(previous); - assert.deepEqual(snapshot.distributionBoards, [ - { - ...previous.distributionBoards[0], - floorId: null, - supplyType: null, - simultaneityFactor: 1, - }, - ]); - }); - - it("upgrades version-three snapshots with all project supply types", () => { - const previous = { - ...legacyMinimalSnapshot(), - schemaVersion: 3 as const, - project: { - ...minimalSnapshot().project, - enabledDistributionBoardSupplyTypes: undefined, - }, - }; - delete previous.project.enabledDistributionBoardSupplyTypes; - const snapshot = parseProjectStateSnapshot(previous); - assert.deepEqual(snapshot.project.enabledDistributionBoardSupplyTypes, [ - "AV", - "SV", - "EV", - "USV", - "MSR", - "SiBe", - ]); - }); - - it("normalizes version-four device voltages to project settings", () => { - const previous = { - ...legacyMinimalSnapshot(), - schemaVersion: 4 as const, - projectDevices: [ - { - id: "device-1", - projectId: "project-1", - name: "Pumpe", - displayName: "Pumpe", - phaseType: "three_phase" as const, - connectionKind: null, - costGroup: null, - category: null, - quantity: 1, - powerPerUnit: 2, - simultaneityFactor: 1, - cosPhi: null, - remark: null, - voltageV: 500, - }, - ], - }; - - const snapshot = parseProjectStateSnapshot(previous); - - assert.equal(snapshot.schemaVersion, projectStateSnapshotSchemaVersion); - assert.equal(snapshot.projectDevices[0]?.voltageV, 400); - }); - - it("upgrades version-five distribution boards with a neutral factor", () => { - const previous = { - ...legacyMinimalSnapshot(), - schemaVersion: 5 as const, - distributionBoards: [ - { - id: "board-1", - projectId: "project-1", - name: "UV 1", - floorId: null, - supplyType: "AV" as const, - }, - ], - }; - - const snapshot = parseProjectStateSnapshot(previous); - - assert.equal(snapshot.schemaVersion, projectStateSnapshotSchemaVersion); - assert.equal( - snapshot.distributionBoards[0]?.simultaneityFactor, - 1 + it("rejects pre-baseline snapshot shapes", () => { + const outdated = structuredClone(minimalSnapshot()); + delete (outdated.project as Record).description; + assert.throws(() => parseProjectStateSnapshot(outdated)); + assert.throws(() => + parseProjectStateSnapshot({ ...minimalSnapshot(), schemaVersion: 7 }) ); }); - it("upgrades version-six sections without inventing component data", () => { - const previous = { - ...legacyMinimalSnapshot(), - schemaVersion: 6 as const, - circuitSections: [ - { - id: "section-lighting", - circuitListId: "list-1", - key: "lighting", - displayName: "Beleuchtung", - prefix: "-1F", - sortOrder: 10, - }, - { - id: "section-unassigned", - circuitListId: "list-1", - key: "unassigned", - displayName: "Nicht zugeordnet", - prefix: "-UF", - sortOrder: 90, - }, - ], - distributionBoards: [ - { - id: "board-1", - projectId: "project-1", - name: "UV 1", - floorId: null, - supplyType: null, - simultaneityFactor: 1, - }, - ], - circuitLists: [ - { - id: "list-1", - projectId: "project-1", - distributionBoardId: "board-1", - name: "UV 1 Stromkreisliste", - }, - ], - }; - - const snapshot = parseProjectStateSnapshot(previous); - - assert.deepEqual( - snapshot.circuitSections.map( - ({ key, category, groupNumber }) => ({ - key, - category, - groupNumber, - }) - ), - [ - { - key: "lighting", - category: "lighting", - groupNumber: 1, - }, - { - key: "unassigned", - category: null, - groupNumber: null, - }, - ] - ); - assert.deepEqual(snapshot.distributionBoardComponents, []); - assert.deepEqual(snapshot.circuitProtectionDevices, []); - assert.deepEqual( - snapshot.distributionBoardComponentProtectionDevices, - [] - ); - }); - - it("validates the complete group, component and protection graph", () => { + it("validates component, protection and equipment relations", () => { const snapshot = parseProjectStateSnapshot(protectedSnapshot()); - - assert.equal(snapshot.circuitSections[0]?.groupNumber, 1); - assert.equal( - snapshot.distributionBoardComponents[0]?.sectionId, - "section-1" - ); - assert.equal( - snapshot.circuitProtectionDevices[0]?.tripCharacteristic, - "B" - ); + assert.equal(snapshot.circuitProtectionDevices[0]?.tripCharacteristic, "B"); assert.equal( snapshot.distributionBoardComponentProtectionDevices[0] ?.ratedResidualCurrentMa, @@ -401,113 +179,27 @@ describe("project state snapshot model", () => { ); }); - it("rejects duplicate ids and cross-project ownership", () => { + it("rejects duplicate ids and invalid ownership", () => { + const duplicate = protectedSnapshot(); + duplicate.distributionBoards.push({ + ...duplicate.distributionBoards[0]!, + name: "UV 2", + }); assert.throws( - () => - parseProjectStateSnapshot({ - ...minimalSnapshot(), - distributionBoards: [ - { - id: "board-1", - projectId: "project-1", - name: "UV 1", - floorId: null, - supplyType: null, - simultaneityFactor: 1, - }, - { - id: "board-1", - projectId: "project-1", - name: "UV 2", - floorId: null, - supplyType: null, - simultaneityFactor: 1, - }, - ], - }), + () => parseProjectStateSnapshot(duplicate), /duplicate distribution board ids/ ); - assert.throws( - () => - parseProjectStateSnapshot({ - ...minimalSnapshot(), - floors: [ - { - id: "floor-1", - projectId: "project-2", - name: "EG", - sortOrder: 10, - }, - ], - }), - /floor belongs to a different project/ - ); - }); - it("rejects invalid references and inconsistent reserve circuits", () => { + const wrongOwner = minimalSnapshot(); + wrongOwner.floors.push({ + id: "floor-1", + projectId: "project-2", + name: "EG", + sortOrder: 10, + }); assert.throws( - () => - parseProjectStateSnapshot({ - ...minimalSnapshot(), - distributionBoards: [ - { - id: "board-1", - projectId: "project-1", - name: "UV 1", - floorId: "missing", - supplyType: "AV", - simultaneityFactor: 1, - }, - ], - }), - /distribution board floor reference is invalid/ - ); - assert.throws( - () => - parseProjectStateSnapshot({ - ...minimalSnapshot(), - rooms: [ - { - id: "room-1", - projectId: "project-1", - floorId: "missing", - roomNumber: "001", - roomName: "Technik", - }, - ], - }), - /room floor reference is invalid/ - ); - assert.throws( - () => - parseProjectStateSnapshot({ - ...minimalSnapshot(), - circuits: [ - { - id: "circuit-1", - circuitListId: "missing", - sectionId: "missing", - equipmentIdentifier: "-1F1", - displayName: null, - sortOrder: 10, - protectionType: null, - protectionRatedCurrent: null, - protectionCharacteristic: null, - cableType: null, - cableCrossSection: null, - cableLength: null, - rcdAssignment: null, - terminalDesignation: null, - voltage: null, - controlRequirement: null, - status: null, - isReserve: true, - remark: null, - deviceRows: [], - }, - ], - }), - /circuit list reference is invalid/ + () => parseProjectStateSnapshot(wrongOwner), + /floor belongs to a different project/ ); }); }); @@ -518,36 +210,29 @@ describe("named project snapshot request", () => { createNamedProjectSnapshotSchema.parse({ expectedRevision: 12, name: " Freigabe ", - description: " Stand vor Ausschreibung ", + description: " Stand vor Änderung ", }), { expectedRevision: 12, name: "Freigabe", - description: "Stand vor Ausschreibung", + description: "Stand vor Änderung", } ); }); it("rejects stale-shaped and excessive input", () => { - for (const input of [ - { expectedRevision: -1, name: "Stand" }, - { expectedRevision: 0, name: " " }, - { expectedRevision: 0, name: "x".repeat(101) }, - { - expectedRevision: 0, - name: "Stand", - description: "x".repeat(501), - }, - { - expectedRevision: 0, - name: "Stand", + assert.throws(() => + createNamedProjectSnapshotSchema.parse({ + expectedRevision: -1, + name: "", unexpected: true, - }, - ]) { - assert.equal( - createNamedProjectSnapshotSchema.safeParse(input).success, - false - ); - } + }) + ); + assert.throws(() => + createNamedProjectSnapshotSchema.parse({ + expectedRevision: 0, + name: "x".repeat(201), + }) + ); }); }); From 5fb81bab04ba9cb6448b499d63d995f6d5d205fb Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Fri, 31 Jul 2026 14:33:47 +0200 Subject: [PATCH 22/59] Support decimal commas and cell units --- .../components/circuit-tree-editor.tsx | 3 ++- .../components/circuit-tree-preview.tsx | 17 ++++++++-------- src/frontend/utils/circuit-grid-model.ts | 20 ++++++++++++++++++- tests/circuit-grid-model.test.ts | 6 ++++++ 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx index b214d60..3ecaf16 100644 --- a/src/frontend/components/circuit-tree-editor.tsx +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -27,6 +27,7 @@ import { buildDeviceRowEditPatch, defaultVisibleColumnKeys, deviceFieldKeys, + formatCellValue, formatPhaseTypeLabel, formatValue, getCircuitSectionLabel, @@ -4392,7 +4393,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str }} /> ) : ( - formatValue(cell.value, column.key) + formatCellValue(cell.value, column.key) )} ); diff --git a/src/frontend/components/circuit-tree-preview.tsx b/src/frontend/components/circuit-tree-preview.tsx index cd93739..207dee4 100644 --- a/src/frontend/components/circuit-tree-preview.tsx +++ b/src/frontend/components/circuit-tree-preview.tsx @@ -5,6 +5,7 @@ import { Fragment } from "react"; import { getCircuitTree } from "../utils/api"; import type { CircuitTreeCircuitDto, CircuitTreeResponseDto } from "../types"; import { + formatCellValue, formatValue, getCircuitSectionLabel, } from "../utils/circuit-grid-model"; @@ -112,7 +113,7 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number {circuit.displayName?.trim() || "Reserve"} - - {formatValue(circuit.circuitTotalPower, "circuitTotalPower")} + {formatCellValue(circuit.circuitTotalPower, "circuitTotalPower")} {circuit.protectionDevice?.type ?? "-"} @@ -140,13 +141,13 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number {row.level ?? "-"} {row.roomNumberSnapshot ?? "-"} {row.roomNameSnapshot ?? "-"} - {formatValue(row.quantity, "quantity")} - {formatValue(row.powerPerUnit, "powerPerUnit")} + {formatCellValue(row.quantity, "quantity")} + {formatCellValue(row.powerPerUnit, "powerPerUnit")} {formatValue(row.simultaneityFactor, "simultaneityFactor")} {formatValue(row.cosPhi, "cosPhi")} - {formatValue(row.rowTotalPower, "rowTotalPower")} + {formatCellValue(row.rowTotalPower, "rowTotalPower")} {circuit.protectionDevice?.type ?? "-"} {formatValue(circuit.protectionDevice?.ratedCurrentA, "protectionRatedCurrent")} @@ -167,7 +168,7 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number {renderCircuitSummaryLabel(circuit)} - - {formatValue(circuit.circuitTotalPower, "circuitTotalPower")} + {formatCellValue(circuit.circuitTotalPower, "circuitTotalPower")} {circuit.protectionDevice?.type ?? "-"} @@ -190,13 +191,13 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number {row.level ?? "-"} {row.roomNumberSnapshot ?? "-"} {row.roomNameSnapshot ?? "-"} - {formatValue(row.quantity, "quantity")} - {formatValue(row.powerPerUnit, "powerPerUnit")} + {formatCellValue(row.quantity, "quantity")} + {formatCellValue(row.powerPerUnit, "powerPerUnit")} {formatValue(row.simultaneityFactor, "simultaneityFactor")} {formatValue(row.cosPhi, "cosPhi")} - {formatValue(row.rowTotalPower, "rowTotalPower")} + {formatCellValue(row.rowTotalPower, "rowTotalPower")} - - - diff --git a/src/frontend/utils/circuit-grid-model.ts b/src/frontend/utils/circuit-grid-model.ts index 47e7189..bdb2fae 100644 --- a/src/frontend/utils/circuit-grid-model.ts +++ b/src/frontend/utils/circuit-grid-model.ts @@ -351,6 +351,24 @@ export function formatValue(value: GridValue, key?: CellKey): string { return String(value); } +export function formatCellValue(value: GridValue, key: CellKey): string { + const formatted = formatValue(value, key); + if (formatted === "-" || typeof value !== "number") { + return formatted; + } + if (key === "quantity") { + return `${formatted} St.`; + } + if ( + key === "powerPerUnit" || + key === "rowTotalPower" || + key === "circuitTotalPower" + ) { + return `${formatted} kW`; + } + return formatted; +} + export function formatPhaseTypeLabel(value: string): string { if (value === "single_phase") { return "1-phasig"; @@ -378,7 +396,7 @@ export function parseNumeric(cellKey: CellKey, draft: string): number | undefine if (trimmed === "") { return undefined; } - const parsed = Number(trimmed); + const parsed = Number(trimmed.replace(",", ".")); if (Number.isNaN(parsed)) { throw new Error(`Ungültiger Zahlenwert in ${cellKey}`); } diff --git a/tests/circuit-grid-model.test.ts b/tests/circuit-grid-model.test.ts index 6dbfef8..1e82a7c 100644 --- a/tests/circuit-grid-model.test.ts +++ b/tests/circuit-grid-model.test.ts @@ -5,6 +5,7 @@ import { buildCircuitEditPatch, buildDeviceRowEditPatch, formatValue, + formatCellValue, getCircuitSectionLabel, getProjectColumnLayoutStorageKey, isGridEditorControlTarget, @@ -173,6 +174,10 @@ describe("circuit grid model", () => { assert.equal(formatValue(16, "protectionRatedCurrent"), "16"); assert.equal(formatValue("single_phase", "phaseType"), "1-phasig"); assert.equal(formatValue("three_phase", "phaseType"), "3-phasig"); + assert.equal(formatCellValue(2, "quantity"), "2 St."); + assert.equal(formatCellValue(0.123456, "powerPerUnit"), "0,123 kW"); + assert.equal(formatCellValue(1.25, "rowTotalPower"), "1,25 kW"); + assert.equal(formatCellValue(undefined, "circuitTotalPower"), "-"); }); it("uses circuit display values for block sorting and falls back to the first device", () => { @@ -183,6 +188,7 @@ describe("circuit grid model", () => { it("parses numeric drafts and rejects invalid values", () => { assert.equal(parseNumeric("quantity", " 2.5 "), 2.5); + assert.equal(parseNumeric("powerPerUnit", " 1,25 "), 1.25); assert.equal(parseNumeric("quantity", ""), undefined); assert.throws(() => parseNumeric("quantity", "two"), /Ungültiger Zahlenwert/); }); From 1e3c721cfa691fc3b5dda5eb20b7d13dc7852e68 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Fri, 31 Jul 2026 14:37:45 +0200 Subject: [PATCH 23/59] Derive project device phases from category --- .../project-device-placement.service.ts | 17 ++-- .../components/circuit-tree-editor.tsx | 3 +- .../components/project-device-modal.tsx | 79 +++++++++++-------- src/frontend/types.ts | 5 +- .../controllers/project-device.controller.ts | 26 ++++-- .../validation/project-device.schemas.ts | 4 +- .../project-device-placement.service.test.ts | 33 +++----- tests/project-device-schema.test.ts | 9 +-- tests/project-version-history.test.ts | 5 +- 9 files changed, 101 insertions(+), 80 deletions(-) diff --git a/src/domain/services/project-device-placement.service.ts b/src/domain/services/project-device-placement.service.ts index 3bf2123..ba47974 100644 --- a/src/domain/services/project-device-placement.service.ts +++ b/src/domain/services/project-device-placement.service.ts @@ -1,6 +1,7 @@ +import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js"; + export interface ProjectDevicePlacementSource { - category?: string | null; - phaseType: "single_phase" | "three_phase"; + category: CircuitGroupCategory; } export interface ProjectDevicePlacementSection { @@ -14,11 +15,13 @@ export type DefaultCircuitSectionKey = "lighting" | "single_phase" | "three_phas export function inferProjectDeviceSectionKey( device: ProjectDevicePlacementSource ): DefaultCircuitSectionKey { - const category = (device.category ?? "").trim().toLowerCase(); - if (category.includes("light") || category.includes("beleuchtung")) { - return "lighting"; - } - return device.phaseType; + return device.category; +} + +export function resolveProjectDevicePhaseType( + category: CircuitGroupCategory +): "single_phase" | "three_phase" { + return category === "three_phase" ? "three_phase" : "single_phase"; } export function isProjectDevicePlacementValid( diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx index 3ecaf16..b63cd8e 100644 --- a/src/frontend/components/circuit-tree-editor.tsx +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -3258,7 +3258,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str > {device.displayName || device.name} Name: {device.name} - Phasenart: {formatPhaseTypeLabel(device.phaseType)} Anzahl: {formatValue(device.quantity, "quantity")} Leistung/Gerät: {formatValue(device.powerPerUnit, "powerPerUnit")} kW @@ -3271,7 +3270,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str Gesamtleistung: {formatValue(device.totalPower, "rowTotalPower")} kW Kostengruppe: {device.costGroup || "-"} - Kategorie: {device.category || "-"} + Kategorie: {circuitGroupCategoryLabels[device.category]} ))} {searchableProjectDevices.length === 0 ?

Keine passenden Projektgeräte gefunden.

: null} diff --git a/src/frontend/components/project-device-modal.tsx b/src/frontend/components/project-device-modal.tsx index 907fffe..55c9d25 100644 --- a/src/frontend/components/project-device-modal.tsx +++ b/src/frontend/components/project-device-modal.tsx @@ -7,6 +7,11 @@ import type { ProjectDeviceDto, } from "../types"; import { FormModal } from "./form-modal"; +import { + circuitGroupCategories, + circuitGroupCategoryLabels, + type CircuitGroupCategory, +} from "../../shared/constants/circuit-group"; interface ProjectDeviceModalProps { globalDevices: GlobalDeviceDto[]; @@ -37,14 +42,12 @@ export function ProjectDeviceModal({ await onSave({ name: values.name.trim(), displayName: values.displayName.trim() || values.name.trim(), - phaseType: - values.phaseType === "three_phase" ? "three_phase" : "single_phase", connectionKind: optionalString(values.connectionKind), costGroup: optionalString(values.costGroup), - category: optionalString(values.category), - quantity: Number(values.quantity), - powerPerUnit: Number(values.powerPerUnit), - simultaneityFactor: Number(values.simultaneityFactor), + category: values.category as CircuitGroupCategory, + quantity: parseDecimal(values.quantity), + powerPerUnit: parseDecimal(values.powerPerUnit), + simultaneityFactor: parseDecimal(values.simultaneityFactor), cosPhi: optionalNumber(values.cosPhi), remark: optionalString(values.remark), }); @@ -52,10 +55,10 @@ export function ProjectDeviceModal({ const isValid = values.name.trim().length > 0 && - Number(values.quantity) >= 0 && - Number(values.powerPerUnit) >= 0 && - Number(values.simultaneityFactor) >= 0 && - Number(values.simultaneityFactor) <= 1; + isNumberInRange(values.quantity, 0) && + isNumberInRange(values.powerPerUnit, 0) && + isNumberInRange(values.simultaneityFactor, 0, 1) && + (!values.cosPhi.trim() || isNumberInRange(values.cosPhi, 0, 1)); return ( update("displayName", value)} value={values.displayName} /> - update("category", value)} - value={values.category} - /> +
+ + +
update("costGroup", value)} value={values.costGroup} /> -
- - -
onChange(event.target.value)} step={step} - type="number" + inputMode="decimal" + type="text" value={value} />
@@ -255,10 +258,9 @@ function toFormValues(device?: ProjectDeviceDto) { return { name: device?.name ?? "", displayName: device?.displayName ?? "", - phaseType: device?.phaseType ?? "single_phase", connectionKind: device?.connectionKind ?? "", costGroup: device?.costGroup ?? "", - category: device?.category ?? "", + category: device?.category ?? "single_phase", quantity: String(device?.quantity ?? 1), powerPerUnit: String(device?.powerPerUnit ?? 0.1), simultaneityFactor: String(device?.simultaneityFactor ?? 1), @@ -272,5 +274,14 @@ function optionalString(value: string) { } function optionalNumber(value: string) { - return value.trim() ? Number(value) : undefined; + return value.trim() ? parseDecimal(value) : undefined; +} + +function parseDecimal(value: string) { + return Number(value.trim().replace(",", ".")); +} + +function isNumberInRange(value: string, min: number, max = Number.POSITIVE_INFINITY) { + const parsed = parseDecimal(value); + return Number.isFinite(parsed) && parsed >= min && parsed <= max; } diff --git a/src/frontend/types.ts b/src/frontend/types.ts index ee58d0d..f7ab550 100644 --- a/src/frontend/types.ts +++ b/src/frontend/types.ts @@ -173,7 +173,7 @@ export interface ProjectDeviceDto { phaseType: "single_phase" | "three_phase"; connectionKind: string | null; costGroup: string | null; - category: string | null; + category: CircuitGroupCategory; quantity: number; powerPerUnit: number; simultaneityFactor: number; @@ -218,10 +218,9 @@ export interface CreateGlobalDeviceInput { export interface CreateProjectDeviceInput { name: string; displayName: string; - phaseType: "single_phase" | "three_phase"; connectionKind?: string; costGroup?: string; - category?: string; + category: CircuitGroupCategory; quantity: number; powerPerUnit: number; simultaneityFactor: number; diff --git a/src/server/controllers/project-device.controller.ts b/src/server/controllers/project-device.controller.ts index 2878f61..b10e718 100644 --- a/src/server/controllers/project-device.controller.ts +++ b/src/server/controllers/project-device.controller.ts @@ -23,6 +23,8 @@ import { import type { CreateProjectDeviceInput } from "../../shared/validation/project-device.schemas.js"; import { respondWithProjectCommandError } from "./project-command.controller.js"; import { resolveProjectVoltage } from "../../domain/services/project-voltage.service.js"; +import { resolveProjectDevicePhaseType } from "../../domain/services/project-device-placement.service.js"; +import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js"; export async function listProjectDevicesByProject(req: Request, res: Response) { const { projectId } = req.params; @@ -156,8 +158,10 @@ export async function copyGlobalDeviceToProject(req: Request, res: Response) { const projectDevice = toProjectDeviceSnapshot(projectId, randomUUID(), { name: source.name, displayName: source.displayName, - phaseType: source.phaseCount === 3 ? "three_phase" : "single_phase", - category: source.category ?? undefined, + category: inferImportedProjectDeviceCategory( + source.category, + source.phaseCount + ), quantity: source.quantity, powerPerUnit: source.installedPowerPerUnitKw, simultaneityFactor: source.demandFactor, @@ -205,22 +209,34 @@ function toProjectDeviceValues( threePhaseVoltageV: number; } ) { + const phaseType = resolveProjectDevicePhaseType(input.category); return { name: input.name, displayName: input.displayName, - phaseType: input.phaseType, + phaseType, connectionKind: input.connectionKind ?? null, costGroup: input.costGroup ?? null, - category: input.category ?? null, + category: input.category, quantity: input.quantity, powerPerUnit: input.powerPerUnit, simultaneityFactor: input.simultaneityFactor, cosPhi: input.cosPhi ?? null, remark: input.remark ?? null, - voltageV: resolveProjectVoltage(input.phaseType, project), + voltageV: resolveProjectVoltage(phaseType, project), }; } +function inferImportedProjectDeviceCategory( + category: string | null, + phaseCount: number | null +): CircuitGroupCategory { + const normalized = category?.trim().toLowerCase() ?? ""; + if (normalized.includes("light") || normalized.includes("beleuchtung")) { + return "lighting"; + } + return phaseCount === 3 ? "three_phase" : "single_phase"; +} + export async function getProjectDeviceSyncPreview(req: Request, res: Response) { const { projectId, projectDeviceId } = req.params; if (typeof projectId !== "string" || typeof projectDeviceId !== "string") { diff --git a/src/shared/validation/project-device.schemas.ts b/src/shared/validation/project-device.schemas.ts index f252502..da6bf65 100644 --- a/src/shared/validation/project-device.schemas.ts +++ b/src/shared/validation/project-device.schemas.ts @@ -1,14 +1,14 @@ import { z } from "zod"; import { projectDeviceSyncFields } from "../constants/project-device-sync-fields.js"; import { expectedProjectRevisionSchema } from "./project-command.schemas.js"; +import { circuitGroupCategories } from "../constants/circuit-group.js"; export const createProjectDeviceSchema = z.object({ name: z.string().min(1), displayName: z.string().min(1), - phaseType: z.enum(["single_phase", "three_phase"]), connectionKind: z.string().optional(), costGroup: z.string().optional(), - category: z.string().optional(), + category: z.enum(circuitGroupCategories), quantity: z.number().min(0), powerPerUnit: z.number().min(0), simultaneityFactor: z.number().min(0).max(1), diff --git a/tests/project-device-placement.service.test.ts b/tests/project-device-placement.service.test.ts index fef7ee6..6625ced 100644 --- a/tests/project-device-placement.service.test.ts +++ b/tests/project-device-placement.service.test.ts @@ -3,36 +3,27 @@ import { describe, it } from "node:test"; import { inferProjectDeviceSectionKey, isProjectDevicePlacementValid, + resolveProjectDevicePhaseType, } from "../src/domain/services/project-device-placement.service.js"; describe("project device placement", () => { - it("routes lighting categories to the lighting section before phase classification", () => { - assert.equal( - inferProjectDeviceSectionKey({ category: "Lighting", phaseType: "three_phase" }), - "lighting" - ); - assert.equal( - inferProjectDeviceSectionKey({ category: "Beleuchtung", phaseType: "single_phase" }), - "lighting" - ); - }); - - it("routes non-lighting devices by phase type", () => { - assert.equal( - inferProjectDeviceSectionKey({ category: "Socket", phaseType: "single_phase" }), - "single_phase" - ); - assert.equal( - inferProjectDeviceSectionKey({ category: "Motor", phaseType: "three_phase" }), - "three_phase" - ); + it("routes project devices by their required category", () => { + assert.equal(inferProjectDeviceSectionKey({ category: "lighting" }), "lighting"); + assert.equal(inferProjectDeviceSectionKey({ category: "single_phase" }), "single_phase"); + assert.equal(inferProjectDeviceSectionKey({ category: "three_phase" }), "three_phase"); }); it("accepts only the inferred default section", () => { - const device = { category: "Motor", phaseType: "three_phase" as const }; + const device = { category: "three_phase" as const }; assert.equal(isProjectDevicePlacementValid(device, { key: "three_phase" }), true); assert.equal(isProjectDevicePlacementValid(device, { key: "single_phase" }), false); assert.equal(isProjectDevicePlacementValid(device, { key: "unassigned" }), false); }); + + it("derives the electrical phase type from the category", () => { + assert.equal(resolveProjectDevicePhaseType("lighting"), "single_phase"); + assert.equal(resolveProjectDevicePhaseType("single_phase"), "single_phase"); + assert.equal(resolveProjectDevicePhaseType("three_phase"), "three_phase"); + }); }); diff --git a/tests/project-device-schema.test.ts b/tests/project-device-schema.test.ts index 7ee2f89..257170a 100644 --- a/tests/project-device-schema.test.ts +++ b/tests/project-device-schema.test.ts @@ -14,10 +14,9 @@ describe("project device circuit-first schema", () => { const result = createProjectDeviceSchema.safeParse({ name: "E-Line Pro", displayName: "Office lighting", - phaseType: "single_phase", connectionKind: "fixed", costGroup: "440", - category: "Lighting", + category: "lighting", quantity: 6, powerPerUnit: 0.04, simultaneityFactor: 0.8, @@ -28,7 +27,7 @@ describe("project device circuit-first schema", () => { assert.equal(result.success, true); }); - it("requires the circuit-first power and phase fields", () => { + it("requires the circuit-first power and category fields", () => { const result = createProjectDeviceSchema.safeParse({ name: "Test device", displayName: "Test device", @@ -45,7 +44,7 @@ describe("project device circuit-first schema", () => { const result = createProjectDeviceSchema.safeParse({ name: "Pumpe", displayName: "Pumpe", - phaseType: "three_phase", + category: "three_phase", quantity: 1, powerPerUnit: 2, simultaneityFactor: 1, @@ -59,7 +58,7 @@ describe("project device circuit-first schema", () => { const device = { name: "E-Line Pro", displayName: "Bürobeleuchtung", - phaseType: "single_phase" as const, + category: "lighting" as const, quantity: 6, powerPerUnit: 0.04, simultaneityFactor: 0.8, diff --git a/tests/project-version-history.test.ts b/tests/project-version-history.test.ts index 300b972..bd6c615 100644 --- a/tests/project-version-history.test.ts +++ b/tests/project-version-history.test.ts @@ -283,7 +283,6 @@ describe("project device modal presentation", () => { "Kategorie", "Anschlussart", "Kostengruppe", - "Phasenart", "Anzahl", "Leistung je Stück [kW]", "Gleichzeitigkeitsfaktor", @@ -292,6 +291,10 @@ describe("project device modal presentation", () => { assert.match(markup, new RegExp(label.replace("[", "\\[").replace("]", "\\]"))); } assert.doesNotMatch(markup, /Spannung \[V\]/); + assert.doesNotMatch(markup, /Phasenart/); + assert.match(markup, /Beleuchtung/); + assert.match(markup, /1-phasig/); + assert.match(markup, /3-phasig/); }); }); From 1eed19ef6bcfc03470815336c38002e3492c0c57 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Fri, 31 Jul 2026 14:46:06 +0200 Subject: [PATCH 24/59] Add editable project floors and rooms --- AGENTS.md | 12 +- docs/circuit-list-editor-api.md | 19 +- docs/current-architecture.md | 42 ++-- src/app/projects/[projectId]/page.tsx | 196 +++++++++++++++--- src/db/repositories/floor.repository.ts | 10 +- ...on-structure-project-command.repository.ts | 82 +++++++- src/db/repositories/room.repository.ts | 10 +- ...ocation-structure-project-command.model.ts | 111 +++++++++- .../services/project-command.service.ts | 12 ++ src/frontend/utils/api.ts | 38 ++++ src/server/controllers/floor.controller.ts | 61 +++++- src/server/controllers/room.controller.ts | 66 +++++- src/server/routes/project.routes.ts | 8 +- .../validation/project-structure.schemas.ts | 17 ++ ...ructure-project-command.repository.test.ts | 139 ++++++++++++- 15 files changed, 743 insertions(+), 80 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 18896c9..f3bdd53 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -245,7 +245,7 @@ Immutable revision metadata is available through the paginated stored command payloads. Named logical snapshots can be created and listed through project-scoped API endpoints. Their schema-versioned payload contains the complete supported -project runtime state and a SHA-256, excludes global/upgrade-only data and does +project runtime state and a SHA-256, excludes global data outside the project and does not change the project revision or undo/redo stacks. Restoring a server-stored snapshot verifies its checksum and the current-state hash, replaces supported project data atomically and records a new `restore` revision with a complete @@ -335,15 +335,15 @@ UUID in foreign-key-safe order. The circuit-tree read model exposes fixed `headerComponents`, group-owned `components`, `footerComponents`, optional group category/number and optional one-to-one protection DTOs for circuits and group components. These are -additive to the retained flat circuit protection fields during the transition. +the only supported circuit-protection source; no flat compatibility fields remain. Distribution-board floor assignment and a project-enabled supply type use `distribution-board.update`; both values are snapshot/export fields and one persistent undo step. -Floor and room setup use `project-floor.insert` and `project-room.insert` with -complete stable snapshots. Their inverses remove only unchanged records and +Floor and room setup uses persistent insert, update and delete commands with +complete stable snapshots. Their inverses remove or restore only unchanged records and reject floors with assigned rooms/distribution boards or rooms referenced by -device rows or retained upgrade data. Both POST routes require `expectedRevision` and return -the updated history state. +device rows. All write routes require `expectedRevision` and return the updated +history state. Required operations: diff --git a/docs/circuit-list-editor-api.md b/docs/circuit-list-editor-api.md index 7e3d2d3..8f225a0 100644 --- a/docs/circuit-list-editor-api.md +++ b/docs/circuit-list-editor-api.md @@ -253,16 +253,27 @@ returns HTTP `409` with `PROJECT_HISTORY_OPERATION_UNAVAILABLE`. - body: `{ "name": "EG", "expectedRevision": 13 }` - executes `project-floor.insert` with a stable floor id - response: `{ "floor": { ... }, "revision": { ... }, "history": { ... } }` +- `PUT /projects/:projectId/floors/:floorId` + - body: `{ "name": "1. OG", "expectedRevision": 14 }` + - executes an exact persistent floor update +- `DELETE /projects/:projectId/floors/:floorId` + - body: `{ "expectedRevision": 15 }` + - rejects floors with assigned rooms or distribution boards - `POST /projects/:projectId/rooms` - body: `{ "floorId": "floor_1", "roomNumber": "001", "roomName": "Technik", "expectedRevision": 14 }` - executes `project-room.insert` with a stable room id; `floorId` is optional and must belong to the project when present - response: `{ "room": { ... }, "revision": { ... }, "history": { ... } }` -- Persistent Undo removes only unchanged, unreferenced records. A floor with - assigned rooms or distribution boards and a room referenced by device rows - or retained upgrade data are rejected instead of silently clearing foreign - keys. +- `PUT /projects/:projectId/rooms/:roomId` + - body: + `{ "floorId": null, "roomNumber": "101", "roomName": "Büro", "expectedRevision": 16 }` + - updates number, name and optional floor as one persistent command +- `DELETE /projects/:projectId/rooms/:roomId` + - body: `{ "expectedRevision": 17 }` + - rejects rooms referenced by device rows +- Persistent Undo/Redo restores exact previous and target records without + silently clearing foreign keys. - Stale revisions return `409 PROJECT_REVISION_CONFLICT`. ### Tree Endpoint diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 076178f..dc8a3f8 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -258,8 +258,8 @@ läuft über `project.restore-state` und ist dadurch eine atomare, dauerhaft rückgängig machbare Projektrevision. Der Modus `duplicate` ordnet Projekt-, Struktur-, Gruppen-, Verteilerkomponenten-, Raum-, Stromkreis- und Gerätezeilen-UUIDs sowie Schutzgeräte-Referenzen vollständig neu zu und legt -die Kopie mit Revision `0` in einer Transaktion an. Upgrade-only- -Fachliche Verknüpfungen innerhalb des unterstützten Laufzeitmodells bleiben +die Kopie mit Revision `0` in einer Transaktion an. Fachliche Verknüpfungen +innerhalb des unterstützten Laufzeitmodells bleiben erhalten. Die Projektübersicht verwendet dafür den separaten Collection-Endpunkt `POST /api/projects/import`, der ausschließlich eine neue Kopie anlegt und @@ -414,34 +414,26 @@ wieder her. verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und ist für bestehende sowie neu angelegte Verteilungen standardmäßig `1`. -Snapshot-Schema 6 und der portable Projekttransfer enthalten diesen Wert; -Schema 5 und älter werden mit dem neutralen Faktor `1` hochgestuft. -Snapshot-Schema 4 enthält bereits Etage und Netzart; Schema 1/2 sowie -gespeicherte Version-1-Strukturcommands werden ohne erfundene Zuordnung -hochgestuft. Snapshot-Schema 3 wird mit allen sechs Netzarten als -Projektauswahl hochgestuft. -Snapshot-Schema 7 ergänzt Gruppenkategorie und -nummer, -Verteilerkomponenten sowie die getrennten 1:1-Schutzgerätedaten. Capture, +Das aktuelle Baseline-Snapshot-Schema enthält den Gleichzeitigkeitsfaktor, +Gruppenkategorie und -nummer, Verteilerkomponenten sowie die getrennten +1:1-Schutzgerätedaten. Capture, benannte und automatische Snapshots, Wiederherstellung, Undo/Redo und beide -JSON-Importmodi verwenden denselben vollständigen Zustand. Schema 6 und älter -bleiben lesbar: Die drei bekannten Abschnittsschlüssel werden deterministisch -Gruppe 1 zugeordnet, neue Komponenten- und Schutzgerätesammlungen bleiben leer -und bestehende flache Schutzangaben unverändert erhalten. -`project-floor.insert` und `project-room.insert` versionieren die Anlage von -Geschossen und Räumen mit stabilen UUIDs. Die vollständigen Datensätze bilden -jeweils die persistierte Inverse für Undo/Redo. Ein Geschoss wird durch Undo nur +JSON-Importmodi verwenden denselben vollständigen Zustand. Vorherige +Entwicklungsschemas werden nicht mehr eingelesen. +Persistente Insert-, Update- und Delete-Commands versionieren Anlage, +Bearbeitung und Löschung von Geschossen und Räumen mit stabilen UUIDs und +exakten Vorher-/Nachher-Snapshots. Ein Geschoss wird nur entfernt, solange ihm weder ein Raum noch eine Verteilung zugeordnet wurde. Ein -Raum wird nur entfernt, -solange weder eine CircuitDeviceRow noch ein aufbewahrter Upgrade-Datensatz auf -ihn verweist. Beide POST-Endpunkte verlangen `expectedRevision`, liefern den -aktualisierten Historienstand und besitzen keinen direkten Create-Schreibweg -mehr. +Raum wird nur entfernt, solange keine CircuitDeviceRow auf ihn verweist. Alle +Schreibendpunkte verlangen `expectedRevision` und liefern den aktualisierten +Historienstand. ## Projektgeräte -`ProjectDevice` verwendet ausschließlich die kanonischen Circuit-First-Felder: -`phaseType`, `powerPerUnit`, `simultaneityFactor`, `cosPhi`, `remark` sowie -optionale technische und kategorisierende Felder. +Die Projektgerätekategorie `lighting`, `single_phase` oder `three_phase` ist die +fachliche Klassifikation und Zielgruppe. `phaseType` ist keine separate +Benutzereingabe, sondern wird daraus für Spannung, Verknüpfung und spätere +Dimensionierung abgeleitet. Beim Einfügen entsteht eine verknüpfte `CircuitDeviceRow`. Der Anzeigename wird kopiert, aber nicht still synchronisiert. Spätere Änderungen am Projektgerät diff --git a/src/app/projects/[projectId]/page.tsx b/src/app/projects/[projectId]/page.tsx index a4933fd..185feb4 100644 --- a/src/app/projects/[projectId]/page.tsx +++ b/src/app/projects/[projectId]/page.tsx @@ -11,7 +11,9 @@ import { createFloor, createProjectDevice, createRoom, + deleteFloor, deleteProjectDevice, + deleteRoom, deleteDistributionBoard, disconnectProjectDeviceRows, exportProjectTransfer, @@ -26,8 +28,10 @@ import { importProjectTransfer, synchronizeProjectDeviceRows, updateDistributionBoard, + updateFloor, updateProjectDevice, updateProjectSettings, + updateRoom, } from "../../../frontend/utils/api"; import type { CircuitListDto, @@ -95,9 +99,11 @@ export default function ProjectDetailPage() { ] = useState("1"); const [editingBoardCopyName, setEditingBoardCopyName] = useState(""); const [floorName, setFloorName] = useState(""); + const [editingFloor, setEditingFloor] = useState(null); const [roomNumber, setRoomNumber] = useState(""); const [roomName, setRoomName] = useState(""); const [roomFloorId, setRoomFloorId] = useState(""); + const [editingRoom, setEditingRoom] = useState(null); const [isProjectSettingsOpen, setIsProjectSettingsOpen] = useState(false); const [structureModal, setStructureModal] = useState< "board" | "floor" | "room" | null @@ -239,7 +245,7 @@ export default function ProjectDetailPage() { } } - async function handleCreateFloor(event: FormEvent) { + async function handleSaveFloor(event: FormEvent) { event.preventDefault(); if (!projectId || !project || !floorName.trim()) { return; @@ -247,23 +253,37 @@ export default function ProjectDetailPage() { setIsSaving(true); setError(null); try { - const result = await createFloor( - projectId, - { name: floorName.trim() }, - project.currentRevision + const result = editingFloor + ? await updateFloor( + projectId, + editingFloor.id, + { name: floorName.trim() }, + project.currentRevision + ) + : await createFloor( + projectId, + { name: floorName.trim() }, + project.currentRevision + ); + setFloors((current) => + editingFloor + ? current.map((floor) => + floor.id === result.floor.id ? result.floor : floor + ) + : [...current, result.floor] ); - setFloors((current) => [...current, result.floor]); applyProjectRevision(result.history.currentRevision); setFloorName(""); + setEditingFloor(null); setStructureModal(null); } catch (err) { - setError(err instanceof Error ? err.message : "Etage konnte nicht erstellt werden."); + setError(err instanceof Error ? err.message : "Etage konnte nicht gespeichert werden."); } finally { setIsSaving(false); } } - async function handleCreateRoom(event: FormEvent) { + async function handleSaveRoom(event: FormEvent) { event.preventDefault(); if ( !projectId || @@ -276,23 +296,34 @@ export default function ProjectDetailPage() { setIsSaving(true); setError(null); try { - const result = await createRoom( - projectId, - { - floorId: roomFloorId || undefined, - roomNumber: roomNumber.trim(), - roomName: roomName.trim(), - }, - project.currentRevision + const input = { + floorId: roomFloorId || undefined, + roomNumber: roomNumber.trim(), + roomName: roomName.trim(), + }; + const result = editingRoom + ? await updateRoom( + projectId, + editingRoom.id, + input, + project.currentRevision + ) + : await createRoom(projectId, input, project.currentRevision); + setRooms((current) => + editingRoom + ? current.map((room) => + room.id === result.room.id ? result.room : room + ) + : [...current, result.room] ); - setRooms((current) => [...current, result.room]); applyProjectRevision(result.history.currentRevision); setRoomNumber(""); setRoomName(""); setRoomFloorId(""); + setEditingRoom(null); setStructureModal(null); } catch (err) { - setError(err instanceof Error ? err.message : "Raum konnte nicht erstellt werden."); + setError(err instanceof Error ? err.message : "Raum konnte nicht gespeichert werden."); } finally { setIsSaving(false); } @@ -467,6 +498,74 @@ export default function ProjectDetailPage() { } } + function openCreateFloor() { + setEditingFloor(null); + setFloorName(""); + setStructureModal("floor"); + } + + function openEditFloor(floor: FloorDto) { + setEditingFloor(floor); + setFloorName(floor.name); + setStructureModal("floor"); + } + + function openCreateRoom() { + setEditingRoom(null); + setRoomNumber(""); + setRoomName(""); + setRoomFloorId(""); + setStructureModal("room"); + } + + function openEditRoom(room: RoomDto) { + setEditingRoom(room); + setRoomNumber(room.roomNumber); + setRoomName(room.roomName); + setRoomFloorId(room.floorId ?? ""); + setStructureModal("room"); + } + + async function handleDeleteFloor() { + if (!projectId || !project || !editingFloor) return; + if (!window.confirm(`Etage „${editingFloor.name}“ wirklich löschen?`)) return; + setIsSaving(true); + setError(null); + try { + const result = await deleteFloor(projectId, editingFloor.id, project.currentRevision); + setFloors((current) => current.filter((floor) => floor.id !== editingFloor.id)); + applyProjectRevision(result.history.currentRevision); + setEditingFloor(null); + setFloorName(""); + setStructureModal(null); + } catch (err) { + setError(err instanceof Error ? err.message : "Etage konnte nicht gelöscht werden."); + } finally { + setIsSaving(false); + } + } + + async function handleDeleteRoom() { + if (!projectId || !project || !editingRoom) return; + if (!window.confirm(`Raum „${editingRoom.roomNumber} ${editingRoom.roomName}“ wirklich löschen?`)) return; + setIsSaving(true); + setError(null); + try { + const result = await deleteRoom(projectId, editingRoom.id, project.currentRevision); + setRooms((current) => current.filter((room) => room.id !== editingRoom.id)); + applyProjectRevision(result.history.currentRevision); + setEditingRoom(null); + setRoomNumber(""); + setRoomName(""); + setRoomFloorId(""); + setStructureModal(null); + } catch (err) { + setError(err instanceof Error ? err.message : "Raum konnte nicht gelöscht werden."); + } finally { + setIsSaving(false); + } + } + async function handleCopyBoard() { if ( !projectId || @@ -907,7 +1006,7 @@ export default function ProjectDetailPage() {
    {floors.map((floor) => ( -
  • - {floor.name} +
  • + {floor.name} +
  • ))} {!floors.length ? ( @@ -935,7 +1037,7 @@ export default function ProjectDetailPage() {
+ ))} {!rooms.length ? ( - + Noch keine Räume vorhanden. @@ -1432,11 +1540,14 @@ export default function ProjectDetailPage() { {structureModal === "floor" ? ( setStructureModal(null)} - onSubmit={handleCreateFloor} + onClose={() => { + setStructureModal(null); + setEditingFloor(null); + }} + onSubmit={handleSaveFloor} submitDisabled={!floorName.trim()} - submitLabel="Etage hinzufügen" - title="Etage hinzufügen" + submitLabel={editingFloor ? "Änderungen speichern" : "Etage hinzufügen"} + title={editingFloor ? "Etage bearbeiten" : "Etage hinzufügen"} > ) : null} {structureModal === "room" ? ( setStructureModal(null)} - onSubmit={handleCreateRoom} + onClose={() => { + setStructureModal(null); + setEditingRoom(null); + }} + onSubmit={handleSaveRoom} submitDisabled={!roomNumber.trim() || !roomName.trim()} - submitLabel="Raum anlegen" - title="Raum hinzufügen" + submitLabel={editingRoom ? "Änderungen speichern" : "Raum anlegen"} + title={editingRoom ? "Raum bearbeiten" : "Raum hinzufügen"} >
@@ -1507,6 +1631,16 @@ export default function ProjectDetailPage() {
+ {editingRoom ? ( +
+

+ In Stromkreisen verwendete Räume können nicht gelöscht werden. +

+ +
+ ) : null}
) : null} diff --git a/src/db/repositories/floor.repository.ts b/src/db/repositories/floor.repository.ts index caaabd3..a0c9645 100644 --- a/src/db/repositories/floor.repository.ts +++ b/src/db/repositories/floor.repository.ts @@ -1,4 +1,4 @@ -import { asc, eq } from "drizzle-orm"; +import { and, asc, eq } from "drizzle-orm"; import type { AppDatabase } from "../database-context.js"; import { floors } from "../schema/floors.js"; @@ -13,4 +13,12 @@ export class FloorRepository { .where(eq(floors.projectId, projectId)) .orderBy(asc(floors.sortOrder), asc(floors.name)); } + + async findById(projectId: string, floorId: string) { + return this.database + .select() + .from(floors) + .where(and(eq(floors.projectId, projectId), eq(floors.id, floorId))) + .get() ?? null; + } } diff --git a/src/db/repositories/project-location-structure-project-command.repository.ts b/src/db/repositories/project-location-structure-project-command.repository.ts index 2a3466a..7cbadde 100644 --- a/src/db/repositories/project-location-structure-project-command.repository.ts +++ b/src/db/repositories/project-location-structure-project-command.repository.ts @@ -2,16 +2,22 @@ import { and, eq } from "drizzle-orm"; import { assertProjectFloorDeleteProjectCommand, assertProjectFloorInsertProjectCommand, + assertProjectFloorUpdateProjectCommand, assertProjectRoomDeleteProjectCommand, assertProjectRoomInsertProjectCommand, + assertProjectRoomUpdateProjectCommand, createProjectFloorDeleteProjectCommand, createProjectFloorInsertProjectCommand, + createProjectFloorUpdateProjectCommand, createProjectRoomDeleteProjectCommand, createProjectRoomInsertProjectCommand, + createProjectRoomUpdateProjectCommand, projectFloorDeleteCommandType, projectFloorInsertCommandType, + projectFloorUpdateCommandType, projectRoomDeleteCommandType, projectRoomInsertCommandType, + projectRoomUpdateCommandType, type ProjectFloorSnapshot, type ProjectLocationStructureProjectCommand, type ProjectRoomSnapshot, @@ -62,6 +68,9 @@ export class ProjectLocationStructureProjectCommandRepository projectId, command.payload.floor ); + case projectFloorUpdateCommandType: + assertProjectFloorUpdateProjectCommand(command); + return this.updateFloor(database, projectId, command.payload.expected, command.payload.target); case projectRoomInsertCommandType: assertProjectRoomInsertProjectCommand(command); return this.insertRoom( @@ -76,6 +85,9 @@ export class ProjectLocationStructureProjectCommandRepository projectId, command.payload.room ); + case projectRoomUpdateCommandType: + assertProjectRoomUpdateProjectCommand(command); + return this.updateRoom(database, projectId, command.payload.expected, command.payload.target); } } @@ -126,7 +138,7 @@ export class ProjectLocationStructureProjectCommandRepository .get(); if (referencedRoom) { throw new Error( - "A project floor with assigned rooms cannot be removed by history." + "Die Etage kann nicht gelöscht werden, solange Räume zugeordnet sind." ); } const referencedDistributionBoard = database @@ -137,7 +149,7 @@ export class ProjectLocationStructureProjectCommandRepository .get(); if (referencedDistributionBoard) { throw new Error( - "A project floor assigned to a distribution board cannot be removed by history." + "Die Etage kann nicht gelöscht werden, solange eine Verteilung zugeordnet ist." ); } const deleted = database @@ -191,6 +203,70 @@ export class ProjectLocationStructureProjectCommandRepository return createProjectRoomDeleteProjectCommand(room); } + private updateFloor( + database: AppDatabase, + projectId: string, + expected: ProjectFloorSnapshot, + target: ProjectFloorSnapshot + ) { + const persisted = database + .select() + .from(floors) + .where(and(eq(floors.id, expected.id), eq(floors.projectId, projectId))) + .get(); + if (!persisted || !sameRecord(expected, persisted)) { + throw new Error("Project floor changed before update."); + } + const updated = database + .update(floors) + .set({ name: target.name }) + .where(and(eq(floors.id, expected.id), eq(floors.projectId, projectId))) + .run(); + if (updated.changes !== 1) { + throw new Error("Die Etage konnte nicht aktualisiert werden."); + } + return createProjectFloorUpdateProjectCommand(target, expected); + } + + private updateRoom( + database: AppDatabase, + projectId: string, + expected: ProjectRoomSnapshot, + target: ProjectRoomSnapshot + ) { + const persisted = database + .select() + .from(rooms) + .where(and(eq(rooms.id, expected.id), eq(rooms.projectId, projectId))) + .get(); + if (!persisted || !sameRecord(expected, persisted)) { + throw new Error("Project room changed before update."); + } + if (target.floorId) { + const targetFloor = database + .select({ id: floors.id }) + .from(floors) + .where(and(eq(floors.id, target.floorId), eq(floors.projectId, projectId))) + .get(); + if (!targetFloor) { + throw new Error("Project room references a foreign floor."); + } + } + const updated = database + .update(rooms) + .set({ + floorId: target.floorId, + roomNumber: target.roomNumber, + roomName: target.roomName, + }) + .where(and(eq(rooms.id, expected.id), eq(rooms.projectId, projectId))) + .run(); + if (updated.changes !== 1) { + throw new Error("Der Raum konnte nicht aktualisiert werden."); + } + return createProjectRoomUpdateProjectCommand(target, expected); + } + private deleteRoom( database: AppDatabase, projectId: string, @@ -217,7 +293,7 @@ export class ProjectLocationStructureProjectCommandRepository .get(); if (referencedDeviceRow) { throw new Error( - "A referenced project room cannot be removed by history." + "Der Raum kann nicht gelöscht werden, solange er in einem Stromkreis verwendet wird." ); } const deleted = database diff --git a/src/db/repositories/room.repository.ts b/src/db/repositories/room.repository.ts index 85e21cc..2a291f3 100644 --- a/src/db/repositories/room.repository.ts +++ b/src/db/repositories/room.repository.ts @@ -1,4 +1,4 @@ -import { asc, eq } from "drizzle-orm"; +import { and, asc, eq } from "drizzle-orm"; import type { AppDatabase } from "../database-context.js"; import { rooms } from "../schema/rooms.js"; @@ -13,4 +13,12 @@ export class RoomRepository { .where(eq(rooms.projectId, projectId)) .orderBy(asc(rooms.roomNumber), asc(rooms.roomName)); } + + async findById(projectId: string, roomId: string) { + return this.database + .select() + .from(rooms) + .where(and(eq(rooms.projectId, projectId), eq(rooms.id, roomId))) + .get() ?? null; + } } diff --git a/src/domain/models/project-location-structure-project-command.model.ts b/src/domain/models/project-location-structure-project-command.model.ts index 32f1a0a..616add9 100644 --- a/src/domain/models/project-location-structure-project-command.model.ts +++ b/src/domain/models/project-location-structure-project-command.model.ts @@ -3,8 +3,10 @@ import type { SerializedProjectCommand } from "./project-command.model.js"; export const projectFloorInsertCommandType = "project-floor.insert" as const; export const projectFloorDeleteCommandType = "project-floor.delete" as const; +export const projectFloorUpdateCommandType = "project-floor.update" as const; export const projectRoomInsertCommandType = "project-room.insert" as const; export const projectRoomDeleteCommandType = "project-room.delete" as const; +export const projectRoomUpdateCommandType = "project-room.update" as const; export const projectLocationStructureCommandSchemaVersion = 1 as const; export interface ProjectFloorSnapshot { @@ -30,6 +32,16 @@ interface ProjectRoomStructureCommandPayload { room: ProjectRoomSnapshot; } +interface ProjectFloorUpdateCommandPayload { + expected: ProjectFloorSnapshot; + target: ProjectFloorSnapshot; +} + +interface ProjectRoomUpdateCommandPayload { + expected: ProjectRoomSnapshot; + target: ProjectRoomSnapshot; +} + export interface ProjectFloorInsertProjectCommand extends SerializedProjectCommand { schemaVersion: typeof projectLocationStructureCommandSchemaVersion; @@ -42,6 +54,12 @@ export interface ProjectFloorDeleteProjectCommand type: typeof projectFloorDeleteCommandType; } +export interface ProjectFloorUpdateProjectCommand + extends SerializedProjectCommand { + schemaVersion: typeof projectLocationStructureCommandSchemaVersion; + type: typeof projectFloorUpdateCommandType; +} + export interface ProjectRoomInsertProjectCommand extends SerializedProjectCommand { schemaVersion: typeof projectLocationStructureCommandSchemaVersion; @@ -54,11 +72,19 @@ export interface ProjectRoomDeleteProjectCommand type: typeof projectRoomDeleteCommandType; } +export interface ProjectRoomUpdateProjectCommand + extends SerializedProjectCommand { + schemaVersion: typeof projectLocationStructureCommandSchemaVersion; + type: typeof projectRoomUpdateCommandType; +} + export type ProjectLocationStructureProjectCommand = | ProjectFloorInsertProjectCommand | ProjectFloorDeleteProjectCommand + | ProjectFloorUpdateProjectCommand | ProjectRoomInsertProjectCommand - | ProjectRoomDeleteProjectCommand; + | ProjectRoomDeleteProjectCommand + | ProjectRoomUpdateProjectCommand; export function createProjectFloorSnapshot( projectId: string, @@ -138,6 +164,30 @@ export function createProjectRoomDeleteProjectCommand( }; } +export function createProjectFloorUpdateProjectCommand( + expected: ProjectFloorSnapshot, + target: ProjectFloorSnapshot +): ProjectFloorUpdateProjectCommand { + assertMatchingFloorUpdate(expected, target); + return { + schemaVersion: projectLocationStructureCommandSchemaVersion, + type: projectFloorUpdateCommandType, + payload: { expected, target }, + }; +} + +export function createProjectRoomUpdateProjectCommand( + expected: ProjectRoomSnapshot, + target: ProjectRoomSnapshot +): ProjectRoomUpdateProjectCommand { + assertMatchingRoomUpdate(expected, target); + return { + schemaVersion: projectLocationStructureCommandSchemaVersion, + type: projectRoomUpdateCommandType, + payload: { expected, target }, + }; +} + export function assertProjectFloorInsertProjectCommand( command: SerializedProjectCommand ): asserts command is ProjectFloorInsertProjectCommand { @@ -174,6 +224,59 @@ export function assertProjectRoomDeleteProjectCommand( ); } +export function assertProjectFloorUpdateProjectCommand( + command: SerializedProjectCommand +): asserts command is ProjectFloorUpdateProjectCommand { + if ( + command.schemaVersion !== projectLocationStructureCommandSchemaVersion || + command.type !== projectFloorUpdateCommandType || + !isPlainObject(command.payload) || + Object.keys(command.payload).length !== 2 + ) { + throw new Error("Unsupported project-floor update command."); + } + assertMatchingFloorUpdate(command.payload.expected, command.payload.target); +} + +export function assertProjectRoomUpdateProjectCommand( + command: SerializedProjectCommand +): asserts command is ProjectRoomUpdateProjectCommand { + if ( + command.schemaVersion !== projectLocationStructureCommandSchemaVersion || + command.type !== projectRoomUpdateCommandType || + !isPlainObject(command.payload) || + Object.keys(command.payload).length !== 2 + ) { + throw new Error("Unsupported project-room update command."); + } + assertMatchingRoomUpdate(command.payload.expected, command.payload.target); +} + +function assertMatchingFloorUpdate(expected: unknown, target: unknown) { + assertProjectFloorSnapshot(expected); + assertProjectFloorSnapshot(target); + if ( + expected.id !== target.id || + expected.projectId !== target.projectId || + expected.sortOrder !== target.sortOrder || + expected.name === target.name + ) { + throw new Error("Project-floor update must change only the name."); + } +} + +function assertMatchingRoomUpdate(expected: unknown, target: unknown) { + assertProjectRoomSnapshot(expected); + assertProjectRoomSnapshot(target); + if ( + expected.id !== target.id || + expected.projectId !== target.projectId || + sameSnapshot(expected, target) + ) { + throw new Error("Project-room update must change room values."); + } +} + export function assertProjectFloorSnapshot( floor: unknown ): asserts floor is ProjectFloorSnapshot { @@ -257,3 +360,9 @@ function assertNormalizedNonEmptyString( function isPlainObject(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } + +function sameSnapshot(left: object, right: object) { + return Object.entries(left).every( + ([key, value]) => (right as Record)[key] === value + ); +} diff --git a/src/domain/services/project-command.service.ts b/src/domain/services/project-command.service.ts index 2f2ba16..65e8059 100644 --- a/src/domain/services/project-command.service.ts +++ b/src/domain/services/project-command.service.ts @@ -72,12 +72,16 @@ import { import { assertProjectFloorDeleteProjectCommand, assertProjectFloorInsertProjectCommand, + assertProjectFloorUpdateProjectCommand, assertProjectRoomDeleteProjectCommand, assertProjectRoomInsertProjectCommand, + assertProjectRoomUpdateProjectCommand, projectFloorDeleteCommandType, projectFloorInsertCommandType, + projectFloorUpdateCommandType, projectRoomDeleteCommandType, projectRoomInsertCommandType, + projectRoomUpdateCommandType, } from "../models/project-location-structure-project-command.model.js"; import { assertProjectStateRestoreCommand, @@ -470,6 +474,10 @@ export class ProjectCommandService implements ProjectCommandExecutor { command: input.command, }).revision; } + case projectFloorUpdateCommandType: { + assertProjectFloorUpdateProjectCommand(input.command); + return this.projectLocationStructureStore.execute({ ...input, command: input.command }).revision; + } case projectRoomInsertCommandType: { assertProjectRoomInsertProjectCommand(input.command); return this.projectLocationStructureStore.execute({ @@ -484,6 +492,10 @@ export class ProjectCommandService implements ProjectCommandExecutor { command: input.command, }).revision; } + case projectRoomUpdateCommandType: { + assertProjectRoomUpdateProjectCommand(input.command); + return this.projectLocationStructureStore.execute({ ...input, command: input.command }).revision; + } case circuitSectionReorderCommandType: { assertCircuitSectionReorderProjectCommand(input.command); return this.circuitSectionReorderStore.execute({ diff --git a/src/frontend/utils/api.ts b/src/frontend/utils/api.ts index 5b6b624..460725d 100644 --- a/src/frontend/utils/api.ts +++ b/src/frontend/utils/api.ts @@ -786,6 +786,25 @@ export function createFloor( ); } +export function updateFloor( + projectId: string, + floorId: string, + input: CreateFloorInput, + expectedRevision: number +) { + return request(`/api/projects/${projectId}/floors/${floorId}`, { + method: "PUT", + body: JSON.stringify({ ...input, expectedRevision }), + }); +} + +export function deleteFloor(projectId: string, floorId: string, expectedRevision: number) { + return request(`/api/projects/${projectId}/floors/${floorId}`, { + method: "DELETE", + body: JSON.stringify({ expectedRevision }), + }); +} + export function listRooms(projectId: string) { return request(`/api/projects/${projectId}/rooms`); } @@ -804,6 +823,25 @@ export function createRoom( ); } +export function updateRoom( + projectId: string, + roomId: string, + input: CreateRoomInput, + expectedRevision: number +) { + return request(`/api/projects/${projectId}/rooms/${roomId}`, { + method: "PUT", + body: JSON.stringify({ ...input, floorId: input.floorId ?? null, expectedRevision }), + }); +} + +export function deleteRoom(projectId: string, roomId: string, expectedRevision: number) { + return request(`/api/projects/${projectId}/rooms/${roomId}`, { + method: "DELETE", + body: JSON.stringify({ expectedRevision }), + }); +} + export function listGlobalDevices() { return request("/api/global-devices"); } diff --git a/src/server/controllers/floor.controller.ts b/src/server/controllers/floor.controller.ts index 677e728..085aa10 100644 --- a/src/server/controllers/floor.controller.ts +++ b/src/server/controllers/floor.controller.ts @@ -1,9 +1,15 @@ import type { Request, Response } from "express"; import { + createProjectFloorDeleteProjectCommand, createProjectFloorInsertProjectCommand, createProjectFloorSnapshot, + createProjectFloorUpdateProjectCommand, } from "../../domain/models/project-location-structure-project-command.model.js"; -import { createFloorSchema } from "../../shared/validation/project-structure.schemas.js"; +import { + createFloorSchema, + deleteProjectLocationSchema, + updateFloorSchema, +} from "../../shared/validation/project-structure.schemas.js"; import { projectCommandService } from "../composition/project-command-stores.js"; import { floorRepository } from "../composition/application-repositories.js"; import { respondWithProjectCommandError } from "./project-command.controller.js"; @@ -50,3 +56,56 @@ export async function createFloor(req: Request, res: Response) { return respondWithProjectCommandError(error, res); } } + +export async function updateFloor(req: Request, res: Response) { + const { projectId, floorId } = req.params; + if (typeof projectId !== "string" || typeof floorId !== "string") { + return res.status(400).json({ error: "Invalid parameters" }); + } + const parsed = updateFloorSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ error: parsed.error.flatten() }); + } + const current = await floorRepository.findById(projectId, floorId); + if (!current) { + return res.status(404).json({ error: "Floor not found" }); + } + const target = { ...current, name: parsed.data.name }; + try { + const result = projectCommandService.executeUser({ + projectId, + expectedRevision: parsed.data.expectedRevision, + description: "Geschoss bearbeiten", + command: createProjectFloorUpdateProjectCommand(current, target), + }); + return res.json({ ...result, floor: target }); + } catch (error) { + return respondWithProjectCommandError(error, res); + } +} + +export async function deleteFloor(req: Request, res: Response) { + const { projectId, floorId } = req.params; + if (typeof projectId !== "string" || typeof floorId !== "string") { + return res.status(400).json({ error: "Invalid parameters" }); + } + const parsed = deleteProjectLocationSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ error: parsed.error.flatten() }); + } + const current = await floorRepository.findById(projectId, floorId); + if (!current) { + return res.status(404).json({ error: "Floor not found" }); + } + try { + const result = projectCommandService.executeUser({ + projectId, + expectedRevision: parsed.data.expectedRevision, + description: "Geschoss löschen", + command: createProjectFloorDeleteProjectCommand(current), + }); + return res.json(result); + } catch (error) { + return respondWithProjectCommandError(error, res); + } +} diff --git a/src/server/controllers/room.controller.ts b/src/server/controllers/room.controller.ts index 330a538..3e78628 100644 --- a/src/server/controllers/room.controller.ts +++ b/src/server/controllers/room.controller.ts @@ -1,9 +1,15 @@ import type { Request, Response } from "express"; import { + createProjectRoomDeleteProjectCommand, createProjectRoomInsertProjectCommand, createProjectRoomSnapshot, + createProjectRoomUpdateProjectCommand, } from "../../domain/models/project-location-structure-project-command.model.js"; -import { createRoomSchema } from "../../shared/validation/project-structure.schemas.js"; +import { + createRoomSchema, + deleteProjectLocationSchema, + updateRoomSchema, +} from "../../shared/validation/project-structure.schemas.js"; import { projectCommandService } from "../composition/project-command-stores.js"; import { roomRepository } from "../composition/application-repositories.js"; import { respondWithProjectCommandError } from "./project-command.controller.js"; @@ -42,3 +48,61 @@ export async function createRoom(req: Request, res: Response) { return respondWithProjectCommandError(error, res); } } + +export async function updateRoom(req: Request, res: Response) { + const { projectId, roomId } = req.params; + if (typeof projectId !== "string" || typeof roomId !== "string") { + return res.status(400).json({ error: "Invalid parameters" }); + } + const parsed = updateRoomSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ error: parsed.error.flatten() }); + } + const current = await roomRepository.findById(projectId, roomId); + if (!current) { + return res.status(404).json({ error: "Room not found" }); + } + const target = { + ...current, + floorId: parsed.data.floorId, + roomNumber: parsed.data.roomNumber, + roomName: parsed.data.roomName, + }; + try { + const result = projectCommandService.executeUser({ + projectId, + expectedRevision: parsed.data.expectedRevision, + description: "Raum bearbeiten", + command: createProjectRoomUpdateProjectCommand(current, target), + }); + return res.json({ ...result, room: target }); + } catch (error) { + return respondWithProjectCommandError(error, res); + } +} + +export async function deleteRoom(req: Request, res: Response) { + const { projectId, roomId } = req.params; + if (typeof projectId !== "string" || typeof roomId !== "string") { + return res.status(400).json({ error: "Invalid parameters" }); + } + const parsed = deleteProjectLocationSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ error: parsed.error.flatten() }); + } + const current = await roomRepository.findById(projectId, roomId); + if (!current) { + return res.status(404).json({ error: "Room not found" }); + } + try { + const result = projectCommandService.executeUser({ + projectId, + expectedRevision: parsed.data.expectedRevision, + description: "Raum löschen", + command: createProjectRoomDeleteProjectCommand(current), + }); + return res.json(result); + } catch (error) { + return respondWithProjectCommandError(error, res); + } +} diff --git a/src/server/routes/project.routes.ts b/src/server/routes/project.routes.ts index 8bdf909..b0bd39f 100644 --- a/src/server/routes/project.routes.ts +++ b/src/server/routes/project.routes.ts @@ -13,8 +13,8 @@ import { updateDistributionBoard, } from "../controllers/distribution-board.controller.js"; import { listCircuitListsByProject } from "../controllers/circuit-list.controller.js"; -import { createFloor, listFloorsByProject } from "../controllers/floor.controller.js"; -import { createRoom, listRoomsByProject } from "../controllers/room.controller.js"; +import { createFloor, deleteFloor, listFloorsByProject, updateFloor } from "../controllers/floor.controller.js"; +import { createRoom, deleteRoom, listRoomsByProject, updateRoom } from "../controllers/room.controller.js"; import { getCircuitTree } from "../controllers/circuit-tree.controller.js"; import { getProjectHistory, @@ -74,5 +74,9 @@ projectRouter.get("/:projectId/circuit-lists", listCircuitListsByProject); projectRouter.get("/:projectId/circuit-lists/:circuitListId/tree", getCircuitTree); projectRouter.get("/:projectId/floors", listFloorsByProject); projectRouter.post("/:projectId/floors", createFloor); +projectRouter.put("/:projectId/floors/:floorId", updateFloor); +projectRouter.delete("/:projectId/floors/:floorId", deleteFloor); projectRouter.get("/:projectId/rooms", listRoomsByProject); projectRouter.post("/:projectId/rooms", createRoom); +projectRouter.put("/:projectId/rooms/:roomId", updateRoom); +projectRouter.delete("/:projectId/rooms/:roomId", deleteRoom); diff --git a/src/shared/validation/project-structure.schemas.ts b/src/shared/validation/project-structure.schemas.ts index cb666e2..bf747da 100644 --- a/src/shared/validation/project-structure.schemas.ts +++ b/src/shared/validation/project-structure.schemas.ts @@ -69,6 +69,14 @@ export const createFloorSchema = z }) .strict(); +export const updateFloorSchema = createFloorSchema; + +export const deleteProjectLocationSchema = z + .object({ + expectedRevision: expectedProjectRevisionSchema, + }) + .strict(); + export const createRoomSchema = z .object({ expectedRevision: expectedProjectRevisionSchema, @@ -78,6 +86,15 @@ export const createRoomSchema = z }) .strict(); +export const updateRoomSchema = z + .object({ + expectedRevision: expectedProjectRevisionSchema, + floorId: z.string().trim().min(1).nullable(), + roomNumber: z.string().trim().min(1), + roomName: z.string().trim().min(1), + }) + .strict(); + export type CreateProjectInput = z.infer; export type UpdateProjectSettingsInput = z.infer< typeof updateProjectSettingsSchema diff --git a/tests/project-location-structure-project-command.repository.test.ts b/tests/project-location-structure-project-command.repository.test.ts index 19f278b..6378965 100644 --- a/tests/project-location-structure-project-command.repository.test.ts +++ b/tests/project-location-structure-project-command.repository.test.ts @@ -18,10 +18,19 @@ import { ProjectRevisionConflictError } from "../src/domain/errors/project-revis import { createProjectFloorInsertProjectCommand, createProjectFloorSnapshot, + createProjectFloorUpdateProjectCommand, createProjectRoomInsertProjectCommand, createProjectRoomSnapshot, + createProjectRoomUpdateProjectCommand, } from "../src/domain/models/project-location-structure-project-command.model.js"; -import { createFloor, createRoom } from "../src/frontend/utils/api.js"; +import { + createFloor, + createRoom, + deleteFloor, + deleteRoom, + updateFloor, + updateRoom, +} from "../src/frontend/utils/api.js"; import { createFloorSchema, createRoomSchema, @@ -83,11 +92,12 @@ describe("project-location structure project command", () => { false ); - const requests: Array<{ url: string; body: unknown }> = []; + const requests: Array<{ url: string; method: string; body: unknown }> = []; const originalFetch = globalThis.fetch; globalThis.fetch = async (input, init) => { requests.push({ url: String(input), + method: init?.method ?? "GET", body: JSON.parse(String(init?.body)), }); return new Response(JSON.stringify({}), { @@ -106,16 +116,27 @@ describe("project-location structure project command", () => { }, 8 ); + await updateFloor("project-1", "floor-1", { name: "1. OG" }, 9); + await updateRoom( + "project-1", + "room-1", + { roomNumber: "101", roomName: "Büro" }, + 10 + ); + await deleteFloor("project-1", "floor-1", 11); + await deleteRoom("project-1", "room-1", 12); } finally { globalThis.fetch = originalFetch; } assert.deepEqual(requests, [ { url: "/api/projects/project-1/floors", + method: "POST", body: { name: "EG", expectedRevision: 7 }, }, { url: "/api/projects/project-1/rooms", + method: "POST", body: { floorId: "floor-1", roomNumber: "001", @@ -123,9 +144,119 @@ describe("project-location structure project command", () => { expectedRevision: 8, }, }, + { + url: "/api/projects/project-1/floors/floor-1", + method: "PUT", + body: { name: "1. OG", expectedRevision: 9 }, + }, + { + url: "/api/projects/project-1/rooms/room-1", + method: "PUT", + body: { + roomNumber: "101", + roomName: "Büro", + floorId: null, + expectedRevision: 10, + }, + }, + { + url: "/api/projects/project-1/floors/floor-1", + method: "DELETE", + body: { expectedRevision: 11 }, + }, + { + url: "/api/projects/project-1/rooms/room-1", + method: "DELETE", + body: { expectedRevision: 12 }, + }, ]); }); + it("updates floors and rooms with exact persisted undo and redo", () => { + const context = createTestDatabase(); + try { + const repository = new ProjectLocationStructureProjectCommandRepository(context.db); + const history = new ProjectHistoryRepository(context.db); + const floor = createProjectFloorSnapshot("project-1", "EG", 0); + const room = createProjectRoomSnapshot("project-1", { + floorId: floor.id, + roomNumber: "001", + roomName: "Technik", + }); + context.db.insert(floors).values(floor).run(); + context.db.insert(rooms).values(room).run(); + + const updatedFloor = { ...floor, name: "Erdgeschoss" }; + const floorCommand = createProjectFloorUpdateProjectCommand(floor, updatedFloor); + const floorResult = repository.execute({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + command: floorCommand, + }); + const updatedRoom = { + ...room, + floorId: null, + roomNumber: "002", + roomName: "Lager", + }; + const roomCommand = createProjectRoomUpdateProjectCommand(room, updatedRoom); + const roomResult = repository.execute({ + projectId: "project-1", + expectedRevision: 1, + source: "user", + command: roomCommand, + }); + assert.equal(context.db.select().from(floors).where(eq(floors.id, floor.id)).get()?.name, "Erdgeschoss"); + assert.deepEqual(context.db.select().from(rooms).where(eq(rooms.id, room.id)).get(), updatedRoom); + + const undoRoom = history.getNextCommand("project-1", "undo"); + assert.ok(undoRoom); + repository.execute({ + projectId: "project-1", + expectedRevision: 2, + source: "undo", + historyTargetChangeSetId: undoRoom.changeSetId, + command: roomResult.inverse, + }); + assert.deepEqual(context.db.select().from(rooms).where(eq(rooms.id, room.id)).get(), room); + + const undoFloor = history.getNextCommand("project-1", "undo"); + assert.ok(undoFloor); + repository.execute({ + projectId: "project-1", + expectedRevision: 3, + source: "undo", + historyTargetChangeSetId: undoFloor.changeSetId, + command: floorResult.inverse, + }); + assert.deepEqual(context.db.select().from(floors).where(eq(floors.id, floor.id)).get(), floor); + + const redoFloor = history.getNextCommand("project-1", "redo"); + assert.ok(redoFloor); + repository.execute({ + projectId: "project-1", + expectedRevision: 4, + source: "redo", + historyTargetChangeSetId: redoFloor.changeSetId, + command: floorCommand, + }); + const redoRoom = history.getNextCommand("project-1", "redo"); + assert.ok(redoRoom); + repository.execute({ + projectId: "project-1", + expectedRevision: 5, + source: "redo", + historyTargetChangeSetId: redoRoom.changeSetId, + command: roomCommand, + }); + assert.deepEqual(context.db.select().from(floors).where(eq(floors.id, floor.id)).get(), updatedFloor); + assert.deepEqual(context.db.select().from(rooms).where(eq(rooms.id, room.id)).get(), updatedRoom); + } finally { + context.close(); + } + }); + it("creates a stable floor and supports persisted undo and redo", () => { const context = createTestDatabase(); try { @@ -218,7 +349,7 @@ describe("project-location structure project command", () => { historyTargetChangeSetId: undoStep.changeSetId, command: inserted.inverse, }), - /assigned to a distribution board/ + /Verteilung zugeordnet/ ); assert.equal( context.db.select().from(projectRevisions).all().length, @@ -416,7 +547,7 @@ describe("project-location structure project command", () => { historyTargetChangeSetId: undoStep.changeSetId, command: inserted.inverse, }), - /assigned rooms/ + /Räume zugeordnet/ ); } finally { floorContext.close(); From facf788ce81f289de51da7f7d522b9b29ed2695a Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Fri, 31 Jul 2026 14:52:37 +0200 Subject: [PATCH 25/59] Remove circuit structure preview --- docs/circuit-list-editor-architecture.md | 4 +- src/app/globals.css | 29 --- .../[circuitListId]/tree-edit/page.tsx | 6 - .../[circuitListId]/tree/page.tsx | 38 --- .../components/circuit-tree-preview.tsx | 219 ------------------ 5 files changed, 2 insertions(+), 294 deletions(-) delete mode 100644 src/app/projects/[projectId]/circuit-lists/[circuitListId]/tree/page.tsx delete mode 100644 src/frontend/components/circuit-tree-preview.tsx diff --git a/docs/circuit-list-editor-architecture.md b/docs/circuit-list-editor-architecture.md index c58589b..a8473c9 100644 --- a/docs/circuit-list-editor-architecture.md +++ b/docs/circuit-list-editor-architecture.md @@ -111,8 +111,8 @@ Primary circuit-first editor route: - `/projects/:projectId/circuit-lists/:circuitListId/tree-edit` -The sibling `/tree` route is a read-only structure preview. Old -`/projects/:projectId/circuit-lists` bookmarks redirect to the project page. +Old `/projects/:projectId/circuit-lists` bookmarks redirect to the project +page. A future print view will provide a dedicated read-only presentation. ## Future Persistence Direction diff --git a/src/app/globals.css b/src/app/globals.css index c863b70..27b2d63 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -305,35 +305,6 @@ a.kpi:hover { min-width: 8rem; } -.circuit-tree-table .section-row td { - background: #e9eef8; - border-top: 2px solid #c6d3ea; - font-weight: 600; -} - -.circuit-tree-table .summary-row td { - background: #f5f8fd; - font-weight: 600; -} - -.circuit-tree-table .device-row td { - background: #ffffff; -} - -.circuit-tree-table .reserve-row td { - background: #f8fbff; -} - -.circuit-tree-table .placeholder-row td { - background: #f7f7f7; - color: #6c757d; - font-style: italic; -} - -.circuit-tree-table .indented-cell { - padding-left: 1.5rem; -} - .tree-editor-shell { display: flex; flex-direction: column; diff --git a/src/app/projects/[projectId]/circuit-lists/[circuitListId]/tree-edit/page.tsx b/src/app/projects/[projectId]/circuit-lists/[circuitListId]/tree-edit/page.tsx index f661942..89679cf 100644 --- a/src/app/projects/[projectId]/circuit-lists/[circuitListId]/tree-edit/page.tsx +++ b/src/app/projects/[projectId]/circuit-lists/[circuitListId]/tree-edit/page.tsx @@ -15,12 +15,6 @@ export default function CircuitTreeEditPage() {

Stromkreise und zugeordnete Geräte bearbeiten.

- - Strukturvorschau - Zum Projekt diff --git a/src/app/projects/[projectId]/circuit-lists/[circuitListId]/tree/page.tsx b/src/app/projects/[projectId]/circuit-lists/[circuitListId]/tree/page.tsx deleted file mode 100644 index 8cfea27..0000000 --- a/src/app/projects/[projectId]/circuit-lists/[circuitListId]/tree/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { useParams } from "next/navigation"; -import { CircuitTreePreview } from "../../../../../../frontend/components/circuit-tree-preview"; - -export default function CircuitTreePreviewPage() { - const params = useParams<{ projectId: string; circuitListId: string }>(); - const projectId = params.projectId; - const circuitListId = params.circuitListId; - - return ( -
-
-
-

Vorschau der Stromkreisstruktur

-

- Schreibgeschützte Vorschau der Bereiche, Stromkreise und Gerätezeilen. -

-
- - Editor öffnen - - - Zum Projekt - -
- - -
- ); -} diff --git a/src/frontend/components/circuit-tree-preview.tsx b/src/frontend/components/circuit-tree-preview.tsx deleted file mode 100644 index 207dee4..0000000 --- a/src/frontend/components/circuit-tree-preview.tsx +++ /dev/null @@ -1,219 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { Fragment } from "react"; -import { getCircuitTree } from "../utils/api"; -import type { CircuitTreeCircuitDto, CircuitTreeResponseDto } from "../types"; -import { - formatCellValue, - formatValue, - getCircuitSectionLabel, -} from "../utils/circuit-grid-model"; - -function renderCircuitSummaryLabel(circuit: CircuitTreeCircuitDto) { - if (circuit.displayName?.trim()) { - return circuit.displayName; - } - if (circuit.deviceRows.length > 1) { - return `${circuit.deviceRows.length} Geräte`; - } - return "Reserve"; -} - -export function CircuitTreePreview(props: { projectId: string; circuitListId: string }) { - const { projectId, circuitListId } = props; - const [data, setData] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - setIsLoading(true); - setError(null); - getCircuitTree(projectId, circuitListId) - .then(setData) - .catch((err: unknown) => - setError(err instanceof Error ? err.message : "Die Stromkreisstruktur konnte nicht geladen werden.") - ) - .finally(() => setIsLoading(false)); - }, [projectId, circuitListId]); - - if (isLoading) { - return
Stromkreisstruktur wird geladen …
; - } - - if (error) { - return
{error}
; - } - - if (!data || !data.sections.length) { - return
Keine Bereiche oder Stromkreise vorhanden.
; - } - - const hasAnyCircuits = data.sections.some((section) => section.circuits.length > 0); - if (!hasAnyCircuits) { - return
Bereiche sind vorhanden, enthalten aber noch keine Stromkreise.
; - } - - return ( -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - {data.sections.map((section) => ( - - ))} - -
BetriebsmittelkennzeichenAnzeigenamePhasenartAnschlussartKostengruppeKategorieEbeneRaumnummerRaumnameAnzahlLeistung / Gerät [kW]Gleichzeitigkeitcos φGesamtsumme [kW]SchutzartBemessungsstromCharakteristikKabeltypKabelquerschnittKabellängeBemerkung
-
-
-
- ); -} - -function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number] }) { - const { section } = props; - return ( - <> - - - {getCircuitSectionLabel(section)} - - - {section.circuits.map((circuit) => { - if (circuit.deviceRows.length === 0) { - return ( - - {circuit.equipmentIdentifier} - {circuit.displayName?.trim() || "Reserve"} - - - - {formatCellValue(circuit.circuitTotalPower, "circuitTotalPower")} - - {circuit.protectionDevice?.type ?? "-"} - - {formatValue(circuit.protectionDevice?.ratedCurrentA, "protectionRatedCurrent")} - - {circuit.protectionDevice?.tripCharacteristic ?? circuit.protectionDevice?.fuseUtilizationCategory ?? "-"} - {circuit.cableType ?? "-"} - {circuit.cableCrossSection ?? "-"} - {formatValue(circuit.cableLength, "cableLength")} - {circuit.remark ?? "-"} - - ); - } - - if (circuit.deviceRows.length === 1) { - const row = circuit.deviceRows[0]; - return ( - - {circuit.equipmentIdentifier} - {row.displayName || row.name} - {formatValue(row.phaseType ?? undefined, "phaseType")} - {row.connectionKind ?? "-"} - {row.costGroup ?? "-"} - {row.category ?? "-"} - {row.level ?? "-"} - {row.roomNumberSnapshot ?? "-"} - {row.roomNameSnapshot ?? "-"} - {formatCellValue(row.quantity, "quantity")} - {formatCellValue(row.powerPerUnit, "powerPerUnit")} - - {formatValue(row.simultaneityFactor, "simultaneityFactor")} - - {formatValue(row.cosPhi, "cosPhi")} - {formatCellValue(row.rowTotalPower, "rowTotalPower")} - {circuit.protectionDevice?.type ?? "-"} - - {formatValue(circuit.protectionDevice?.ratedCurrentA, "protectionRatedCurrent")} - - {circuit.protectionDevice?.tripCharacteristic ?? circuit.protectionDevice?.fuseUtilizationCategory ?? "-"} - {circuit.cableType ?? "-"} - {circuit.cableCrossSection ?? "-"} - {formatValue(circuit.cableLength, "cableLength")} - {row.remark ?? circuit.remark ?? "-"} - - ); - } - - return ( - - - {circuit.equipmentIdentifier} - {renderCircuitSummaryLabel(circuit)} - - - - {formatCellValue(circuit.circuitTotalPower, "circuitTotalPower")} - - {circuit.protectionDevice?.type ?? "-"} - - {formatValue(circuit.protectionDevice?.ratedCurrentA, "protectionRatedCurrent")} - - {circuit.protectionDevice?.tripCharacteristic ?? circuit.protectionDevice?.fuseUtilizationCategory ?? "-"} - {circuit.cableType ?? "-"} - {circuit.cableCrossSection ?? "-"} - {formatValue(circuit.cableLength, "cableLength")} - {circuit.remark ?? "-"} - - {circuit.deviceRows.map((row) => ( - - - {row.displayName || row.name} - {formatValue(row.phaseType ?? undefined, "phaseType")} - {row.connectionKind ?? "-"} - {row.costGroup ?? "-"} - {row.category ?? "-"} - {row.level ?? "-"} - {row.roomNumberSnapshot ?? "-"} - {row.roomNameSnapshot ?? "-"} - {formatCellValue(row.quantity, "quantity")} - {formatCellValue(row.powerPerUnit, "powerPerUnit")} - - {formatValue(row.simultaneityFactor, "simultaneityFactor")} - - {formatValue(row.cosPhi, "cosPhi")} - {formatCellValue(row.rowTotalPower, "rowTotalPower")} - - - - - - - - - - - - - {row.remark ?? "-"} - - ))} - - ); - })} - - -frei- - Freie Zeile - - - ); -} From 43a3bb0b69a5e1631a37f79970712a80fc9714c3 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Fri, 31 Jul 2026 16:41:37 +0200 Subject: [PATCH 26/59] Align documentation with clean baseline --- docs/circuit-list-editor-api.md | 4 +- docs/current-architecture.md | 16 +-- ...history-and-external-model-architecture.md | 3 +- docs/spec/03-data-model-concept.md | 22 ++-- docs/spec/07-implementation-phases-todo.md | 8 +- docs/spec/08-current-product-backlog.md | 46 +++---- ...-board-components-and-protection-groups.md | 114 +++++++----------- 7 files changed, 91 insertions(+), 122 deletions(-) diff --git a/docs/circuit-list-editor-api.md b/docs/circuit-list-editor-api.md index 8f225a0..bd975d1 100644 --- a/docs/circuit-list-editor-api.md +++ b/docs/circuit-list-editor-api.md @@ -74,8 +74,8 @@ Protected-board structure uses persisted inverse of a delete. The dispatcher also supports the documented project-device, project settings, distribution-board and project-location commands. The generic editor commands listed here use schema version `1`; -older stored formats can remain supported by their owning command boundary. -Unsupported types are rejected. Insert commands contain the complete entity or circuit block +the clean pre-release baseline does not accept older development formats. +Unsupported types or schema versions are rejected. Insert commands contain the complete entity or circuit block with stable ids; delete commands include the expected parent identity. Circuit snapshots contain zero, one or multiple complete device rows. Move commands contain each row's expected and target circuit plus its exact expected and diff --git a/docs/current-architecture.md b/docs/current-architecture.md index dc8a3f8..42f4adc 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -267,7 +267,7 @@ deshalb weder eine bestehende Projekt-ID noch `expectedRevision` annimmt. Das Ersetzen eines Projekts bleibt auf dessen Einstellungsmodal und den projektgebundenen Endpunkt beschränkt. `distribution-board.insert` versioniert die Anlage einer Verteilung mit -Command-Schema 3 als vollständigen Block aus Verteilung, Stromkreisliste, den +Command-Schema 1 als vollständigen Block aus Verteilung, Stromkreisliste, den drei Gruppen `Beleuchtung 1`, `1-phasig 1`, `3-phasig 1`, Hauptschalter `-Q0` und Überspannungsableiter `-FA`. Alle UUIDs entstehen vor dem Command und bleiben über Undo/Redo stabil. @@ -349,9 +349,9 @@ Teilwiederherstellungen. Der Circuit-Tree liefert zusätzlich `headerComponents`, gruppenbezogene `components` und `footerComponents`. Abschnitte enthalten optionale Gruppenkategorie und -nummer; Stromkreise und Gruppenkomponenten -enthalten ihre optionale getrennte Schutzgerätekonfiguration. Die bisherigen -flachen Stromkreis-Schutzfelder bleiben während der Übergangsphase additiv -erhalten. Separate aktive Read-Repositories lesen Komponenten- und beide +enthalten ihre getrennte Schutzgerätekonfiguration. Die +1:1-Schutzgerätetabellen sind die einzige unterstützte Quelle dieser Daten. +Separate aktive Read-Repositories lesen Komponenten- und beide Schutzgerätetabellen; Schreibzugriffe bleiben ausschließlich in Commands. `src/frontend/utils/circuit-structure-projection.ts` projiziert diesen Tree rein und deterministisch in die drei Zonen Kopf, vollständige Gruppen und Fuß. @@ -381,12 +381,12 @@ Projektzugehörigkeit und schreibt Schutzgerät, Revision und Historienübergang atomar. Benutzer können Schutzdaten anlegen oder ändern, aber nicht entfernen; Undo einer erstmaligen Anlage darf den zuvor fehlenden Datensatz exakt wiederherstellen. -Der vollständige `CircuitSnapshot` kann rückwärtskompatibel einen -`protectionDevice`-Datensatz enthalten. Neue gruppierte Stromkreise und durch -Geräteverschiebung erzeugte Zielstromkreise verwenden die vereinbarten +Der vollständige `CircuitSnapshot` enthält den zugehörigen +`protectionDevice`-Datensatz, sofern einer vorhanden ist. Neue Stromkreise und +durch Geräteverschiebung erzeugte Zielstromkreise verwenden die vereinbarten Kategorie-Standardwerte und schreiben Schutzgerät, Stromkreis sowie Gerätezeilen atomar. Delete/Undo erfasst denselben Datensatz vollständig. -Im Editor sind die bisherigen Schutzspalten deshalb nur noch eine +Im Editor sind die Schutzspalten deshalb eine schreibgeschützte Projektion der 1:1-Daten; Änderungen erfolgen über ein geräteabhängiges Schutzgeräte-Modal und `circuit-protection.update`. Gruppen lassen sich im Editor schrittweise nur gegenüber einer benachbarten diff --git a/docs/project-history-and-external-model-architecture.md b/docs/project-history-and-external-model-architecture.md index ae03868..a4d4cbd 100644 --- a/docs/project-history-and-external-model-architecture.md +++ b/docs/project-history-and-external-model-architecture.md @@ -84,7 +84,8 @@ Implemented named-snapshot foundation: stores the complete project state in one SQLite transaction - the versioned payload contains project settings, distribution boards, circuit lists, sections, circuits with device rows, project devices, floors - and rooms while excluding global and upgrade-only data + and rooms while excluding the global device catalog and other + non-project-owned data - snapshot creation/listing does not create a project revision or affect undo/redo eligibility - public Create/List endpoints expose metadata but not the logical payload diff --git a/docs/spec/03-data-model-concept.md b/docs/spec/03-data-model-concept.md index 545c450..dec30f6 100644 --- a/docs/spec/03-data-model-concept.md +++ b/docs/spec/03-data-model-concept.md @@ -6,9 +6,10 @@ This is a conceptual model for Codex. It is not a final database schema, but the A distribution board contains a circuit list. -A circuit list contains sections. +A circuit list contains circuit groups. `CircuitSection` is the persisted name +of that group aggregate. -A section contains circuits. +A circuit group contains circuits of one category. A circuit contains zero, one or multiple device rows. @@ -60,29 +61,28 @@ User-facing fields: - `section` - `displayName` optional - `circuitTotalPower` -- `protectionType` -- `protectionRatedCurrent` -- `protectionCharacteristic` +- `protectionDevice` - `cableType` - `cableCrossSection` - `cableLength` -- `rcdAssignment` optional - `terminalDesignation` optional -- `voltage` optional +- `voltage`, derived from project settings and phase category - `controlRequirement` optional, for example DALI or KNX - `status` optional - `isReserve` optional - `remark` optional +`protectionDevice` is the circuit-owned one-to-one protection configuration. +It contains the device type, rated current and only the type-dependent values +such as trip characteristic or residual current. Newly created circuits +receive the category default defined in the protection catalog. + Circuit-level fields only: -- protection type -- protection rated current -- protection characteristic +- protection device and all of its technical values - cable type - cable cross-section - cable length -- RCD assignment - terminal designation - circuit total power diff --git a/docs/spec/07-implementation-phases-todo.md b/docs/spec/07-implementation-phases-todo.md index 16124c0..3683534 100644 --- a/docs/spec/07-implementation-phases-todo.md +++ b/docs/spec/07-implementation-phases-todo.md @@ -559,11 +559,11 @@ PostgreSQL should be introduced when shared multi-user operation, background job Goal: -After the legacy consumer path has been removed, provide one accurate entry point for developers, contributors and LLM-assisted work. +With the former Consumer path removed, provide one accurate entry point for developers, contributors and LLM-assisted work. Timing: -- perform the final rewrite after legacy cutover so the documentation describes only the supported architecture +- keep the post-cleanup documentation limited to the supported architecture - update documentation incrementally during earlier phases, but do not present transitional paths as the long-term design README target structure: @@ -618,8 +618,8 @@ Implemented handoff: - README is a concise setup and project entry point without prototype status lists - `docs/README.md` separates current, operational, future, specification and archived material -- current runtime and module boundaries are documented without unsupported - upgrade-only data paths +- current runtime and module boundaries are documented without compatibility + paths for unsupported pre-release data formats - contributor workflow covers clean databases, safe demo data, migrations, tests and commits - development Compose is clearly separated from the currently unsupported production deployment - `AGENTS.md` points LLM-assisted work to supported code paths and deferred architecture diff --git a/docs/spec/08-current-product-backlog.md b/docs/spec/08-current-product-backlog.md index 69046fb..4a81bb5 100644 --- a/docs/spec/08-current-product-backlog.md +++ b/docs/spec/08-current-product-backlog.md @@ -14,11 +14,11 @@ requirements and intended sequencing, not proof of implementation. component model in phased work packages. - [x] Phase A: central protection catalog, validation, defaults and pure grouped BMK rules. -- [x] Phase B1: additive relational schema, group backfill, component/protection - tables and shared BMK uniqueness. -- [x] Phase B2: snapshot/transfer schema upgrade and deterministic compatibility - mapping. -- [x] Phase C1: versioned new-board command with three groups and fixed header +- [x] Phase B1: relational group, component and protection tables with shared + BMK uniqueness. +- [x] Phase B2: complete snapshot/transfer schema for the supported project + state. +- [x] Phase C1: new-board command with three groups and fixed header components. - [x] Phase C2a1: persistent insert/delete commands for mutable group protection and auxiliary components. @@ -52,22 +52,22 @@ requirements and intended sequencing, not proof of implementation. ## First Release Baseline -Complete this immediately before the first supported release, once the schema -and initial feature set are frozen: +The pre-release cleanup established the following compatibility boundary: -- Decide explicitly whether pre-release databases, portable JSON exports, - logical snapshots and persisted command histories remain supported. -- Create one clean baseline migration that builds the complete release schema - on an empty database. -- Replace the active development migration chain and Drizzle metadata only if - no released installation depends on it. -- Remove upgrade-only database, snapshot and command compatibility code only - for formats that are explicitly declared unsupported. -- Preserve the pre-release migration history in Git history or a release tag. -- Verify installation, migrations, application startup and core workflows - against a completely empty database. -- Mark the resulting compatibility boundary with the first release tag. -- Treat every migration published after that release as immutable. +- [x] Pre-release databases, portable JSON exports, logical snapshots and + persisted command histories are explicitly unsupported. +- [x] Migration `0000` builds the complete supported schema on an empty + database. +- [x] The development migration chain and Drizzle metadata were replaced + before any supported release depended on them. +- [x] Upgrade-only database, snapshot and command compatibility paths were + removed. +- [x] Pre-cleanup development history remains available through Git history. +- [x] Clean installation, migration, startup and core workflows were verified + against an empty database. +- [ ] Mark this compatibility boundary with the first supported release tag. +- After that release, every published migration is immutable and later schema + changes must use new additive migrations. ## Project History @@ -109,9 +109,9 @@ and initial feature set are frozen: device rows with a valid phase are three-phase. - [x] Changing project voltage settings updates all project devices and circuits atomically in the same persistent Undo/Redo step. -- [x] Database migration `0019` and snapshot schema version `5` normalize older - stored values. Version-four and older imports remain supported and are - normalized while being upgraded. +- [x] Baseline migration `0000` and project-state schema version `1` store only + the canonical derived values. Pre-release imports are intentionally + unsupported. ## Circuit List Power Summary diff --git a/docs/spec/09-distribution-board-components-and-protection-groups.md b/docs/spec/09-distribution-board-components-and-protection-groups.md index c3df4f2..a8fe03f 100644 --- a/docs/spec/09-distribution-board-components-and-protection-groups.md +++ b/docs/spec/09-distribution-board-components-and-protection-groups.md @@ -4,9 +4,8 @@ This document records the agreed and implemented behavior for distribution-board components, protected circuit groups and circuit -protection devices. Delivery phases A through E are complete. Phase F retains -the final GUI checklist; the current runtime paths are documented in -`docs/current-architecture.md`. +protection devices. Delivery phases A through F are complete. The current +runtime paths are documented in `docs/current-architecture.md`. Full rule-based protection and cable sizing is deliberately deferred. This phase establishes the structure and manually selected technical values that a @@ -34,7 +33,7 @@ later calculation and warning engine will evaluate. - channel-level relationships between actors and controlled circuits - PostgreSQL or multi-user operation -## Existing Model Audit +## Implemented Model Boundary ### Keep @@ -44,33 +43,28 @@ later calculation and warning engine will evaluate. - Circuit-level load, cable and control fields remain circuit-owned. - Existing stable UUIDs, project revisions, snapshots and persistent Undo/Redo remain authoritative. -- `CircuitSection` remains the migration starting point for a protected circuit +- `CircuitSection` is the persisted representation of a protected circuit group. - Grid projection remains separate from persisted domain structure. ### Change -- A `CircuitSection` gains an explicit category and group number and represents +- A `CircuitSection` has an explicit category and group number and represents one protected circuit group. -- Circuit protection changes from loosely related flat strings to an explicit +- Circuit protection uses an explicit one-to-one protection-device model. - Distribution-board components become independent BMK-bearing list entries. - Equipment-identifier uniqueness covers circuits and all distribution-board components in the complete circuit list. - Moving a circuit to another group of the same category atomically assigns the next identifier of the target group. -- New distribution boards create three default groups rather than treating the - optional unassigned compatibility section as a normal fourth group. +- New distribution boards create exactly three default groups. -### Remove Eventually +### Remaining Model Cleanup -- hard-coded assumptions that each category has exactly one section -- numbering that only understands `prefix + integer` -- free-text protection-device combinations that cannot be validated -- `rcdAssignment` as a substitute for an explicit group relationship - -Removal happens only after migrated state, snapshots, commands and the editor -use the replacement model. +- The older optional `rcdAssignment` circuit field still exists separately + from the explicit group relationship. Removing it requires its own reviewed + domain and UI change; it is not a compatibility path for pre-release data. ## Target Domain Structure @@ -464,7 +458,7 @@ Every command must: - commit the domain mutation, revision and history transition atomically - remain undoable and redoable after application restart -## Snapshot and Transfer Compatibility +## Snapshot and Transfer Boundary The supported logical project snapshot and portable JSON transfer must include: @@ -473,28 +467,15 @@ The supported logical project snapshot and portable JSON transfer must include: - circuit protection-device state - all new component relationships and sort positions -Introducing the model requires a snapshot-schema version increase and explicit -upgraders for supported older payloads. +The clean pre-release baseline uses project-state schema version `1`. It +contains the complete supported structure above and is shared by logical +snapshots, restore, Undo/Redo and portable JSON transfer. Payloads from earlier +development schemas are intentionally unsupported and rejected before any +write. -Database migration rules: - -- preserve all existing stable UUIDs -- preserve existing circuit equipment identifiers during schema migration -- map the existing lighting, single-phase and three-phase sections to group 1 - of the respective category -- preserve an existing unassigned section only when required by existing data; - it is not created for a new distribution board -- preserve existing flat circuit protection values when creating the new - one-to-one protection state -- do not silently replace an existing protection selection with a new default -- allow legacy identifiers to remain until an explicit, collision-safe - conversion or renumber action is approved - -New defaults apply to newly created circuits and boards, not as an implicit -rewrite of existing planning data. - -Historical persisted commands and snapshots that remain in the supported -compatibility window must still deserialize and execute after the migration. +Migration `0000` creates the complete relational model on an empty database. +New defaults apply when boards, groups or circuits are created; they never +silently overwrite a planner's later selection. ## Later Sizing and Warning Engine @@ -544,7 +525,7 @@ Acceptance: - invalid rating/type/field combinations are rejected - defaults produce the exact agreed values -### B. Persistence and Compatibility Model +### B. Persistence and Snapshot Model Status: Complete. @@ -552,44 +533,31 @@ Status: Complete. - add distribution-board component persistence - add one-to-one circuit protection persistence - implement shared BMK uniqueness -- generate and inspect one additive migration -- add snapshot/transfer schema upgrade -- preserve existing identifiers and protection values +- create the complete relational baseline schema +- include the model in snapshots and portable transfers +- preserve identifiers and protection values within supported project state -Implemented in B1: +Implemented persistence: -- additive migration `0024` adds group identity to circuit sections and maps - the three established sections to group 1 without changing UUIDs +- baseline migration `0000` creates group identity, components and protection + state directly for clean installations - separate component and circuit/component protection tables preserve the one-to-one ownership boundaries - a trigger-maintained circuit-list registry rejects normalized BMK collisions across circuits and distribution-board components -- the existing flat circuit protection fields remain untouched until their - deterministic snapshot/runtime transition is implemented -- clean and populated pre-migration databases are covered by migration tests -- newly created legacy-compatible board structures receive the same initial - group identity while their persisted command format remains unchanged - -Implemented in B2: - -- snapshot schema version 7 includes group identity, distribution-board +- snapshot schema version `1` includes group identity, distribution-board components and both one-to-one protection collections - capture, named and automatic snapshots, restore, Undo/Redo and portable JSON transfers preserve the complete new relational state - duplicate imports remap component UUIDs and both protection-owner references together with the existing project structure -- versions 1 through 6 remain readable; version 6 maps the three established - section keys to group 1 and leaves new collections empty -- older flat circuit protection values remain present and are not guessed into - strict new protection configurations -- current snapshots reject invalid ownership, duplicate group numbers, +- snapshots reject invalid ownership, duplicate group numbers, cross-entity BMKs and invalid protection configurations before persistence Acceptance: -- a current database migrates without data loss - an empty database creates the complete schema -- old supported snapshots and transfers upgrade deterministically +- unsupported pre-release snapshots and transfers are rejected before writing - BMK collisions across circuits and components are rejected ### C. Persistent Commands and Board Defaults @@ -603,14 +571,13 @@ Status: Complete. Implemented in C1: -- `distribution-board.insert` schema version 3 carries stable UUIDs for three +- `distribution-board.insert` schema version `1` carries stable UUIDs for three initial groups, main switch `-Q0` and surge protective device `-FA` - its inverse verifies and removes the unchanged complete structure; Redo restores the same UUIDs - a late revision/history failure rolls back board, list, groups and components together -- stored schema versions 1 and 2 retain four legacy sections and receive no - invented components +- unsupported command schema versions are rejected Implemented in C2a1: @@ -819,13 +786,13 @@ checklist are complete. Acceptance: - documentation distinguishes implemented behavior from later sizing -- clean install and migrated database both work +- clean installation and baseline migration work - snapshots, JSON transfer and Undo/Redo cover all new state ## Risks and Mitigations -- Mixed legacy and grouped BMKs: - preserve old identifiers and provide an explicit conversion path. +- Incorrect implicit BMK changes: + preserve identifiers during sorting and require explicit renumbering. - Cross-entity BMK collisions: enforce one transaction-level uniqueness boundary for the complete list. - Partial destructive writes: @@ -836,8 +803,9 @@ Acceptance: use one shared typed catalog for UI, API and domain validation. - Silent planning changes: apply defaults only on creation and keep later recommendations explicit. -- Snapshot incompatibility: - version payloads and test every supported upgrade path. +- Future snapshot evolution: + introduce a new explicit schema version and upgrader only after defining its + supported compatibility window. ## Resolved Implementation Decisions @@ -848,6 +816,6 @@ Acceptance: - structural component rows remain outside editable-cell filtering and spreadsheet navigation; circuit filtering continues to preserve complete circuit blocks -- explicit UI for converting retained legacy identifiers to grouped numbering -- whether fixed header components become editable beyond name and BMK in this - phase +- explicit group renumbering updates grouped identifiers collision-safely +- fixed header components remain read-only; mutable group and auxiliary + components use their dedicated modals From 44b6fde940bc2ee2d13bee47b0d8d8df8246c77e Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Fri, 31 Jul 2026 17:41:30 +0200 Subject: [PATCH 27/59] Add public building project setting --- AGENTS.md | 16 +- README.md | 7 +- docs/circuit-list-editor-api.md | 14 +- docs/current-architecture.md | 21 +- ...history-and-external-model-architecture.md | 5 +- docs/revit-requirements-llm-context.md | 2 +- .../spec/06-future-sizing-and-calculations.md | 6 + docs/spec/07-implementation-phases-todo.md | 6 +- docs/spec/08-current-product-backlog.md | 15 +- ...-board-components-and-protection-groups.md | 12 +- scripts/db-verify-circuit-schema.js | 1 + .../0001_add_public_building_setting.sql | 1 + src/db/migrations/meta/0001_snapshot.json | 1748 +++++++++++++++++ src/db/migrations/meta/_journal.json | 7 + ...ect-settings-project-command.repository.ts | 38 +- ...roject-state-restore-command.repository.ts | 1 + .../project-state-snapshot.persistence.ts | 2 + src/db/repositories/project.repository.ts | 1 + src/db/schema/projects.ts | 3 + .../project-settings-project-command.model.ts | 43 +- .../models/project-state-snapshot.model.ts | 43 +- .../components/project-settings-modal.tsx | 26 + src/frontend/types.ts | 1 + src/frontend/utils/api.ts | 1 + .../validation/project-structure.schemas.ts | 2 + tests/project-command.service.test.ts | 5 + ...ettings-project-command.repository.test.ts | 68 + tests/project-state-snapshot.test.ts | 14 +- tests/project-version-history.test.ts | 2 + 29 files changed, 2057 insertions(+), 54 deletions(-) create mode 100644 src/db/migrations/0001_add_public_building_setting.sql create mode 100644 src/db/migrations/meta/0001_snapshot.json diff --git a/AGENTS.md b/AGENTS.md index f3bdd53..8499827 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -250,9 +250,11 @@ not change the project revision or undo/redo stacks. Restoring a server-stored snapshot verifies its checksum and the current-state hash, replaces supported project data atomically and records a new `restore` revision with a complete inverse command. Restore can therefore be undone and redone after a restart. -Baseline snapshot schema version 1 contains circuit-group identity, -distribution-board components and the separate circuit/component protection -records. Pre-baseline snapshots are unsupported. Portable duplicate imports +Current snapshot schema version 2 additionally stores whether the project is a +public building. Supported baseline version 1 snapshots are upgraded with +`isPublicBuilding = false`; pre-baseline snapshots remain unsupported. Both +versions contain circuit-group identity, distribution-board components and the +separate circuit/component protection records. Portable duplicate imports remap component ids and protection-owner references together with the existing project graph. The central revision boundary creates an automatic logical snapshot after each @@ -284,9 +286,11 @@ global-to-project copy API/UI paths use these persistent commands and track the returned project revision. ProjectDevice synchronization/disconnect API and UI paths do the same; their undo action uses the project-wide history endpoint. Project settings use the persistent `project.update-settings` command. Project -metadata, both voltage defaults and the enabled distribution-board supply types -change in one revision and Undo/Redo restores them together; the project PUT -route requires `expectedRevision`. The system catalog is `AV`, `SV`, `EV`, +metadata, the public-building classification, both voltage defaults and the +enabled distribution-board supply types change in one revision and Undo/Redo +restores them together; the project PUT route requires `expectedRevision`. +The public-building flag is the future sizing input for requiring halogen-free +cables and lines. The system catalog is `AV`, `SV`, `EV`, `USV`, `MSR`, `SiBe`; at least one must be enabled and a type used by a board cannot be disabled. Distribution-board setup uses `distribution-board.insert` schema version 1 diff --git a/README.md b/README.md index 1b60bcd..c333917 100644 --- a/README.md +++ b/README.md @@ -108,9 +108,10 @@ npm run db:generate ``` `db:backup` verwendet die SQLite-Online-Backup-API und prüft das Ergebnis auf -Integrität und Fremdschlüsselverletzungen. Die Migration `0000` ist die -Release-Baseline und richtet ausschließlich eine neue, leere Datenbank ein. -Datenbanken aus Entwicklungsständen vor dieser Baseline werden nicht unterstützt. +Integrität und Fremdschlüsselverletzungen. Migration `0000` ist die +Release-Baseline für eine leere Datenbank; nachfolgende nummerierte Migrationen +entwickeln diese Baseline additiv weiter. Datenbanken aus Entwicklungsständen +vor der Baseline werden nicht unterstützt. ## Dokumentation diff --git a/docs/circuit-list-editor-api.md b/docs/circuit-list-editor-api.md index bd975d1..13afbe9 100644 --- a/docs/circuit-list-editor-api.md +++ b/docs/circuit-list-editor-api.md @@ -73,9 +73,11 @@ Protected-board structure uses `circuit-protection.update`. A subtree restore is normally emitted only as the persisted inverse of a delete. The dispatcher also supports the documented project-device, project settings, distribution-board and project-location -commands. The generic editor commands listed here use schema version `1`; -the clean pre-release baseline does not accept older development formats. -Unsupported types or schema versions are rejected. Insert commands contain the complete entity or circuit block +commands. The generic editor commands listed here use schema version `1`. +`project.update-settings` uses version `2` and still accepts supported baseline +version `1` commands without changing the newer building classification. +Pre-baseline formats remain unsupported. Unsupported types or schema versions +are rejected. Insert commands contain the complete entity or circuit block with stable ids; delete commands include the expected parent identity. Circuit snapshots contain zero, one or multiple complete device rows. Move commands contain each row's expected and target circuit plus its exact expected and @@ -170,12 +172,12 @@ synchronizes linked circuit rows implicitly. - `PUT /projects/:projectId` - request: - `{ "expectedRevision": 12, "singlePhaseVoltageV": 230, "threePhaseVoltageV": 400, "enabledDistributionBoardSupplyTypes": ["AV", "MSR", "SiBe"], ...projectMetadata }` + `{ "expectedRevision": 12, "isPublicBuilding": true, "singlePhaseVoltageV": 230, "threePhaseVoltageV": 400, "enabledDistributionBoardSupplyTypes": ["AV", "MSR", "SiBe"], ...projectMetadata }` - executes `project.update-settings` as one atomic revision - response: `{ "project": { ... }, "revision": { ... }, "history": { ... } }` - - persistent Undo/Redo restores metadata, voltage values and the enabled - distribution-board supply types together; project-device and circuit + - persistent Undo/Redo restores metadata, the public-building flag, voltage + values and enabled distribution-board supply types together; project-device and circuit voltages are recalculated from the restored project settings in the same transaction - project-device and circuit voltage are derived values and are not accepted diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 42f4adc..5e9ce1d 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -232,8 +232,10 @@ und Historienstapel teilen dieselbe Transaktion. Create, Import aus der globalen Gerätebibliothek und Delete laufen über dieselbe Command-Grenze; ihre Antworten liefern Gerät und aktualisierten Historienstand an die Projektseite zurück. `project.update-settings` versioniert Projektname, interne und externe -Projektnummer, Bauherr, Beschreibung, beide Standardspannungen sowie die im -Projekt freigeschalteten Verteiler-Netzarten als eine atomare Änderung. Der +Projektnummer, Bauherr, Beschreibung, die Kennzeichnung als öffentliches +Gebäude, beide Standardspannungen sowie die im Projekt freigeschalteten +Verteiler-Netzarten als eine atomare Änderung. Die Gebäudekennzeichnung ist +der spätere Eingang für die Auswahl halogenfreier Kabel und Leitungen. Der Systemkatalog besteht aus `AV`, `SV`, `EV`, `USV`, `MSR` und `SiBe`; mindestens eine Netzart muss aktiv bleiben und eine bereits von einer Verteilung verwendete Netzart kann nicht deaktiviert werden. Die Projektseite bearbeitet diese Angaben in einem @@ -414,12 +416,13 @@ wieder her. verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und ist für bestehende sowie neu angelegte Verteilungen standardmäßig `1`. -Das aktuelle Baseline-Snapshot-Schema enthält den Gleichzeitigkeitsfaktor, +Snapshot-Schema 2 enthält zusätzlich die Gebäudekennzeichnung sowie den Gleichzeitigkeitsfaktor, Gruppenkategorie und -nummer, Verteilerkomponenten sowie die getrennten 1:1-Schutzgerätedaten. Capture, benannte und automatische Snapshots, Wiederherstellung, Undo/Redo und beide -JSON-Importmodi verwenden denselben vollständigen Zustand. Vorherige -Entwicklungsschemas werden nicht mehr eingelesen. +JSON-Importmodi verwenden denselben vollständigen Zustand. Die unterstützte +Baseline-Version 1 wird mit `isPublicBuilding = false` hochgestuft; Formate vor +dieser Baseline werden nicht eingelesen. Persistente Insert-, Update- und Delete-Commands versionieren Anlage, Bearbeitung und Löschung von Geschossen und Räumen mit stabilen UUIDs und exakten Vorher-/Nachher-Snapshots. Ein Geschoss wird nur @@ -447,8 +450,8 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät. - SQLite ist die aktuell unterstützte Datenbank. - Fremdschlüssel werden für jeden Datenbankkontext aktiviert. -- `npm run db:migrate` richtet mit der einzelnen Migration `0000` das komplette - aktuelle Schema einer leeren Datenbank ein. +- `npm run db:migrate` richtet mit Baseline-Migration `0000` und den folgenden + additiven Migrationen das aktuelle Schema ein. - `npm run db:verify:circuit-schema` prüft erforderliche und entfernte Spalten. - `npm run db:backup` erzeugt ein konsistentes und verifiziertes Online-Backup. - `npm run typecheck:scripts` prüft die TypeScript-Wartungsskripte, ohne Code zu @@ -469,8 +472,8 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät. flachen Stromkreis-Schutzfelder sind aus der Baseline entfernt. Ein triggergeführtes Register erzwingt bereits eine normalisierte, stromkreislistenweite BMK-Eindeutigkeit über Stromkreise und - Verteilerkomponenten. Snapshot- und Transfer-Integration verwenden - Snapshot-Schema 1 der Baseline. Persistente Insert/Delete/Update-Commands für + Verteilerkomponenten. Snapshot- und Transfer-Integration verwenden aktuell + Snapshot-Schema 2. Persistente Insert/Delete/Update-Commands für veränderliche Verteilerkomponenten, Gruppen einschließlich befüllter Unterbäume sowie vollständige Gruppensortierung sind integriert. Der Editor zeigt die geschützte Struktur an und bearbeitet veränderliche Gruppen- und diff --git a/docs/project-history-and-external-model-architecture.md b/docs/project-history-and-external-model-architecture.md index a4d4cbd..4c0a8ea 100644 --- a/docs/project-history-and-external-model-architecture.md +++ b/docs/project-history-and-external-model-architecture.md @@ -262,8 +262,9 @@ Completed foundation: - `project-device.insert` and `project-device.delete` preserve stable device ids; delete undo restores links only from complete unchanged disconnected-row snapshots in the same transaction -- `project.update-settings` persists both project voltage defaults, its exact - inverse, revision and history transition atomically; the project page and +- `project.update-settings` persists the public-building classification and + both project voltage defaults with their exact inverse, revision and history + transition atomically; the project page and existing PUT route require the current expected revision; the same transaction derives every project-device and circuit voltage again, so Undo/Redo cannot leave mixed voltage states diff --git a/docs/revit-requirements-llm-context.md b/docs/revit-requirements-llm-context.md index 965eb47..314820b 100644 --- a/docs/revit-requirements-llm-context.md +++ b/docs/revit-requirements-llm-context.md @@ -52,7 +52,7 @@ Wichtige fachliche Grenzen: Für einen späteren Revit-Austausch besonders relevante Daten sind: - Projekt: Name, interne/externe Projektnummer, Bauherr, Beschreibung, - Projektspannungen. + Kennzeichnung als öffentliches Gebäude und Projektspannungen. - Geschoss und Raum: interne UUID, Name beziehungsweise Raumnummer und Raumname. - Verteilung: interne UUID, Name, Etage, Netzart und Gleichzeitigkeitsfaktor. diff --git a/docs/spec/06-future-sizing-and-calculations.md b/docs/spec/06-future-sizing-and-calculations.md index b9a5c24..f2bb232 100644 --- a/docs/spec/06-future-sizing-and-calculations.md +++ b/docs/spec/06-future-sizing-and-calculations.md @@ -14,12 +14,18 @@ Future suggestions or checks may include: - cable type - number of cores - cable cross-section +- halogen-free cable or line requirement for public buildings - minimum cable cross-section based on current - maximum permissible cable length based on voltage drop - warning messages if cable length, cross-section or protection device do not match Users must remain able to manually override suggestions. +The project-owned `isPublicBuilding` setting is the authoritative input for +the later halogen-free selection rule. The setting is persisted now; this +document does not yet define or implement the complete cable catalog or sizing +algorithm. + ## Initial Design Rule Examples These are common planning defaults, not a replacement for full norm-compliant calculation. diff --git a/docs/spec/07-implementation-phases-todo.md b/docs/spec/07-implementation-phases-todo.md index 3683534..4ebae4f 100644 --- a/docs/spec/07-implementation-phases-todo.md +++ b/docs/spec/07-implementation-phases-todo.md @@ -465,9 +465,9 @@ Implemented foundation: - ProjectDevice synchronization and disconnect endpoints also require `expectedRevision`; their UI undo uses project-wide persistent history and the obsolete direct restore/reconnect write paths are removed -- project voltage settings use `project.update-settings`; both values, inverse, - revision and history transition commit atomically and the former direct - settings write is removed +- project settings use `project.update-settings`; metadata, public-building + classification, voltage values, inverse, revision and history transition + commit atomically and the former direct settings write is removed - distribution-board setup uses `distribution-board.insert` with a complete stable board/list/default-section snapshot; Undo removes only an unchanged empty setup and the former direct controller write is removed diff --git a/docs/spec/08-current-product-backlog.md b/docs/spec/08-current-product-backlog.md index 4a81bb5..c90dd24 100644 --- a/docs/spec/08-current-product-backlog.md +++ b/docs/spec/08-current-product-backlog.md @@ -54,7 +54,7 @@ requirements and intended sequencing, not proof of implementation. The pre-release cleanup established the following compatibility boundary: -- [x] Pre-release databases, portable JSON exports, logical snapshots and +- [x] Pre-baseline databases, portable JSON exports, logical snapshots and persisted command histories are explicitly unsupported. - [x] Migration `0000` builds the complete supported schema on an empty database. @@ -109,9 +109,16 @@ The pre-release cleanup established the following compatibility boundary: device rows with a valid phase are three-phase. - [x] Changing project voltage settings updates all project devices and circuits atomically in the same persistent Undo/Redo step. -- [x] Baseline migration `0000` and project-state schema version `1` store only - the canonical derived values. Pre-release imports are intentionally - unsupported. +- [x] Baseline migration `0000` stores only canonical derived values. + Pre-baseline imports are intentionally unsupported. + +## Building Classification + +- [x] Store whether a project represents a public building. +- [x] Edit the value in the project settings and preserve it through persistent + Undo/Redo, snapshots and portable project transfers. +- [x] Provide the value as an explicit future sizing input for selecting + halogen-free cables and lines; automatic sizing remains deferred. ## Circuit List Power Summary diff --git a/docs/spec/09-distribution-board-components-and-protection-groups.md b/docs/spec/09-distribution-board-components-and-protection-groups.md index a8fe03f..9c7ee11 100644 --- a/docs/spec/09-distribution-board-components-and-protection-groups.md +++ b/docs/spec/09-distribution-board-components-and-protection-groups.md @@ -467,11 +467,11 @@ The supported logical project snapshot and portable JSON transfer must include: - circuit protection-device state - all new component relationships and sort positions -The clean pre-release baseline uses project-state schema version `1`. It -contains the complete supported structure above and is shared by logical -snapshots, restore, Undo/Redo and portable JSON transfer. Payloads from earlier -development schemas are intentionally unsupported and rejected before any -write. +Current project-state schema version `2` contains the complete supported +structure above and the project building classification. It is shared by +logical snapshots, restore, Undo/Redo and portable JSON transfer. Supported +baseline version `1` is upgraded with the non-public default; payloads from +before that baseline are rejected before any write. Migration `0000` creates the complete relational model on an empty database. New defaults apply when boards, groups or circuits are created; they never @@ -545,7 +545,7 @@ Implemented persistence: one-to-one ownership boundaries - a trigger-maintained circuit-list registry rejects normalized BMK collisions across circuits and distribution-board components -- snapshot schema version `1` includes group identity, distribution-board +- snapshot schema version `2` includes group identity, distribution-board components and both one-to-one protection collections - capture, named and automatic snapshots, restore, Undo/Redo and portable JSON transfers preserve the complete new relational state diff --git a/scripts/db-verify-circuit-schema.js b/scripts/db-verify-circuit-schema.js index 6d291f0..318e549 100644 --- a/scripts/db-verify-circuit-schema.js +++ b/scripts/db-verify-circuit-schema.js @@ -78,6 +78,7 @@ const requiredProjectColumns = [ "external_project_number", "building_owner", "description", + "is_public_building", "enabled_distribution_board_supply_types", ]; const projectColumns = new Set( diff --git a/src/db/migrations/0001_add_public_building_setting.sql b/src/db/migrations/0001_add_public_building_setting.sql new file mode 100644 index 0000000..c646f8d --- /dev/null +++ b/src/db/migrations/0001_add_public_building_setting.sql @@ -0,0 +1 @@ +ALTER TABLE `projects` ADD `is_public_building` integer DEFAULT false NOT NULL; \ No newline at end of file diff --git a/src/db/migrations/meta/0001_snapshot.json b/src/db/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..3cac431 --- /dev/null +++ b/src/db/migrations/meta/0001_snapshot.json @@ -0,0 +1,1748 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "62320e94-1976-4288-8640-d37cfc07f1ee", + "prevId": "2f464eb0-b1c4-4d2d-bfc8-a1466788c961", + "tables": { + "circuit_device_rows": { + "name": "circuit_device_rows", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_id": { + "name": "circuit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "linked_project_device_id": { + "name": "linked_project_device_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase_type": { + "name": "phase_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_kind": { + "name": "connection_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_group": { + "name": "cost_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_number_snapshot": { + "name": "room_number_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_name_snapshot": { + "name": "room_name_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "power_per_unit": { + "name": "power_per_unit", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cos_phi": { + "name": "cos_phi", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "overridden_fields": { + "name": "overridden_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "circuit_device_rows_circuit_id_circuits_id_fk": { + "name": "circuit_device_rows_circuit_id_circuits_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "circuits", + "columnsFrom": [ + "circuit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { + "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "project_devices", + "columnsFrom": [ + "linked_project_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "circuit_device_rows_room_id_rooms_id_fk": { + "name": "circuit_device_rows_room_id_rooms_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_list_equipment_identifiers": { + "name": "circuit_list_equipment_identifiers", + "columns": { + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "circuit_list_equipment_identifiers_owner_unique": { + "name": "circuit_list_equipment_identifiers_owner_unique", + "columns": [ + "owner_type", + "owner_id" + ], + "isUnique": true + }, + "circuit_list_equipment_identifiers_list_identifier_unique": { + "name": "circuit_list_equipment_identifiers_list_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk": { + "name": "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuit_list_equipment_identifiers", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_lists": { + "name": "circuit_lists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "distribution_board_id": { + "name": "distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "circuit_lists_distribution_board_id_unique": { + "name": "circuit_lists_distribution_board_id_unique", + "columns": [ + "distribution_board_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_lists_project_id_projects_id_fk": { + "name": "circuit_lists_project_id_projects_id_fk", + "tableFrom": "circuit_lists", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuit_lists_distribution_board_id_distribution_boards_id_fk": { + "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "circuit_lists", + "tableTo": "distribution_boards", + "columnsFrom": [ + "distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_protection_devices": { + "name": "circuit_protection_devices", + "columns": { + "circuit_id": { + "name": "circuit_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rated_current_a": { + "name": "rated_current_a", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fuse_utilization_category": { + "name": "fuse_utilization_category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trip_characteristic": { + "name": "trip_characteristic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_type": { + "name": "rcd_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rated_residual_current_ma": { + "name": "rated_residual_current_ma", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "circuit_protection_devices_circuit_id_circuits_id_fk": { + "name": "circuit_protection_devices_circuit_id_circuits_id_fk", + "tableFrom": "circuit_protection_devices", + "tableTo": "circuits", + "columnsFrom": [ + "circuit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_sections": { + "name": "circuit_sections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_number": { + "name": "group_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "circuit_sections_list_key_unique": { + "name": "circuit_sections_list_key_unique", + "columns": [ + "circuit_list_id", + "key" + ], + "isUnique": true + }, + "circuit_sections_list_prefix_unique": { + "name": "circuit_sections_list_prefix_unique", + "columns": [ + "circuit_list_id", + "prefix" + ], + "isUnique": true + }, + "circuit_sections_list_category_group_unique": { + "name": "circuit_sections_list_category_group_unique", + "columns": [ + "circuit_list_id", + "category", + "group_number" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_sections_circuit_list_id_circuit_lists_id_fk": { + "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuit_sections", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuits": { + "name": "circuits", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cable_type": { + "name": "cable_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cable_cross_section": { + "name": "cable_cross_section", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cable_length": { + "name": "cable_length", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_assignment": { + "name": "rcd_assignment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_designation": { + "name": "terminal_designation", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voltage": { + "name": "voltage", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "control_requirement": { + "name": "control_requirement", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_reserve": { + "name": "is_reserve", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "circuits_list_equipment_identifier_unique": { + "name": "circuits_list_equipment_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuits_circuit_list_id_circuit_lists_id_fk": { + "name": "circuits_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuits", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuits_section_id_circuit_sections_id_fk": { + "name": "circuits_section_id_circuit_sections_id_fk", + "tableFrom": "circuits", + "tableTo": "circuit_sections", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_board_component_protection_devices": { + "name": "distribution_board_component_protection_devices", + "columns": { + "component_id": { + "name": "component_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rated_current_a": { + "name": "rated_current_a", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fuse_utilization_category": { + "name": "fuse_utilization_category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trip_characteristic": { + "name": "trip_characteristic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_type": { + "name": "rcd_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rated_residual_current_ma": { + "name": "rated_residual_current_ma", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk": { + "name": "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk", + "tableFrom": "distribution_board_component_protection_devices", + "tableTo": "distribution_board_components", + "columnsFrom": [ + "component_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_board_components": { + "name": "distribution_board_components", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "placement": { + "name": "placement", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "distribution_board_components_list_identifier_unique": { + "name": "distribution_board_components_list_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + }, + "distribution_board_components_section_role_unique": { + "name": "distribution_board_components_section_role_unique", + "columns": [ + "section_id", + "role" + ], + "isUnique": true + } + }, + "foreignKeys": { + "distribution_board_components_circuit_list_id_circuit_lists_id_fk": { + "name": "distribution_board_components_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "distribution_board_components", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "distribution_board_components_section_id_circuit_sections_id_fk": { + "name": "distribution_board_components_section_id_circuit_sections_id_fk", + "tableFrom": "distribution_board_components", + "tableTo": "circuit_sections", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_boards": { + "name": "distribution_boards", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "floor_id": { + "name": "floor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supply_type": { + "name": "supply_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "distribution_boards_project_id_projects_id_fk": { + "name": "distribution_boards_project_id_projects_id_fk", + "tableFrom": "distribution_boards", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "distribution_boards_floor_id_floors_id_fk": { + "name": "distribution_boards_floor_id_floors_id_fk", + "tableFrom": "distribution_boards", + "tableTo": "floors", + "columnsFrom": [ + "floor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "floors": { + "name": "floors", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "floors_project_id_projects_id_fk": { + "name": "floors_project_id_projects_id_fk", + "tableFrom": "floors", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "global_devices": { + "name": "global_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "installed_power_per_unit_kw": { + "name": "installed_power_per_unit_kw", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "demand_factor": { + "name": "demand_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "voltage_v": { + "name": "voltage_v", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phase_count": { + "name": "phase_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "power_factor": { + "name": "power_factor", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_change_sets": { + "name": "project_change_sets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_revision_id": { + "name": "project_revision_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_type": { + "name": "command_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_schema_version": { + "name": "payload_schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forward_payload_json": { + "name": "forward_payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inverse_payload_json": { + "name": "inverse_payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_change_sets_revision_unique": { + "name": "project_change_sets_revision_unique", + "columns": [ + "project_revision_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_change_sets_project_revision_id_project_revisions_id_fk": { + "name": "project_change_sets_project_revision_id_project_revisions_id_fk", + "tableFrom": "project_change_sets", + "tableTo": "project_revisions", + "columnsFrom": [ + "project_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_devices": { + "name": "project_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase_type": { + "name": "phase_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'single_phase'" + }, + "connection_kind": { + "name": "connection_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_group": { + "name": "cost_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "power_per_unit": { + "name": "power_per_unit", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "cos_phi": { + "name": "cos_phi", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voltage_v": { + "name": "voltage_v", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "project_devices_project_id_projects_id_fk": { + "name": "project_devices_project_id_projects_id_fk", + "tableFrom": "project_devices", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_history_stack_entries": { + "name": "project_history_stack_entries", + "columns": { + "change_set_id": { + "name": "change_set_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stack": { + "name": "stack", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_history_stack_entries_position_unique": { + "name": "project_history_stack_entries_position_unique", + "columns": [ + "project_id", + "stack", + "position" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { + "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", + "tableFrom": "project_history_stack_entries", + "tableTo": "project_change_sets", + "columnsFrom": [ + "change_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_history_stack_entries_project_id_projects_id_fk": { + "name": "project_history_stack_entries_project_id_projects_id_fk", + "tableFrom": "project_history_stack_entries", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "project_history_stack_entries_stack_check": { + "name": "project_history_stack_entries_stack_check", + "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" + } + } + }, + "project_revisions": { + "name": "project_revisions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "project_revisions_project_number_unique": { + "name": "project_revisions_project_number_unique", + "columns": [ + "project_id", + "revision_number" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_revisions_project_id_projects_id_fk": { + "name": "project_revisions_project_id_projects_id_fk", + "tableFrom": "project_revisions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_snapshots": { + "name": "project_snapshots", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_revision": { + "name": "source_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'named'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_sha256": { + "name": "payload_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "project_snapshots_project_created_idx": { + "name": "project_snapshots_project_created_idx", + "columns": [ + "project_id", + "created_at_iso" + ], + "isUnique": false + }, + "project_snapshots_project_kind_revision_idx": { + "name": "project_snapshots_project_kind_revision_idx", + "columns": [ + "project_id", + "kind", + "source_revision" + ], + "isUnique": false + }, + "project_snapshots_project_name_unique": { + "name": "project_snapshots_project_name_unique", + "columns": [ + "project_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_snapshots_project_id_projects_id_fk": { + "name": "project_snapshots_project_id_projects_id_fk", + "tableFrom": "project_snapshots", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_project_number": { + "name": "internal_project_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_project_number": { + "name": "external_project_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "building_owner": { + "name": "building_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_public_building": { + "name": "is_public_building", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "single_phase_voltage_v": { + "name": "single_phase_voltage_v", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 230 + }, + "three_phase_voltage_v": { + "name": "three_phase_voltage_v", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 400 + }, + "enabled_distribution_board_supply_types": { + "name": "enabled_distribution_board_supply_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[\"AV\",\"SV\",\"EV\",\"USV\",\"MSR\",\"SiBe\"]'" + }, + "current_revision": { + "name": "current_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rooms": { + "name": "rooms", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "floor_id": { + "name": "floor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_number": { + "name": "room_number", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "room_name": { + "name": "room_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "rooms_project_id_projects_id_fk": { + "name": "rooms_project_id_projects_id_fk", + "tableFrom": "rooms", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rooms_floor_id_floors_id_fk": { + "name": "rooms_floor_id_floors_id_fk", + "tableFrom": "rooms", + "tableTo": "floors", + "columnsFrom": [ + "floor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 44a9552..63252a5 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1785499119431, "tag": "0000_whole_surge", "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1785511894897, + "tag": "0001_add_public_building_setting", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/repositories/project-settings-project-command.repository.ts b/src/db/repositories/project-settings-project-command.repository.ts index 9ff2405..01a38f3 100644 --- a/src/db/repositories/project-settings-project-command.repository.ts +++ b/src/db/repositories/project-settings-project-command.repository.ts @@ -40,22 +40,33 @@ export class ProjectSettingsProjectCommandRepository if (!current) { throw new Error("Project not found."); } - const target = input.command.payload; + const target: ProjectSettingsValues = + input.command.schemaVersion === 1 + ? { + ...input.command.payload, + isPublicBuilding: current.isPublicBuilding, + } + : input.command.payload; if (projectSettingsEqual(current, target)) { throw new Error("Project settings did not change."); } - const inverse = createProjectSettingsUpdateProjectCommand({ + const inverseValues: ProjectSettingsValues = { name: current.name, internalProjectNumber: current.internalProjectNumber, externalProjectNumber: current.externalProjectNumber, buildingOwner: current.buildingOwner, description: current.description, + isPublicBuilding: current.isPublicBuilding, singlePhaseVoltageV: current.singlePhaseVoltageV, threePhaseVoltageV: current.threePhaseVoltageV, enabledDistributionBoardSupplyTypes: current.enabledDistributionBoardSupplyTypes, - }); + }; + const inverse = + input.command.schemaVersion === 1 + ? createBaselineProjectSettingsUpdateCommand(inverseValues) + : createProjectSettingsUpdateProjectCommand(inverseValues); assertDisabledSupplyTypesAreUnused(tx, input.projectId, target); const updated = tx .update(projects) @@ -71,6 +82,26 @@ export class ProjectSettingsProjectCommandRepository } } +function createBaselineProjectSettingsUpdateCommand( + values: ProjectSettingsValues +) { + return { + schemaVersion: 1 as const, + type: "project.update-settings" as const, + payload: { + name: values.name, + internalProjectNumber: values.internalProjectNumber, + externalProjectNumber: values.externalProjectNumber, + buildingOwner: values.buildingOwner, + description: values.description, + singlePhaseVoltageV: values.singlePhaseVoltageV, + threePhaseVoltageV: values.threePhaseVoltageV, + enabledDistributionBoardSupplyTypes: + values.enabledDistributionBoardSupplyTypes, + }, + }; +} + function projectSettingsEqual( current: ProjectSettingsValues, target: ProjectSettingsValues @@ -81,6 +112,7 @@ function projectSettingsEqual( current.externalProjectNumber === target.externalProjectNumber && current.buildingOwner === target.buildingOwner && current.description === target.description && + current.isPublicBuilding === target.isPublicBuilding && current.singlePhaseVoltageV === target.singlePhaseVoltageV && current.threePhaseVoltageV === target.threePhaseVoltageV && sameSupplyTypes( diff --git a/src/db/repositories/project-state-restore-command.repository.ts b/src/db/repositories/project-state-restore-command.repository.ts index aa0f05d..e2fde68 100644 --- a/src/db/repositories/project-state-restore-command.repository.ts +++ b/src/db/repositories/project-state-restore-command.repository.ts @@ -208,6 +208,7 @@ function replaceProjectState( externalProjectNumber: state.project.externalProjectNumber, buildingOwner: state.project.buildingOwner, description: state.project.description, + isPublicBuilding: state.project.isPublicBuilding, singlePhaseVoltageV: state.project.singlePhaseVoltageV, threePhaseVoltageV: state.project.threePhaseVoltageV, enabledDistributionBoardSupplyTypes: diff --git a/src/db/repositories/project-state-snapshot.persistence.ts b/src/db/repositories/project-state-snapshot.persistence.ts index f82702c..bb5606d 100644 --- a/src/db/repositories/project-state-snapshot.persistence.ts +++ b/src/db/repositories/project-state-snapshot.persistence.ts @@ -39,6 +39,7 @@ export function readProjectStateSnapshot( externalProjectNumber: projects.externalProjectNumber, buildingOwner: projects.buildingOwner, description: projects.description, + isPublicBuilding: projects.isPublicBuilding, singlePhaseVoltageV: projects.singlePhaseVoltageV, threePhaseVoltageV: projects.threePhaseVoltageV, enabledDistributionBoardSupplyTypes: @@ -233,6 +234,7 @@ export function readProjectStateSnapshot( externalProjectNumber: project.externalProjectNumber, buildingOwner: project.buildingOwner, description: project.description, + isPublicBuilding: project.isPublicBuilding, singlePhaseVoltageV: project.singlePhaseVoltageV, threePhaseVoltageV: project.threePhaseVoltageV, enabledDistributionBoardSupplyTypes: diff --git a/src/db/repositories/project.repository.ts b/src/db/repositories/project.repository.ts index bc96bee..171290e 100644 --- a/src/db/repositories/project.repository.ts +++ b/src/db/repositories/project.repository.ts @@ -25,6 +25,7 @@ export class ProjectRepository { externalProjectNumber: input.externalProjectNumber ?? null, buildingOwner: input.buildingOwner ?? null, description: input.description ?? null, + isPublicBuilding: input.isPublicBuilding ?? false, singlePhaseVoltageV: input.singlePhaseVoltageV ?? 230, threePhaseVoltageV: input.threePhaseVoltageV ?? 400, enabledDistributionBoardSupplyTypes: [ diff --git a/src/db/schema/projects.ts b/src/db/schema/projects.ts index 04671d1..d694258 100644 --- a/src/db/schema/projects.ts +++ b/src/db/schema/projects.ts @@ -9,6 +9,9 @@ export const projects = sqliteTable("projects", { externalProjectNumber: text("external_project_number"), buildingOwner: text("building_owner"), description: text("description"), + isPublicBuilding: integer("is_public_building", { mode: "boolean" }) + .notNull() + .default(false), singlePhaseVoltageV: integer("single_phase_voltage_v").notNull().default(230), threePhaseVoltageV: integer("three_phase_voltage_v").notNull().default(400), enabledDistributionBoardSupplyTypes: text( diff --git a/src/domain/models/project-settings-project-command.model.ts b/src/domain/models/project-settings-project-command.model.ts index 37bb132..69e9c5c 100644 --- a/src/domain/models/project-settings-project-command.model.ts +++ b/src/domain/models/project-settings-project-command.model.ts @@ -6,7 +6,8 @@ import { export const projectSettingsUpdateCommandType = "project.update-settings" as const; -export const projectSettingsUpdateCommandSchemaVersion = 1 as const; +export const projectSettingsUpdateCommandSchemaVersion = 2 as const; +const previousProjectSettingsUpdateCommandSchemaVersion = 1 as const; export interface ProjectSettingsValues { name: string; @@ -14,21 +15,37 @@ export interface ProjectSettingsValues { externalProjectNumber: string | null; buildingOwner: string | null; description: string | null; + isPublicBuilding: boolean; singlePhaseVoltageV: number; threePhaseVoltageV: number; enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[]; } -export interface ProjectSettingsUpdateProjectCommand +type PreviousProjectSettingsValues = Omit< + ProjectSettingsValues, + "isPublicBuilding" +>; + +export interface CurrentProjectSettingsUpdateProjectCommand extends SerializedProjectCommand { schemaVersion: typeof projectSettingsUpdateCommandSchemaVersion; type: typeof projectSettingsUpdateCommandType; } +interface PreviousProjectSettingsUpdateProjectCommand + extends SerializedProjectCommand { + schemaVersion: typeof previousProjectSettingsUpdateCommandSchemaVersion; + type: typeof projectSettingsUpdateCommandType; +} + +export type ProjectSettingsUpdateProjectCommand = + | CurrentProjectSettingsUpdateProjectCommand + | PreviousProjectSettingsUpdateProjectCommand; + export function createProjectSettingsUpdateProjectCommand( values: ProjectSettingsValues -): ProjectSettingsUpdateProjectCommand { - const command: ProjectSettingsUpdateProjectCommand = { +): CurrentProjectSettingsUpdateProjectCommand { + const command: CurrentProjectSettingsUpdateProjectCommand = { schemaVersion: projectSettingsUpdateCommandSchemaVersion, type: projectSettingsUpdateCommandType, payload: { ...values }, @@ -42,7 +59,8 @@ export function assertProjectSettingsUpdateProjectCommand( ): asserts command is ProjectSettingsUpdateProjectCommand { if ( command.type !== projectSettingsUpdateCommandType || - command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion + (command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion && + command.schemaVersion !== previousProjectSettingsUpdateCommandSchemaVersion) ) { throw new Error("Unsupported project settings update command."); } @@ -58,8 +76,7 @@ export function assertProjectSettingsUpdateProjectCommand( !isNullableTrimmedString(command.payload.buildingOwner, 200) || !isNullableTrimmedString(command.payload.description, 2000) || !isPositiveFiniteNumber(command.payload.singlePhaseVoltageV) || - !isPositiveFiniteNumber(command.payload.threePhaseVoltageV) || - Object.keys(command.payload).length !== 8 + !isPositiveFiniteNumber(command.payload.threePhaseVoltageV) ) { throw new Error( "Project settings update command contains invalid values." @@ -74,6 +91,18 @@ export function assertProjectSettingsUpdateProjectCommand( "Project settings update command contains invalid supply types." ); } + const isPreviousSchema = + command.schemaVersion === previousProjectSettingsUpdateCommandSchemaVersion; + if ( + (isPreviousSchema && Object.keys(command.payload).length !== 8) || + (!isPreviousSchema && + (typeof command.payload.isPublicBuilding !== "boolean" || + Object.keys(command.payload).length !== 9)) + ) { + throw new Error( + "Project settings update command contains invalid values." + ); + } } function isValidSupplyTypes( diff --git a/src/domain/models/project-state-snapshot.model.ts b/src/domain/models/project-state-snapshot.model.ts index 53e2ed2..86d84c4 100644 --- a/src/domain/models/project-state-snapshot.model.ts +++ b/src/domain/models/project-state-snapshot.model.ts @@ -19,13 +19,14 @@ import { resolveProjectVoltage, } from "../services/project-voltage.service.js"; -export const projectStateSnapshotSchemaVersion = 1 as const; +export const projectStateSnapshotSchemaVersion = 2 as const; +const previousProjectStateSnapshotSchemaVersion = 1 as const; const idSchema = z.string().trim().min(1); const nullableStringSchema = z.string().nullable(); const finiteNumberSchema = z.number().finite(); -const projectSchema = z +const previousProjectSchema = z .object({ id: idSchema, name: z.string().trim().min(1), @@ -42,6 +43,10 @@ const projectSchema = z }) .strict(); +const projectSchema = previousProjectSchema + .extend({ isPublicBuilding: z.boolean() }) + .strict(); + const distributionBoardSchema = z .object({ id: idSchema, @@ -267,6 +272,15 @@ export const projectStateSnapshotSchema = z }) .strict(); +const previousProjectStateSnapshotSchema = z + .object({ + schemaVersion: z.literal(previousProjectStateSnapshotSchemaVersion), + project: previousProjectSchema, + distributionBoards: z.array(distributionBoardSchema), + ...projectStateSnapshotContents, + }) + .strict(); + export type ProjectStateSnapshot = z.infer< typeof projectStateSnapshotSchema >; @@ -274,12 +288,35 @@ export type ProjectStateSnapshot = z.infer< export function parseProjectStateSnapshot( value: unknown ): ProjectStateSnapshot { - const parsedSnapshot = projectStateSnapshotSchema.parse(value); + const parsedSnapshot = projectStateSnapshotSchema.parse( + upgradePreviousProjectStateSnapshot(value) + ); const snapshot = normalizeSnapshotPhaseTypes(parsedSnapshot); assertProjectStateSnapshotRelations(snapshot); return snapshot; } +function upgradePreviousProjectStateSnapshot(value: unknown): unknown { + if ( + value === null || + typeof value !== "object" || + Array.isArray(value) || + (value as { schemaVersion?: unknown }).schemaVersion !== + previousProjectStateSnapshotSchemaVersion + ) { + return value; + } + const previous = previousProjectStateSnapshotSchema.parse(value); + return { + ...previous, + schemaVersion: projectStateSnapshotSchemaVersion, + project: { + ...previous.project, + isPublicBuilding: false, + }, + }; +} + function normalizeSnapshotPhaseTypes( snapshot: ProjectStateSnapshot ): ProjectStateSnapshot { diff --git a/src/frontend/components/project-settings-modal.tsx b/src/frontend/components/project-settings-modal.tsx index 228f600..77295df 100644 --- a/src/frontend/components/project-settings-modal.tsx +++ b/src/frontend/components/project-settings-modal.tsx @@ -14,6 +14,7 @@ export interface ProjectSettingsInput { externalProjectNumber: string | null; buildingOwner: string | null; description: string | null; + isPublicBuilding: boolean; singlePhaseVoltageV: number; threePhaseVoltageV: number; enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[]; @@ -54,6 +55,9 @@ export function ProjectSettingsModal({ const [description, setDescription] = useState( project.description ?? "" ); + const [isPublicBuilding, setIsPublicBuilding] = useState( + project.isPublicBuilding + ); const [singlePhaseVoltageV, setSinglePhaseVoltageV] = useState( String(project.singlePhaseVoltageV) ); @@ -82,6 +86,7 @@ export function ProjectSettingsModal({ externalProjectNumber: toNullableString(externalProjectNumber), buildingOwner: toNullableString(buildingOwner), description: toNullableString(description), + isPublicBuilding, singlePhaseVoltageV: Number(singlePhaseVoltageV), threePhaseVoltageV: Number(threePhaseVoltageV), enabledDistributionBoardSupplyTypes, @@ -269,6 +274,27 @@ export function ProjectSettingsModal({ value={description} />
+
+ +
+ Wird bei der späteren Leitungsauslegung berücksichtigt, + insbesondere bei der Auswahl halogenfreier Kabel und + Leitungen. +
+
+
); } diff --git a/src/frontend/components/revit-csv-modal.tsx b/src/frontend/components/revit-csv-modal.tsx new file mode 100644 index 0000000..d62e78b --- /dev/null +++ b/src/frontend/components/revit-csv-modal.tsx @@ -0,0 +1,220 @@ +"use client"; + +import { useEffect, useMemo, useState, type FormEvent } from "react"; +import { + createDefaultExternalCsvConfiguration, + type ExternalCsvConfiguration, + type ExternalCsvFamilyTypeRule, +} from "../../external-model/csv/external-csv-contracts"; +import type { ExternalCsvPreviewDto } from "../types"; +import { + getExternalCsvConfiguration, + previewExternalCsv, + updateExternalCsvConfiguration, +} from "../utils/api"; +import { FormModal } from "./form-modal"; + +interface RevitCsvModalProps { + currentRevision: number; + onClose: () => void; + onRevisionChange: (currentRevision: number) => void; + projectId: string; +} + +const defaultConfiguration = createDefaultExternalCsvConfiguration({ + ifcGuid: "IfcGUID", + roomNumber: "MEP-Raum: Nummer", + roomName: "MEP-Raum: Name", + familyAndType: "Familie und Typ", + selectionMarker: "CAx_Auswahlkenner", + circuitIdentifier: "kbp_Stromkreisnummer", + power: "kbp-E-Elektrische Leistung", + quantity: null, +}); +defaultConfiguration.additionalSourceMappings = [ + { sourceColumn: "CAx_Anmerkung", targetField: "sourceRemark" }, + { sourceColumn: "kbp-E-Spannung", targetField: "sourceVoltage" }, + { sourceColumn: "kbp-E-Stromstärke", targetField: "sourceCurrent" }, + { sourceColumn: "kbp-E-Versorgung von ELT", targetField: "sourceElectricalSupply" }, + { sourceColumn: "kbp-E-Versorgung von MSR/GLT", targetField: "sourceControlSupply" }, +]; + +const columnFields = [ + ["ifcGuid", "IFC-GUID"], + ["roomNumber", "Raumnummer"], + ["roomName", "Raumname"], + ["familyAndType", "Familie und Typ"], + ["selectionMarker", "CAx-Auswahlkenner"], + ["circuitIdentifier", "Stromkreiskennzeichnung"], + ["power", "Elektrische Leistung"], + ["quantity", "Menge (optional)"], +] as const; + +export function RevitCsvModal({ + currentRevision, + onClose, + onRevisionChange, + projectId, +}: RevitCsvModalProps) { + const [configuration, setConfiguration] = useState( + structuredClone(defaultConfiguration) + ); + const [savedConfiguration, setSavedConfiguration] = useState(null); + const [file, setFile] = useState(null); + const [preview, setPreview] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [isPreviewing, setIsPreviewing] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + getExternalCsvConfiguration(projectId) + .then(({ configuration: stored }) => { + const next = stored?.configuration ?? structuredClone(defaultConfiguration); + setConfiguration(next); + setSavedConfiguration(stored ? JSON.stringify(next) : null); + }) + .catch((reason: unknown) => + setError(reason instanceof Error ? reason.message : "Konfiguration konnte nicht geladen werden.") + ) + .finally(() => setIsLoading(false)); + }, [projectId]); + + const isDirty = savedConfiguration !== JSON.stringify(configuration); + + function replaceConfiguration(next: ExternalCsvConfiguration) { + setConfiguration(next); + setPreview(null); + } + + async function handleSave(event: FormEvent) { + event.preventDefault(); + setIsSaving(true); + setError(null); + try { + const result = await updateExternalCsvConfiguration( + projectId, + currentRevision, + configuration + ); + setSavedConfiguration(JSON.stringify(result.configuration.configuration)); + onRevisionChange(result.history.currentRevision); + } catch (reason) { + setError(reason instanceof Error ? reason.message : "Konfiguration konnte nicht gespeichert werden."); + } finally { + setIsSaving(false); + } + } + + async function handlePreview() { + if (!file || isDirty || savedConfiguration === null) return; + setIsPreviewing(true); + setError(null); + try { + const contentBase64 = await fileToBase64(file); + setPreview(await previewExternalCsv(projectId, file.name, contentBase64)); + } catch (reason) { + setPreview(null); + setError(reason instanceof Error ? reason.message : "CSV-Vorschau konnte nicht erstellt werden."); + } finally { + setIsPreviewing(false); + } + } + + function updateColumn(key: (typeof columnFields)[number][0], value: string) { + setConfiguration((current) => ({ + ...current, + columns: { ...current.columns, [key]: key === "quantity" && !value.trim() ? null : value }, + })); + setPreview(null); + } + + return ( + + {error ?
{error}
: null} + {isLoading ?

Konfiguration wird geladen …

: ( +
+
+

Transport

+
+ + + + + replaceConfiguration({ ...configuration, wattsPerSourceUnit: Number(event.target.value) })} /> +
+
+ +
+

Spaltenzuordnung

+
+ {columnFields.map(([key, label]) => ( + + updateColumn(key, event.target.value)} /> + + ))} +
+
+ + replaceConfiguration({ ...configuration, additionalSourceMappings })} /> + replaceConfiguration({ ...configuration, familyTypeRules })} /> + +
+

CSV prüfen

+

Die Vorschau speichert keine Datei und verändert keine Projektdaten.

+
+ { setFile(event.target.files?.[0] ?? null); setPreview(null); }} style={{ maxWidth: 480 }} type="file" /> + +
+ {isDirty ?
Vor der Vorschau muss die Konfiguration gespeichert werden.
: null} +
+ + {preview ? : null} +
+ )} +
+ ); +} + +function Field({ children, label }: { children: React.ReactNode; label: string }) { + return ; +} + +function AdditionalMappings({ configuration, onChange }: { configuration: ExternalCsvConfiguration; onChange: (value: ExternalCsvConfiguration["additionalSourceMappings"]) => void }) { + return

Weitere Quellfelder

{configuration.additionalSourceMappings.map((mapping, index) =>
onChange(configuration.additionalSourceMappings.map((entry, entryIndex) => entryIndex === index ? { ...entry, sourceColumn: event.target.value } : entry))} />
onChange(configuration.additionalSourceMappings.map((entry, entryIndex) => entryIndex === index ? { ...entry, targetField: event.target.value } : entry))} />
)}
; +} + +function FamilyRules({ configuration, onChange }: { configuration: ExternalCsvConfiguration; onChange: (value: ExternalCsvFamilyTypeRule[]) => void }) { + const rules = configuration.familyTypeRules; + function change(index: number, patch: Partial) { onChange(rules.map((rule, ruleIndex) => ruleIndex === index ? { ...rule, ...patch } : rule)); } + return

Familie-und-Typ-Regeln

Exakter Textvergleich; Reihenfolge wird beibehalten.
{rules.map((rule, index) =>
change(index, { exactFamilyAndType: event.target.value })} /> change(index, { internalDeviceType: event.target.value })} /> change(index, { connectionKind: event.target.value || null })} />{rule.quantityRule.kind === "fixed" ? change(index, { quantityRule: { kind: "fixed", quantity: Number(event.target.value) } })} /> : null}{rule.displayNameSuggestion?.kind === "fixed" ? change(index, { displayNameSuggestion: { kind: "fixed", value: event.target.value } })} /> : null}
)}
; +} + +function PreviewResult({ preview }: { preview: ExternalCsvPreviewDto }) { + const visibleObjects = useMemo(() => preview.objects.slice(0, 25), [preview]); + return

Prüfergebnis

SHA-256: {preview.sha256}

{visibleObjects.map((object) => )}
ZeileIFC-GUIDRaumFamilie und TypAuswahlkennerStromkreis
{object.rowNumber}{object.ifcGuid}{[object.roomNumber, object.roomName].filter(Boolean).join(" · ") || "–"}{object.familyAndType || "–"}{object.selectionMarker || "–"}{object.circuitIdentifier || "–"}
{preview.objects.length > visibleObjects.length ?

Es werden die ersten {visibleObjects.length} von {preview.objects.length} Objekten angezeigt.

: null}
; +} + +function Stat({ label, value }: { label: string; value: number }) { return
{label}
{value}
; } + +function fileToBase64(file: File) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = () => reject(new Error("CSV-Datei konnte nicht gelesen werden.")); + reader.onload = () => { + const result = String(reader.result ?? ""); + const commaIndex = result.indexOf(","); + if (commaIndex < 0) reject(new Error("CSV-Datei konnte nicht kodiert werden.")); + else resolve(result.slice(commaIndex + 1)); + }; + reader.readAsDataURL(file); + }); +} diff --git a/src/frontend/types.ts b/src/frontend/types.ts index 2fb588c..d950986 100644 --- a/src/frontend/types.ts +++ b/src/frontend/types.ts @@ -10,6 +10,46 @@ import type { ProtectionDeviceType, RcdType, } from "../shared/constants/protection-device"; +import type { + ExternalCsvConfiguration, + ExternalCsvDialect, +} from "../external-model/csv/external-csv-contracts"; + +export interface ExternalCsvConfigurationSnapshotDto { + id: string; + projectId: string; + configurationVersion: number; + configuration: ExternalCsvConfiguration; +} + +export interface ExternalCsvPreviewObjectDto { + rowNumber: number; + ifcGuid: string; + roomNumber: string; + roomName: string; + familyAndType: string; + selectionMarker: string; + circuitIdentifier: string; + power: string; + quantity: string | null; + additionalSourceValues: Record; +} + +export interface ExternalCsvPreviewDto { + fileName: string; + byteLength: number; + sha256: string; + dialect: ExternalCsvDialect; + headerRowNumber: number; + headerColumns: string[]; + rowCount: number; + objectCount: number; + passthroughCount: number; + nonEmptyPassthroughCount: number; + suspectObjectCount: number; + objects: ExternalCsvPreviewObjectDto[]; + suspectRowNumbers: number[]; +} export interface ProjectDto { id: string; diff --git a/src/frontend/utils/api.ts b/src/frontend/utils/api.ts index 4e49127..a98a56a 100644 --- a/src/frontend/utils/api.ts +++ b/src/frontend/utils/api.ts @@ -26,7 +26,10 @@ import type { ProjectDto, RoomDto, CircuitTreeResponseDto, + ExternalCsvConfigurationSnapshotDto, + ExternalCsvPreviewDto, } from "../types"; +import type { ExternalCsvConfiguration } from "../../external-model/csv/external-csv-contracts"; import type { ProjectDeviceSyncField } from "../../shared/constants/project-device-sync-fields"; import { createCircuitUpdateProjectCommand, @@ -334,6 +337,39 @@ export function updateDistributionBoard( ); } +export function getExternalCsvConfiguration(projectId: string) { + return request<{ configuration: ExternalCsvConfigurationSnapshotDto | null }>( + `/api/projects/${projectId}/external-csv/configuration` + ); +} + +export function updateExternalCsvConfiguration( + projectId: string, + expectedRevision: number, + configuration: ExternalCsvConfiguration +) { + return request< + ProjectCommandResultDto & { configuration: ExternalCsvConfigurationSnapshotDto } + >(`/api/projects/${projectId}/external-csv/configuration`, { + method: "PUT", + body: JSON.stringify({ expectedRevision, configuration }), + }); +} + +export function previewExternalCsv( + projectId: string, + fileName: string, + contentBase64: string +) { + return request( + `/api/projects/${projectId}/external-csv/preview`, + { + method: "POST", + body: JSON.stringify({ fileName, contentBase64 }), + } + ); +} + export function copyDistributionBoard( projectId: string, distributionBoardId: string, diff --git a/tests/revit-csv-frontend-api.test.ts b/tests/revit-csv-frontend-api.test.ts new file mode 100644 index 0000000..e70c71c --- /dev/null +++ b/tests/revit-csv-frontend-api.test.ts @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + getExternalCsvConfiguration, + previewExternalCsv, + updateExternalCsvConfiguration, +} from "../src/frontend/utils/api.js"; +import { createDefaultExternalCsvConfiguration } from "../src/external-model/csv/external-csv-contracts.js"; +import { externalCsvTestColumns } from "./fixtures/revit-csv-fixtures.js"; + +describe("Revit CSV frontend API", () => { + it("uses the project-scoped configuration and preview endpoints", async () => { + const requests: Array<{ url: string; method: string; body: unknown }> = []; + const originalFetch = globalThis.fetch; + globalThis.fetch = async (input, init) => { + requests.push({ + url: String(input), + method: init?.method ?? "GET", + body: init?.body ? JSON.parse(String(init.body)) : null, + }); + return new Response(JSON.stringify({ configuration: null }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }; + const configuration = createDefaultExternalCsvConfiguration(externalCsvTestColumns); + + try { + await getExternalCsvConfiguration("project-1"); + await updateExternalCsvConfiguration("project-1", 7, configuration); + await previewExternalCsv("project-1", "revit.csv", "YWJj"); + } finally { + globalThis.fetch = originalFetch; + } + + assert.deepEqual(requests, [ + { + url: "/api/projects/project-1/external-csv/configuration", + method: "GET", + body: null, + }, + { + url: "/api/projects/project-1/external-csv/configuration", + method: "PUT", + body: { expectedRevision: 7, configuration }, + }, + { + url: "/api/projects/project-1/external-csv/preview", + method: "POST", + body: { fileName: "revit.csv", contentBase64: "YWJj" }, + }, + ]); + }); +}); From d0fc1a7cafd8de75b612fe959ab1b11d779e57e5 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 2 Aug 2026 17:10:22 +0200 Subject: [PATCH 36/59] Widen Revit CSV dialog --- src/frontend/components/form-modal.tsx | 7 ++++++- src/frontend/components/revit-csv-modal.tsx | 3 ++- tests/revit-csv-frontend-api.test.ts | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/frontend/components/form-modal.tsx b/src/frontend/components/form-modal.tsx index 43aa10e..05e18eb 100644 --- a/src/frontend/components/form-modal.tsx +++ b/src/frontend/components/form-modal.tsx @@ -5,6 +5,7 @@ import React, { type FormEvent, type ReactNode } from "react"; interface FormModalProps { children: ReactNode; description?: string; + dialogSize?: "large" | "viewport"; isSaving: boolean; onClose: () => void; onSubmit: (event: FormEvent) => void; @@ -16,6 +17,7 @@ interface FormModalProps { export function FormModal({ children, description, + dialogSize = "large", isSaving, onClose, onSubmit, @@ -31,7 +33,10 @@ export function FormModal({ role="dialog" tabIndex={-1} > -
+
diff --git a/src/frontend/components/revit-csv-modal.tsx b/src/frontend/components/revit-csv-modal.tsx index d62e78b..35affc1 100644 --- a/src/frontend/components/revit-csv-modal.tsx +++ b/src/frontend/components/revit-csv-modal.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo, useState, type FormEvent } from "react"; +import React, { useEffect, useMemo, useState, type FormEvent } from "react"; import { createDefaultExternalCsvConfiguration, type ExternalCsvConfiguration, @@ -132,6 +132,7 @@ export function RevitCsvModal({ return ( { ]); }); }); + +describe("Revit CSV modal presentation", () => { + it("uses the viewport-wide dialog variant", () => { + const markup = renderToStaticMarkup( + createElement(RevitCsvModal, { + currentRevision: 0, + onClose: () => undefined, + onRevisionChange: () => undefined, + projectId: "project-1", + }) + ); + + assert.match(markup, /max-width:calc\(100vw - 2rem\)/); + assert.doesNotMatch(markup, /modal-lg/); + }); +}); From 76493b573ef2734bd6cbf791979bea7c3943819d Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 2 Aug 2026 17:18:24 +0200 Subject: [PATCH 37/59] Add external model persistence foundation --- AGENTS.md | 7 + docs/current-architecture.md | 19 +- .../spec/revit-csv-phase-14-audit-and-plan.md | 14 +- src/db/migrations/0003_outstanding_maddog.sql | 74 + src/db/migrations/meta/0003_snapshot.json | 2389 +++++++++++++++++ src/db/migrations/meta/_journal.json | 7 + .../external-model-state.repository.ts | 74 + src/db/schema/external-import-batches.ts | 43 + src/db/schema/external-model-objects.ts | 80 + src/db/schema/external-model-sources.ts | 23 + src/db/schema/external-room-mappings.ts | 37 + .../ports/external-model-state.reader.ts | 10 + .../domain/external-model-contracts.ts | 100 + .../domain/external-model-matching.ts | 147 + tests/external-model-foundation.test.ts | 191 ++ 15 files changed, 3207 insertions(+), 8 deletions(-) create mode 100644 src/db/migrations/0003_outstanding_maddog.sql create mode 100644 src/db/migrations/meta/0003_snapshot.json create mode 100644 src/db/repositories/external-model-state.repository.ts create mode 100644 src/db/schema/external-import-batches.ts create mode 100644 src/db/schema/external-model-objects.ts create mode 100644 src/db/schema/external-model-sources.ts create mode 100644 src/db/schema/external-room-mappings.ts create mode 100644 src/domain/ports/external-model-state.reader.ts create mode 100644 src/external-model/domain/external-model-contracts.ts create mode 100644 src/external-model/domain/external-model-matching.ts create mode 100644 tests/external-model-foundation.test.ts diff --git a/AGENTS.md b/AGENTS.md index 7790117..dd0a5d7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -269,6 +269,13 @@ complete transport/column/family-rule configuration through the revision-safe endpoint and requests a stateless preview. Preview responses include all mapped objects, while the UI renders only the first 25. No preview creates external objects, rows, revisions or server-side drafts. +Phase 14.2 migration `0003` provides one implicit Revit CSV source per project, +immutable import batches with original bytes plus the classified cell matrix, +normalized source-room mappings and external objects unique by +`(sourceId, ifcGuid)`. Source values, planning values and overrides are separate. +The current repository is read-only; no runtime import command writes these +tables yet, and snapshot/transfer schema remains version 3 until the next work +package adds version 4 atomically. The central revision boundary creates an automatic logical snapshot after each 25 new revisions and retains only the newest 12 automatic snapshots per project. Named snapshots are never removed by this retention policy. diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 704b540..832b354 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -500,8 +500,20 @@ einschließlich BOM, CRLF und Quotierung bytegleich zurück. Der lokale Befehl je Projekt. `external-csv-configuration.update` schreibt Anlage, Änderung oder Entfernung zusammen mit Revision und persistentem Undo/Redo. Snapshot-Schema 3, Restore und portabler Projekttransfer enthalten diesen Zustand; beim Duplizieren -werden Konfigurations-UUID und Projektlink remapped. Persistente externe Objekte -und die bestätigte Importübernahme sind noch nicht implementiert. +werden Konfigurations-UUID und Projektlink remapped. + +Migration `0003` ergänzt die relationale Grundlage für genau eine implizite +`revit_csv`-Quelle je Projekt, geordnete Importbatches, normalisierte +Quellraummappings und über `(sourceId, ifcGuid)` eindeutige externe Objekte. +Batches halten Konfigurationssnapshot, Originalbytes und die klassifizierte +Zellmatrix zusammen. Quellwerte, lokale Planungswerte und Override-Metadaten +der Objekte sind getrennt; optionale Links auf Raum, Verteilung, ProjectDevice +und CircuitDeviceRow bleiben nullable. `ExternalModelStateRepository` liest den +vollständigen Zustand deterministisch und unterscheidet unbekannte von noch +nicht importierten Projekten. Der letzte bestätigte Batch wird aus der +Batchreihenfolge abgeleitet statt redundant an der Quelle gespeichert. +Snapshot/Transfer v4, Import-Command und bestätigte Importübernahme sind noch +nicht implementiert; bloße Vorschau erzeugt weiterhin keine dieser Zeilen. `GET` und `PUT /api/projects/:projectId/external-csv/configuration` lesen oder ändern die Konfiguration; der PUT plant Identität und nächsten Konfigurationsstand serverseitig und verwendet den typisierten Command. @@ -519,7 +531,8 @@ Response verfügbar, ohne alle Zeilen in den DOM einzufügen. ## Noch nicht unterstützt - Mehrbenutzerbetrieb und Konfliktauflösung -- vollständiger Revit-/CSV-/IFCGUID-Round-trip über die vorhandene Grundlage hinaus +- bestätigter Revit-Erstimport und vollständiger CSV-/IFCGUID-Round-trip über + die vorhandene Transport- und Persistenzgrundlage hinaus - vollständige elektrische Dimensionierung - Produktionsdeployment diff --git a/docs/spec/revit-csv-phase-14-audit-and-plan.md b/docs/spec/revit-csv-phase-14-audit-and-plan.md index 0249f98..3d22b12 100644 --- a/docs/spec/revit-csv-phase-14-audit-and-plan.md +++ b/docs/spec/revit-csv-phase-14-audit-and-plan.md @@ -3,10 +3,10 @@ ## Status Phase 14.0 ist abgeschlossen und freigegeben. Dieses Dokument beschreibt den -gegen den aktuellen Code geprüften Zielzuschnitt. Phase 14.1 befindet sich in -Umsetzung; Transportverträge, Konfigurationsvalidatoren, die zustandsfreie -CSV-Transportgrundlage und die persistente projektweite Konfiguration sind -vorhanden. +gegen den aktuellen Code geprüften Zielzuschnitt. Phase 14.1 ist abgeschlossen. +Phase 14.2 hat mit den externen Domänenverträgen, Identitätsregeln und dem +additiven Persistenzschema begonnen; eine bestätigte Importübernahme existiert +noch nicht. Fachliche Quelle bleibt [Revit-CSV-Integration – Anforderungen und Implementierungsanweisung](revit-csv-integration-requirements.md). @@ -81,7 +81,8 @@ jeweilige englische Pluralform in `snake_case`. - stabile interne UUID; - `projectId`; - technischer Name und Quelltyp `revit_csv`; -- Verweis auf den zuletzt bestätigten Importbatch. +- der zuletzt bestätigte Importbatch wird aus der geordneten Batchhistorie + abgeleitet und nicht als redundanter Zeiger gespeichert. In der ersten Ausbaustufe wird genau eine Quelle je Projekt automatisch angelegt. Es gibt bewusst keine sichtbare Mehrquellenverwaltung. Die @@ -268,6 +269,9 @@ einen Commit aufgenommen. ### 14.2 – Persistente Objekte und Erstimport 1. Schema, Repositories und reine Identitäts-/Matchingregeln ergänzen. + **Erledigt:** Migration `0003`, vollständiger externer Read-State sowie + exaktes IFCGUID-, Raum- und Familie-und-Typ-Matching sind vorhanden. Es gibt + weiterhin keinen Import-Write außerhalb eines Commands. 2. Snapshot v4, Restore und Projektduplikat samt UUID-Remapping ergänzen. 3. Erstimport-Command mit echten SQLite-Commit-/Rollback-Tests ergänzen. 4. Raum-, Verteilungs-, Klassifizierungs- und ProjectDevice-Schritte im Wizard diff --git a/src/db/migrations/0003_outstanding_maddog.sql b/src/db/migrations/0003_outstanding_maddog.sql new file mode 100644 index 0000000..81f6947 --- /dev/null +++ b/src/db/migrations/0003_outstanding_maddog.sql @@ -0,0 +1,74 @@ +CREATE TABLE `external_import_batches` ( + `id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `source_id` text NOT NULL, + `import_kind` text NOT NULL, + `imported_at_iso` text NOT NULL, + `file_name` text NOT NULL, + `sha256` text NOT NULL, + `applied_project_revision` integer NOT NULL, + `configuration_snapshot` text NOT NULL, + `original_bytes` blob NOT NULL, + `document` text NOT NULL, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`source_id`) REFERENCES `external_model_sources`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `external_import_batches_project_revision_idx` ON `external_import_batches` (`project_id`,`applied_project_revision`);--> statement-breakpoint +CREATE INDEX `external_import_batches_source_imported_idx` ON `external_import_batches` (`source_id`,`imported_at_iso`);--> statement-breakpoint +CREATE TABLE `external_model_objects` ( + `id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `source_id` text NOT NULL, + `ifc_guid` text NOT NULL, + `last_seen_import_batch_id` text NOT NULL, + `last_accepted_import_batch_id` text NOT NULL, + `accepted_source_values` text NOT NULL, + `planning_values` text NOT NULL, + `overridden_fields` text NOT NULL, + `external_room_mapping_id` text, + `distribution_board_id` text, + `linked_project_device_id` text, + `circuit_device_row_id` text, + `presence_status` text DEFAULT 'present' NOT NULL, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`source_id`) REFERENCES `external_model_sources`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`last_seen_import_batch_id`) REFERENCES `external_import_batches`(`id`) ON UPDATE no action ON DELETE restrict, + FOREIGN KEY (`last_accepted_import_batch_id`) REFERENCES `external_import_batches`(`id`) ON UPDATE no action ON DELETE restrict, + FOREIGN KEY (`external_room_mapping_id`) REFERENCES `external_room_mappings`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`distribution_board_id`) REFERENCES `distribution_boards`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`linked_project_device_id`) REFERENCES `project_devices`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`circuit_device_row_id`) REFERENCES `circuit_device_rows`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE UNIQUE INDEX `external_model_objects_source_ifc_guid_unique` ON `external_model_objects` (`source_id`,`ifc_guid`);--> statement-breakpoint +CREATE INDEX `external_model_objects_project_presence_idx` ON `external_model_objects` (`project_id`,`presence_status`);--> statement-breakpoint +CREATE INDEX `external_model_objects_distribution_board_idx` ON `external_model_objects` (`distribution_board_id`);--> statement-breakpoint +CREATE INDEX `external_model_objects_circuit_device_row_idx` ON `external_model_objects` (`circuit_device_row_id`);--> statement-breakpoint +CREATE TABLE `external_model_sources` ( + `id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `name` text NOT NULL, + `source_type` text DEFAULT 'revit_csv' NOT NULL, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `external_model_sources_project_type_unique` ON `external_model_sources` (`project_id`,`source_type`);--> statement-breakpoint +CREATE TABLE `external_room_mappings` ( + `id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `source_id` text NOT NULL, + `normalized_source_room_key` text NOT NULL, + `source_floor_name` text, + `source_room_number` text NOT NULL, + `source_room_name` text NOT NULL, + `room_id` text, + `default_distribution_board_id` text, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`source_id`) REFERENCES `external_model_sources`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`room_id`) REFERENCES `rooms`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`default_distribution_board_id`) REFERENCES `distribution_boards`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE UNIQUE INDEX `external_room_mappings_source_key_unique` ON `external_room_mappings` (`source_id`,`normalized_source_room_key`);--> statement-breakpoint +CREATE INDEX `external_room_mappings_project_room_idx` ON `external_room_mappings` (`project_id`,`room_id`); \ No newline at end of file diff --git a/src/db/migrations/meta/0003_snapshot.json b/src/db/migrations/meta/0003_snapshot.json new file mode 100644 index 0000000..2b5972e --- /dev/null +++ b/src/db/migrations/meta/0003_snapshot.json @@ -0,0 +1,2389 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "6012d737-97f7-420d-a44d-85edbe5e53cf", + "prevId": "45d3523a-a37a-4c68-8fa1-abd6585108ab", + "tables": { + "circuit_device_rows": { + "name": "circuit_device_rows", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_id": { + "name": "circuit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "linked_project_device_id": { + "name": "linked_project_device_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase_type": { + "name": "phase_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_kind": { + "name": "connection_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_group": { + "name": "cost_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_number_snapshot": { + "name": "room_number_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_name_snapshot": { + "name": "room_name_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "power_per_unit": { + "name": "power_per_unit", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cos_phi": { + "name": "cos_phi", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "overridden_fields": { + "name": "overridden_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "circuit_device_rows_circuit_id_circuits_id_fk": { + "name": "circuit_device_rows_circuit_id_circuits_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "circuits", + "columnsFrom": [ + "circuit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { + "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "project_devices", + "columnsFrom": [ + "linked_project_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "circuit_device_rows_room_id_rooms_id_fk": { + "name": "circuit_device_rows_room_id_rooms_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_list_equipment_identifiers": { + "name": "circuit_list_equipment_identifiers", + "columns": { + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "circuit_list_equipment_identifiers_owner_unique": { + "name": "circuit_list_equipment_identifiers_owner_unique", + "columns": [ + "owner_type", + "owner_id" + ], + "isUnique": true + }, + "circuit_list_equipment_identifiers_list_identifier_unique": { + "name": "circuit_list_equipment_identifiers_list_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk": { + "name": "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuit_list_equipment_identifiers", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_lists": { + "name": "circuit_lists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "distribution_board_id": { + "name": "distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "circuit_lists_distribution_board_id_unique": { + "name": "circuit_lists_distribution_board_id_unique", + "columns": [ + "distribution_board_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_lists_project_id_projects_id_fk": { + "name": "circuit_lists_project_id_projects_id_fk", + "tableFrom": "circuit_lists", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuit_lists_distribution_board_id_distribution_boards_id_fk": { + "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "circuit_lists", + "tableTo": "distribution_boards", + "columnsFrom": [ + "distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_protection_devices": { + "name": "circuit_protection_devices", + "columns": { + "circuit_id": { + "name": "circuit_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rated_current_a": { + "name": "rated_current_a", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fuse_utilization_category": { + "name": "fuse_utilization_category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trip_characteristic": { + "name": "trip_characteristic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_type": { + "name": "rcd_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rated_residual_current_ma": { + "name": "rated_residual_current_ma", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "circuit_protection_devices_circuit_id_circuits_id_fk": { + "name": "circuit_protection_devices_circuit_id_circuits_id_fk", + "tableFrom": "circuit_protection_devices", + "tableTo": "circuits", + "columnsFrom": [ + "circuit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_sections": { + "name": "circuit_sections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_number": { + "name": "group_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "circuit_sections_list_key_unique": { + "name": "circuit_sections_list_key_unique", + "columns": [ + "circuit_list_id", + "key" + ], + "isUnique": true + }, + "circuit_sections_list_prefix_unique": { + "name": "circuit_sections_list_prefix_unique", + "columns": [ + "circuit_list_id", + "prefix" + ], + "isUnique": true + }, + "circuit_sections_list_category_group_unique": { + "name": "circuit_sections_list_category_group_unique", + "columns": [ + "circuit_list_id", + "category", + "group_number" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_sections_circuit_list_id_circuit_lists_id_fk": { + "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuit_sections", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuits": { + "name": "circuits", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cable_type": { + "name": "cable_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cable_cross_section": { + "name": "cable_cross_section", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cable_length": { + "name": "cable_length", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_assignment": { + "name": "rcd_assignment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_designation": { + "name": "terminal_designation", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voltage": { + "name": "voltage", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "control_requirement": { + "name": "control_requirement", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_reserve": { + "name": "is_reserve", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "circuits_list_equipment_identifier_unique": { + "name": "circuits_list_equipment_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuits_circuit_list_id_circuit_lists_id_fk": { + "name": "circuits_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuits", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuits_section_id_circuit_sections_id_fk": { + "name": "circuits_section_id_circuit_sections_id_fk", + "tableFrom": "circuits", + "tableTo": "circuit_sections", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_board_component_protection_devices": { + "name": "distribution_board_component_protection_devices", + "columns": { + "component_id": { + "name": "component_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rated_current_a": { + "name": "rated_current_a", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fuse_utilization_category": { + "name": "fuse_utilization_category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trip_characteristic": { + "name": "trip_characteristic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_type": { + "name": "rcd_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rated_residual_current_ma": { + "name": "rated_residual_current_ma", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk": { + "name": "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk", + "tableFrom": "distribution_board_component_protection_devices", + "tableTo": "distribution_board_components", + "columnsFrom": [ + "component_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_board_components": { + "name": "distribution_board_components", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "placement": { + "name": "placement", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "distribution_board_components_list_identifier_unique": { + "name": "distribution_board_components_list_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + }, + "distribution_board_components_section_role_unique": { + "name": "distribution_board_components_section_role_unique", + "columns": [ + "section_id", + "role" + ], + "isUnique": true + } + }, + "foreignKeys": { + "distribution_board_components_circuit_list_id_circuit_lists_id_fk": { + "name": "distribution_board_components_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "distribution_board_components", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "distribution_board_components_section_id_circuit_sections_id_fk": { + "name": "distribution_board_components_section_id_circuit_sections_id_fk", + "tableFrom": "distribution_board_components", + "tableTo": "circuit_sections", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_boards": { + "name": "distribution_boards", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "floor_id": { + "name": "floor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supply_type": { + "name": "supply_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "distribution_boards_project_id_projects_id_fk": { + "name": "distribution_boards_project_id_projects_id_fk", + "tableFrom": "distribution_boards", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "distribution_boards_floor_id_floors_id_fk": { + "name": "distribution_boards_floor_id_floors_id_fk", + "tableFrom": "distribution_boards", + "tableTo": "floors", + "columnsFrom": [ + "floor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_csv_configurations": { + "name": "external_csv_configurations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration_version": { + "name": "configuration_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration": { + "name": "configuration", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_csv_configurations_project_id_unique": { + "name": "external_csv_configurations_project_id_unique", + "columns": [ + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "external_csv_configurations_project_id_projects_id_fk": { + "name": "external_csv_configurations_project_id_projects_id_fk", + "tableFrom": "external_csv_configurations", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_import_batches": { + "name": "external_import_batches", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "import_kind": { + "name": "import_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imported_at_iso": { + "name": "imported_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_project_revision": { + "name": "applied_project_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration_snapshot": { + "name": "configuration_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "original_bytes": { + "name": "original_bytes", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "document": { + "name": "document", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_import_batches_project_revision_idx": { + "name": "external_import_batches_project_revision_idx", + "columns": [ + "project_id", + "applied_project_revision" + ], + "isUnique": false + }, + "external_import_batches_source_imported_idx": { + "name": "external_import_batches_source_imported_idx", + "columns": [ + "source_id", + "imported_at_iso" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_import_batches_project_id_projects_id_fk": { + "name": "external_import_batches_project_id_projects_id_fk", + "tableFrom": "external_import_batches", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_import_batches_source_id_external_model_sources_id_fk": { + "name": "external_import_batches_source_id_external_model_sources_id_fk", + "tableFrom": "external_import_batches", + "tableTo": "external_model_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_model_objects": { + "name": "external_model_objects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ifc_guid": { + "name": "ifc_guid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen_import_batch_id": { + "name": "last_seen_import_batch_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_accepted_import_batch_id": { + "name": "last_accepted_import_batch_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accepted_source_values": { + "name": "accepted_source_values", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "planning_values": { + "name": "planning_values", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "overridden_fields": { + "name": "overridden_fields", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_room_mapping_id": { + "name": "external_room_mapping_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "distribution_board_id": { + "name": "distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_project_device_id": { + "name": "linked_project_device_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "circuit_device_row_id": { + "name": "circuit_device_row_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "presence_status": { + "name": "presence_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'present'" + } + }, + "indexes": { + "external_model_objects_source_ifc_guid_unique": { + "name": "external_model_objects_source_ifc_guid_unique", + "columns": [ + "source_id", + "ifc_guid" + ], + "isUnique": true + }, + "external_model_objects_project_presence_idx": { + "name": "external_model_objects_project_presence_idx", + "columns": [ + "project_id", + "presence_status" + ], + "isUnique": false + }, + "external_model_objects_distribution_board_idx": { + "name": "external_model_objects_distribution_board_idx", + "columns": [ + "distribution_board_id" + ], + "isUnique": false + }, + "external_model_objects_circuit_device_row_idx": { + "name": "external_model_objects_circuit_device_row_idx", + "columns": [ + "circuit_device_row_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_model_objects_project_id_projects_id_fk": { + "name": "external_model_objects_project_id_projects_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_model_objects_source_id_external_model_sources_id_fk": { + "name": "external_model_objects_source_id_external_model_sources_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_model_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_model_objects_last_seen_import_batch_id_external_import_batches_id_fk": { + "name": "external_model_objects_last_seen_import_batch_id_external_import_batches_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_import_batches", + "columnsFrom": [ + "last_seen_import_batch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "external_model_objects_last_accepted_import_batch_id_external_import_batches_id_fk": { + "name": "external_model_objects_last_accepted_import_batch_id_external_import_batches_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_import_batches", + "columnsFrom": [ + "last_accepted_import_batch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "external_model_objects_external_room_mapping_id_external_room_mappings_id_fk": { + "name": "external_model_objects_external_room_mapping_id_external_room_mappings_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_room_mappings", + "columnsFrom": [ + "external_room_mapping_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_model_objects_distribution_board_id_distribution_boards_id_fk": { + "name": "external_model_objects_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "distribution_boards", + "columnsFrom": [ + "distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_model_objects_linked_project_device_id_project_devices_id_fk": { + "name": "external_model_objects_linked_project_device_id_project_devices_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "project_devices", + "columnsFrom": [ + "linked_project_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_model_objects_circuit_device_row_id_circuit_device_rows_id_fk": { + "name": "external_model_objects_circuit_device_row_id_circuit_device_rows_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "circuit_device_rows", + "columnsFrom": [ + "circuit_device_row_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_model_sources": { + "name": "external_model_sources", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'revit_csv'" + } + }, + "indexes": { + "external_model_sources_project_type_unique": { + "name": "external_model_sources_project_type_unique", + "columns": [ + "project_id", + "source_type" + ], + "isUnique": true + } + }, + "foreignKeys": { + "external_model_sources_project_id_projects_id_fk": { + "name": "external_model_sources_project_id_projects_id_fk", + "tableFrom": "external_model_sources", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_room_mappings": { + "name": "external_room_mappings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "normalized_source_room_key": { + "name": "normalized_source_room_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_floor_name": { + "name": "source_floor_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_room_number": { + "name": "source_room_number", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_room_name": { + "name": "source_room_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_distribution_board_id": { + "name": "default_distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "external_room_mappings_source_key_unique": { + "name": "external_room_mappings_source_key_unique", + "columns": [ + "source_id", + "normalized_source_room_key" + ], + "isUnique": true + }, + "external_room_mappings_project_room_idx": { + "name": "external_room_mappings_project_room_idx", + "columns": [ + "project_id", + "room_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_room_mappings_project_id_projects_id_fk": { + "name": "external_room_mappings_project_id_projects_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_room_mappings_source_id_external_model_sources_id_fk": { + "name": "external_room_mappings_source_id_external_model_sources_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "external_model_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_room_mappings_room_id_rooms_id_fk": { + "name": "external_room_mappings_room_id_rooms_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_room_mappings_default_distribution_board_id_distribution_boards_id_fk": { + "name": "external_room_mappings_default_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "distribution_boards", + "columnsFrom": [ + "default_distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "floors": { + "name": "floors", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "floors_project_id_projects_id_fk": { + "name": "floors_project_id_projects_id_fk", + "tableFrom": "floors", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "global_devices": { + "name": "global_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "installed_power_per_unit_kw": { + "name": "installed_power_per_unit_kw", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "demand_factor": { + "name": "demand_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "voltage_v": { + "name": "voltage_v", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phase_count": { + "name": "phase_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "power_factor": { + "name": "power_factor", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_change_sets": { + "name": "project_change_sets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_revision_id": { + "name": "project_revision_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_type": { + "name": "command_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_schema_version": { + "name": "payload_schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forward_payload_json": { + "name": "forward_payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inverse_payload_json": { + "name": "inverse_payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_change_sets_revision_unique": { + "name": "project_change_sets_revision_unique", + "columns": [ + "project_revision_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_change_sets_project_revision_id_project_revisions_id_fk": { + "name": "project_change_sets_project_revision_id_project_revisions_id_fk", + "tableFrom": "project_change_sets", + "tableTo": "project_revisions", + "columnsFrom": [ + "project_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_devices": { + "name": "project_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase_type": { + "name": "phase_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'single_phase'" + }, + "connection_kind": { + "name": "connection_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_group": { + "name": "cost_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "power_per_unit": { + "name": "power_per_unit", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "cos_phi": { + "name": "cos_phi", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voltage_v": { + "name": "voltage_v", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "project_devices_project_id_projects_id_fk": { + "name": "project_devices_project_id_projects_id_fk", + "tableFrom": "project_devices", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_history_stack_entries": { + "name": "project_history_stack_entries", + "columns": { + "change_set_id": { + "name": "change_set_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stack": { + "name": "stack", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_history_stack_entries_position_unique": { + "name": "project_history_stack_entries_position_unique", + "columns": [ + "project_id", + "stack", + "position" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { + "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", + "tableFrom": "project_history_stack_entries", + "tableTo": "project_change_sets", + "columnsFrom": [ + "change_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_history_stack_entries_project_id_projects_id_fk": { + "name": "project_history_stack_entries_project_id_projects_id_fk", + "tableFrom": "project_history_stack_entries", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "project_history_stack_entries_stack_check": { + "name": "project_history_stack_entries_stack_check", + "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" + } + } + }, + "project_revisions": { + "name": "project_revisions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "project_revisions_project_number_unique": { + "name": "project_revisions_project_number_unique", + "columns": [ + "project_id", + "revision_number" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_revisions_project_id_projects_id_fk": { + "name": "project_revisions_project_id_projects_id_fk", + "tableFrom": "project_revisions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_snapshots": { + "name": "project_snapshots", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_revision": { + "name": "source_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'named'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_sha256": { + "name": "payload_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "project_snapshots_project_created_idx": { + "name": "project_snapshots_project_created_idx", + "columns": [ + "project_id", + "created_at_iso" + ], + "isUnique": false + }, + "project_snapshots_project_kind_revision_idx": { + "name": "project_snapshots_project_kind_revision_idx", + "columns": [ + "project_id", + "kind", + "source_revision" + ], + "isUnique": false + }, + "project_snapshots_project_name_unique": { + "name": "project_snapshots_project_name_unique", + "columns": [ + "project_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_snapshots_project_id_projects_id_fk": { + "name": "project_snapshots_project_id_projects_id_fk", + "tableFrom": "project_snapshots", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_project_number": { + "name": "internal_project_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_project_number": { + "name": "external_project_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "building_owner": { + "name": "building_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_public_building": { + "name": "is_public_building", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "single_phase_voltage_v": { + "name": "single_phase_voltage_v", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 230 + }, + "three_phase_voltage_v": { + "name": "three_phase_voltage_v", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 400 + }, + "enabled_distribution_board_supply_types": { + "name": "enabled_distribution_board_supply_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[\"AV\",\"SV\",\"EV\",\"USV\",\"MSR\",\"SiBe\"]'" + }, + "current_revision": { + "name": "current_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rooms": { + "name": "rooms", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "floor_id": { + "name": "floor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_number": { + "name": "room_number", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "room_name": { + "name": "room_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "rooms_project_id_projects_id_fk": { + "name": "rooms_project_id_projects_id_fk", + "tableFrom": "rooms", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rooms_floor_id_floors_id_fk": { + "name": "rooms_floor_id_floors_id_fk", + "tableFrom": "rooms", + "tableTo": "floors", + "columnsFrom": [ + "floor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index d1e3295..1ac144a 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1785680771603, "tag": "0002_awesome_madripoor", "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1785683710155, + "tag": "0003_outstanding_maddog", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/repositories/external-model-state.repository.ts b/src/db/repositories/external-model-state.repository.ts new file mode 100644 index 0000000..36079c9 --- /dev/null +++ b/src/db/repositories/external-model-state.repository.ts @@ -0,0 +1,74 @@ +import { asc, eq } from "drizzle-orm"; +import type { ExternalModelStateReader } from "../../domain/ports/external-model-state.reader.js"; +import type { ExternalModelStateSnapshot } from "../../external-model/domain/external-model-contracts.js"; +import type { AppDatabase } from "../database-context.js"; +import { externalImportBatches } from "../schema/external-import-batches.js"; +import { externalModelObjects } from "../schema/external-model-objects.js"; +import { externalModelSources } from "../schema/external-model-sources.js"; +import { externalRoomMappings } from "../schema/external-room-mappings.js"; +import { projects } from "../schema/projects.js"; + +const emptyState = (): ExternalModelStateSnapshot => ({ + source: null, + importBatches: [], + roomMappings: [], + objects: [], +}); + +export class ExternalModelStateRepository implements ExternalModelStateReader { + constructor(private readonly database: AppDatabase) {} + + getByProject(projectId: string) { + const project = this.database + .select({ id: projects.id }) + .from(projects) + .where(eq(projects.id, projectId)) + .get(); + if (!project) return { projectExists: false, state: emptyState() }; + + const source = this.database + .select() + .from(externalModelSources) + .where(eq(externalModelSources.projectId, projectId)) + .get() ?? null; + if (!source) return { projectExists: true, state: emptyState() }; + + const batches = this.database + .select() + .from(externalImportBatches) + .where(eq(externalImportBatches.sourceId, source.id)) + .orderBy( + asc(externalImportBatches.appliedProjectRevision), + asc(externalImportBatches.id) + ) + .all(); + const roomMappings = this.database + .select() + .from(externalRoomMappings) + .where(eq(externalRoomMappings.sourceId, source.id)) + .orderBy( + asc(externalRoomMappings.normalizedSourceRoomKey), + asc(externalRoomMappings.id) + ) + .all(); + const objects = this.database + .select() + .from(externalModelObjects) + .where(eq(externalModelObjects.sourceId, source.id)) + .orderBy(asc(externalModelObjects.ifcGuid), asc(externalModelObjects.id)) + .all(); + + return { + projectExists: true, + state: { + source, + importBatches: batches.map(({ originalBytes, ...batch }) => ({ + ...batch, + originalContentBase64: Buffer.from(originalBytes).toString("base64"), + })), + roomMappings, + objects, + }, + }; + } +} diff --git a/src/db/schema/external-import-batches.ts b/src/db/schema/external-import-batches.ts new file mode 100644 index 0000000..bf317e9 --- /dev/null +++ b/src/db/schema/external-import-batches.ts @@ -0,0 +1,43 @@ +import { blob, index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import type { + ExternalCsvConfiguration, + ExternalCsvDocument, +} from "../../external-model/csv/external-csv-contracts.js"; +import { externalImportKinds } from "../../external-model/domain/external-model-contracts.js"; +import { externalModelSources } from "./external-model-sources.js"; +import { projects } from "./projects.js"; + +export const externalImportBatches = sqliteTable( + "external_import_batches", + { + id: text("id").primaryKey(), + projectId: text("project_id") + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + sourceId: text("source_id") + .notNull() + .references(() => externalModelSources.id, { onDelete: "cascade" }), + importKind: text("import_kind", { enum: externalImportKinds }).notNull(), + importedAtIso: text("imported_at_iso").notNull(), + fileName: text("file_name").notNull(), + sha256: text("sha256").notNull(), + appliedProjectRevision: integer("applied_project_revision").notNull(), + configurationSnapshot: text("configuration_snapshot", { mode: "json" }) + .$type() + .notNull(), + originalBytes: blob("original_bytes", { mode: "buffer" }).notNull(), + document: text("document", { mode: "json" }) + .$type() + .notNull(), + }, + (table) => [ + index("external_import_batches_project_revision_idx").on( + table.projectId, + table.appliedProjectRevision + ), + index("external_import_batches_source_imported_idx").on( + table.sourceId, + table.importedAtIso + ), + ] +); diff --git a/src/db/schema/external-model-objects.ts b/src/db/schema/external-model-objects.ts new file mode 100644 index 0000000..fbd50b7 --- /dev/null +++ b/src/db/schema/external-model-objects.ts @@ -0,0 +1,80 @@ +import { index, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; +import type { + ExternalModelObjectOverrideField, + ExternalModelObjectPlanningValues, + ExternalModelObjectSourceValues, +} from "../../external-model/domain/external-model-contracts.js"; +import { externalObjectPresenceStatuses } from "../../external-model/domain/external-model-contracts.js"; +import { circuitDeviceRows } from "./circuit-device-rows.js"; +import { distributionBoards } from "./distribution-boards.js"; +import { externalImportBatches } from "./external-import-batches.js"; +import { externalModelSources } from "./external-model-sources.js"; +import { externalRoomMappings } from "./external-room-mappings.js"; +import { projectDevices } from "./project-devices.js"; +import { projects } from "./projects.js"; + +export const externalModelObjects = sqliteTable( + "external_model_objects", + { + id: text("id").primaryKey(), + projectId: text("project_id") + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + sourceId: text("source_id") + .notNull() + .references(() => externalModelSources.id, { onDelete: "cascade" }), + ifcGuid: text("ifc_guid").notNull(), + lastSeenImportBatchId: text("last_seen_import_batch_id") + .notNull() + .references(() => externalImportBatches.id, { onDelete: "restrict" }), + lastAcceptedImportBatchId: text("last_accepted_import_batch_id") + .notNull() + .references(() => externalImportBatches.id, { onDelete: "restrict" }), + acceptedSourceValues: text("accepted_source_values", { mode: "json" }) + .$type() + .notNull(), + planningValues: text("planning_values", { mode: "json" }) + .$type() + .notNull(), + overriddenFields: text("overridden_fields", { mode: "json" }) + .$type() + .notNull(), + externalRoomMappingId: text("external_room_mapping_id").references( + () => externalRoomMappings.id, + { onDelete: "set null" } + ), + distributionBoardId: text("distribution_board_id").references( + () => distributionBoards.id, + { onDelete: "set null" } + ), + linkedProjectDeviceId: text("linked_project_device_id").references( + () => projectDevices.id, + { onDelete: "set null" } + ), + circuitDeviceRowId: text("circuit_device_row_id").references( + () => circuitDeviceRows.id, + { onDelete: "set null" } + ), + presenceStatus: text("presence_status", { + enum: externalObjectPresenceStatuses, + }) + .notNull() + .default("present"), + }, + (table) => [ + uniqueIndex("external_model_objects_source_ifc_guid_unique").on( + table.sourceId, + table.ifcGuid + ), + index("external_model_objects_project_presence_idx").on( + table.projectId, + table.presenceStatus + ), + index("external_model_objects_distribution_board_idx").on( + table.distributionBoardId + ), + index("external_model_objects_circuit_device_row_idx").on( + table.circuitDeviceRowId + ), + ] +); diff --git a/src/db/schema/external-model-sources.ts b/src/db/schema/external-model-sources.ts new file mode 100644 index 0000000..a4f9ded --- /dev/null +++ b/src/db/schema/external-model-sources.ts @@ -0,0 +1,23 @@ +import { sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; +import { externalModelSourceTypes } from "../../external-model/domain/external-model-contracts.js"; +import { projects } from "./projects.js"; + +export const externalModelSources = sqliteTable( + "external_model_sources", + { + id: text("id").primaryKey(), + projectId: text("project_id") + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + name: text("name").notNull(), + sourceType: text("source_type", { enum: externalModelSourceTypes }) + .notNull() + .default("revit_csv"), + }, + (table) => [ + uniqueIndex("external_model_sources_project_type_unique").on( + table.projectId, + table.sourceType + ), + ] +); diff --git a/src/db/schema/external-room-mappings.ts b/src/db/schema/external-room-mappings.ts new file mode 100644 index 0000000..d7cced4 --- /dev/null +++ b/src/db/schema/external-room-mappings.ts @@ -0,0 +1,37 @@ +import { index, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; +import { distributionBoards } from "./distribution-boards.js"; +import { externalModelSources } from "./external-model-sources.js"; +import { projects } from "./projects.js"; +import { rooms } from "./rooms.js"; + +export const externalRoomMappings = sqliteTable( + "external_room_mappings", + { + id: text("id").primaryKey(), + projectId: text("project_id") + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + sourceId: text("source_id") + .notNull() + .references(() => externalModelSources.id, { onDelete: "cascade" }), + normalizedSourceRoomKey: text("normalized_source_room_key").notNull(), + sourceFloorName: text("source_floor_name"), + sourceRoomNumber: text("source_room_number").notNull(), + sourceRoomName: text("source_room_name").notNull(), + roomId: text("room_id").references(() => rooms.id, { onDelete: "set null" }), + defaultDistributionBoardId: text("default_distribution_board_id").references( + () => distributionBoards.id, + { onDelete: "set null" } + ), + }, + (table) => [ + uniqueIndex("external_room_mappings_source_key_unique").on( + table.sourceId, + table.normalizedSourceRoomKey + ), + index("external_room_mappings_project_room_idx").on( + table.projectId, + table.roomId + ), + ] +); diff --git a/src/domain/ports/external-model-state.reader.ts b/src/domain/ports/external-model-state.reader.ts new file mode 100644 index 0000000..68ffa48 --- /dev/null +++ b/src/domain/ports/external-model-state.reader.ts @@ -0,0 +1,10 @@ +import type { ExternalModelStateSnapshot } from "../../external-model/domain/external-model-contracts.js"; + +export interface ExternalModelStateReadResult { + projectExists: boolean; + state: ExternalModelStateSnapshot; +} + +export interface ExternalModelStateReader { + getByProject(projectId: string): ExternalModelStateReadResult; +} diff --git a/src/external-model/domain/external-model-contracts.ts b/src/external-model/domain/external-model-contracts.ts new file mode 100644 index 0000000..e18d2e5 --- /dev/null +++ b/src/external-model/domain/external-model-contracts.ts @@ -0,0 +1,100 @@ +import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js"; +import type { + ExternalCsvConfiguration, + ExternalCsvDocument, +} from "../csv/external-csv-contracts.js"; + +export const externalModelSourceTypes = ["revit_csv"] as const; +export type ExternalModelSourceType = (typeof externalModelSourceTypes)[number]; + +export const externalImportKinds = ["initial", "follow_up"] as const; +export type ExternalImportKind = (typeof externalImportKinds)[number]; + +export const externalObjectPresenceStatuses = ["present", "missing"] as const; +export type ExternalObjectPresenceStatus = + (typeof externalObjectPresenceStatuses)[number]; + +export interface ExternalModelSourceSnapshot { + id: string; + projectId: string; + name: string; + sourceType: ExternalModelSourceType; +} + +export interface ExternalImportBatchSnapshot { + id: string; + projectId: string; + sourceId: string; + importKind: ExternalImportKind; + importedAtIso: string; + fileName: string; + sha256: string; + appliedProjectRevision: number; + configurationSnapshot: ExternalCsvConfiguration; + originalContentBase64: string; + document: ExternalCsvDocument; +} + +export interface ExternalRoomMappingSnapshot { + id: string; + projectId: string; + sourceId: string; + normalizedSourceRoomKey: string; + sourceFloorName: string | null; + sourceRoomNumber: string; + sourceRoomName: string; + roomId: string | null; + defaultDistributionBoardId: string | null; +} + +export interface ExternalModelObjectSourceValues { + rowNumber: number; + roomNumber: string; + roomName: string; + familyAndType: string; + selectionMarker: string; + circuitIdentifier: string; + power: string; + quantity: string | null; + additionalSourceValues: Record; +} + +export interface ExternalModelObjectPlanningValues { + displayName: string | null; + internalDeviceType: string | null; + category: CircuitGroupCategory | null; + connectionKind: string | null; + effectiveQuantity: number; + powerPerUnitW: number | null; + simultaneityFactor: number; + cosPhi: number | null; + costGroup: string | null; + remark: string | null; +} + +export type ExternalModelObjectOverrideField = + keyof ExternalModelObjectPlanningValues; + +export interface ExternalModelObjectSnapshot { + id: string; + projectId: string; + sourceId: string; + ifcGuid: string; + lastSeenImportBatchId: string; + lastAcceptedImportBatchId: string; + acceptedSourceValues: ExternalModelObjectSourceValues; + planningValues: ExternalModelObjectPlanningValues; + overriddenFields: ExternalModelObjectOverrideField[]; + externalRoomMappingId: string | null; + distributionBoardId: string | null; + linkedProjectDeviceId: string | null; + circuitDeviceRowId: string | null; + presenceStatus: ExternalObjectPresenceStatus; +} + +export interface ExternalModelStateSnapshot { + source: ExternalModelSourceSnapshot | null; + importBatches: ExternalImportBatchSnapshot[]; + roomMappings: ExternalRoomMappingSnapshot[]; + objects: ExternalModelObjectSnapshot[]; +} diff --git a/src/external-model/domain/external-model-matching.ts b/src/external-model/domain/external-model-matching.ts new file mode 100644 index 0000000..3085211 --- /dev/null +++ b/src/external-model/domain/external-model-matching.ts @@ -0,0 +1,147 @@ +import type { ExternalCsvConfiguration } from "../csv/external-csv-contracts.js"; +import type { + ExternalModelObjectPlanningValues, + ExternalModelObjectSourceValues, +} from "./external-model-contracts.js"; + +export type InitialExternalObjectIssue = + | "unknown-family-and-type" + | "invalid-power" + | "invalid-quantity" + | "missing-room"; + +export interface InitialExternalObjectProjection { + ifcGuid: string; + sourceValues: ExternalModelObjectSourceValues; + planningValues: ExternalModelObjectPlanningValues; + issues: InitialExternalObjectIssue[]; +} + +export interface ExternalObjectImportCandidate { + rowNumber: number; + ifcGuid: string; + roomNumber: string; + roomName: string; + familyAndType: string; + selectionMarker: string; + circuitIdentifier: string; + power: string; + quantity: string | null; + additionalSourceValues: Record; +} + +export function normalizeExternalRoomPart(value: string): string { + return value.trim().normalize("NFKC").toLocaleUpperCase("de-DE"); +} + +export function createExternalRoomKey( + roomNumber: string, + roomName: string +): string | null { + const normalizedNumber = normalizeExternalRoomPart(roomNumber); + if (normalizedNumber) return `number:${normalizedNumber}`; + const normalizedName = normalizeExternalRoomPart(roomName); + return normalizedName ? `name:${normalizedName}` : null; +} + +export function indexExternalObjectsByIfcGuid( + objects: readonly T[] +): Map { + const index = new Map(); + for (const object of objects) { + if (!object.ifcGuid || object.ifcGuid !== object.ifcGuid.trim()) { + throw new Error("IfcGUID must be non-empty and must not contain outer whitespace."); + } + if (index.has(object.ifcGuid)) { + throw new Error(`Duplicate IfcGUID: ${object.ifcGuid}`); + } + index.set(object.ifcGuid, object); + } + return index; +} + +export function projectInitialExternalObject( + object: ExternalObjectImportCandidate, + configuration: ExternalCsvConfiguration +): InitialExternalObjectProjection { + const rule = configuration.familyTypeRules.find( + (candidate) => candidate.exactFamilyAndType === object.familyAndType + ); + const issues: InitialExternalObjectIssue[] = []; + if (!rule) issues.push("unknown-family-and-type"); + if (!createExternalRoomKey(object.roomNumber, object.roomName)) { + issues.push("missing-room"); + } + + const parsedPower = parseConfiguredNumber(object.power, configuration); + if (object.power.trim() && parsedPower === null) issues.push("invalid-power"); + + let effectiveQuantity = 1; + if (rule?.quantityRule.kind === "fixed") { + effectiveQuantity = rule.quantityRule.quantity; + } else if (rule?.quantityRule.kind === "mapped-column") { + const parsedQuantity = parseConfiguredNumber(object.quantity ?? "", configuration); + if (parsedQuantity === null || !Number.isInteger(parsedQuantity) || parsedQuantity <= 0) { + issues.push("invalid-quantity"); + } else { + effectiveQuantity = parsedQuantity; + } + } + + return { + ifcGuid: object.ifcGuid, + sourceValues: { + rowNumber: object.rowNumber, + roomNumber: object.roomNumber, + roomName: object.roomName, + familyAndType: object.familyAndType, + selectionMarker: object.selectionMarker, + circuitIdentifier: object.circuitIdentifier, + power: object.power, + quantity: object.quantity, + additionalSourceValues: { ...object.additionalSourceValues }, + }, + planningValues: { + displayName: rule + ? resolveDisplayName(rule.displayNameSuggestion, object) + : null, + internalDeviceType: rule?.internalDeviceType ?? null, + category: rule?.category ?? null, + connectionKind: rule?.connectionKind ?? null, + effectiveQuantity, + powerPerUnitW: + parsedPower === null ? null : parsedPower * configuration.wattsPerSourceUnit, + simultaneityFactor: 1, + cosPhi: null, + costGroup: null, + remark: null, + }, + issues, + }; +} + +function parseConfiguredNumber( + value: string, + configuration: ExternalCsvConfiguration +): number | null { + const trimmed = value.trim(); + if (!trimmed) return null; + const normalized = configuration.decimalSeparator === "," + ? trimmed.replace(",", ".") + : trimmed; + if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(normalized)) return null; + const parsed = Number(normalized); + return Number.isFinite(parsed) && parsed >= 0 ? parsed : null; +} + +function resolveDisplayName( + suggestion: ExternalCsvConfiguration["familyTypeRules"][number]["displayNameSuggestion"], + object: ExternalObjectImportCandidate +): string | null { + if (suggestion === null) return null; + if (suggestion.kind === "fixed") return suggestion.value; + if (suggestion.kind === "selection-marker") { + return object.selectionMarker.trim() || null; + } + return object.familyAndType.trim() || null; +} diff --git a/tests/external-model-foundation.test.ts b/tests/external-model-foundation.test.ts new file mode 100644 index 0000000..aa67c0a --- /dev/null +++ b/tests/external-model-foundation.test.ts @@ -0,0 +1,191 @@ +import path from "node:path"; +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { migrate } from "drizzle-orm/better-sqlite3/migrator"; +import { createDatabaseContext } from "../src/db/database-context.js"; +import { ExternalModelStateRepository } from "../src/db/repositories/external-model-state.repository.js"; +import { externalImportBatches } from "../src/db/schema/external-import-batches.js"; +import { externalModelObjects } from "../src/db/schema/external-model-objects.js"; +import { externalModelSources } from "../src/db/schema/external-model-sources.js"; +import { externalRoomMappings } from "../src/db/schema/external-room-mappings.js"; +import { projects } from "../src/db/schema/projects.js"; +import { createExternalCsvPreview } from "../src/external-model/application/external-csv-preview.js"; +import { parseExternalCsv } from "../src/external-model/csv/external-csv-transport.js"; +import { + createExternalRoomKey, + indexExternalObjectsByIfcGuid, + projectInitialExternalObject, +} from "../src/external-model/domain/external-model-matching.js"; +import { + externalCsvTestConfiguration, + firstIfcGuid, + quotedRevitCsv, + utf8Bytes, +} from "./fixtures/revit-csv-fixtures.js"; + +function configuredCsv() { + const configuration = structuredClone(externalCsvTestConfiguration); + configuration.familyTypeRules.push({ + exactFamilyAndType: "Steckdose: Doppelsteckdose", + internalDeviceType: "Steckdose", + connectionKind: "socket", + category: "single_phase", + quantityRule: { kind: "mapped-column" }, + displayNameSuggestion: { kind: "selection-marker" }, + }); + return configuration; +} + +describe("external model matching", () => { + it("normalizes source rooms and rejects duplicate IFC identities", () => { + assert.equal(createExternalRoomKey(" 01/101 ", "Technik"), "number:01/101"); + assert.equal(createExternalRoomKey("", " Büro Nord "), "name:BÜRO NORD"); + assert.equal(createExternalRoomKey(" ", " "), null); + assert.throws( + () => indexExternalObjectsByIfcGuid([{ ifcGuid: firstIfcGuid }, { ifcGuid: firstIfcGuid }]), + /Duplicate IfcGUID/ + ); + }); + + it("derives initial planning values from an exact family rule", () => { + const configuration = configuredCsv(); + const preview = createExternalCsvPreview({ + fileName: "revit.csv", + bytes: utf8Bytes(quotedRevitCsv, true), + configuration, + }); + const projected = projectInitialExternalObject(preview.objects[0], configuration); + + assert.deepEqual(projected.issues, []); + assert.equal(projected.ifcGuid, firstIfcGuid); + assert.equal(projected.planningValues.displayName, "Arbeitsplatz"); + assert.equal(projected.planningValues.category, "single_phase"); + assert.equal(projected.planningValues.effectiveQuantity, 2); + assert.equal(projected.planningValues.powerPerUnitW, 120.5); + }); + + it("keeps unclassified or invalid values visible as issues", () => { + const projected = projectInitialExternalObject( + { + rowNumber: 4, + ifcGuid: firstIfcGuid, + roomNumber: "", + roomName: "", + familyAndType: "Unbekannt", + selectionMarker: "", + circuitIdentifier: "", + power: "12,3,4", + quantity: "0", + additionalSourceValues: {}, + }, + externalCsvTestConfiguration + ); + + assert.deepEqual(projected.issues, [ + "unknown-family-and-type", + "missing-room", + "invalid-power", + ]); + assert.equal(projected.planningValues.powerPerUnitW, null); + }); +}); + +describe("external model state repository", () => { + it("reads a deterministic complete external state without changing project data", () => { + const context = createDatabaseContext(":memory:"); + migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations") }); + try { + context.db.insert(projects).values({ id: "project-1", name: "Projekt" }).run(); + context.db.insert(externalModelSources).values({ + id: "source-1", + projectId: "project-1", + name: "Revit-Gesamtmodell", + sourceType: "revit_csv", + }).run(); + const configuration = configuredCsv(); + const bytes = utf8Bytes(quotedRevitCsv, true); + const document = parseExternalCsv(bytes, configuration); + context.db.insert(externalImportBatches).values({ + id: "batch-1", + projectId: "project-1", + sourceId: "source-1", + importKind: "initial", + importedAtIso: "2026-08-02T15:30:00.000Z", + fileName: "revit.csv", + sha256: "hash", + appliedProjectRevision: 1, + configurationSnapshot: configuration, + originalBytes: Buffer.from(bytes), + document, + }).run(); + context.db.insert(externalRoomMappings).values({ + id: "mapping-1", + projectId: "project-1", + sourceId: "source-1", + normalizedSourceRoomKey: "number:01/101", + sourceFloorName: null, + sourceRoomNumber: "01/101", + sourceRoomName: "Technik", + roomId: null, + defaultDistributionBoardId: null, + }).run(); + const preview = createExternalCsvPreview({ + fileName: "revit.csv", + bytes, + configuration, + }); + const projected = projectInitialExternalObject(preview.objects[0], configuration); + context.db.insert(externalModelObjects).values({ + id: "object-1", + projectId: "project-1", + sourceId: "source-1", + ifcGuid: projected.ifcGuid, + lastSeenImportBatchId: "batch-1", + lastAcceptedImportBatchId: "batch-1", + acceptedSourceValues: projected.sourceValues, + planningValues: projected.planningValues, + overriddenFields: [], + externalRoomMappingId: "mapping-1", + distributionBoardId: null, + linkedProjectDeviceId: null, + circuitDeviceRowId: null, + presenceStatus: "present", + }).run(); + + const result = new ExternalModelStateRepository(context.db).getByProject("project-1"); + assert.equal(result.projectExists, true); + assert.equal(result.state.source?.id, "source-1"); + assert.equal(result.state.importBatches[0].originalContentBase64, Buffer.from(bytes).toString("base64")); + assert.equal(result.state.roomMappings[0].normalizedSourceRoomKey, "number:01/101"); + assert.equal(result.state.objects[0].ifcGuid, firstIfcGuid); + assert.equal(result.state.objects[0].circuitDeviceRowId, null); + assert.equal(result.state.objects[0].planningValues.effectiveQuantity, 2); + + assert.throws( + () => context.db.insert(externalModelObjects).values({ + ...context.db.select().from(externalModelObjects).get()!, + id: "object-2", + }).run(), + /UNIQUE constraint failed/ + ); + } finally { + context.close(); + } + }); + + it("distinguishes an empty project from an unknown project", () => { + const context = createDatabaseContext(":memory:"); + migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations") }); + try { + context.db.insert(projects).values({ id: "project-1", name: "Projekt" }).run(); + const repository = new ExternalModelStateRepository(context.db); + assert.deepEqual(repository.getByProject("project-1"), { + projectExists: true, + state: { source: null, importBatches: [], roomMappings: [], objects: [] }, + }); + assert.equal(repository.getByProject("missing").projectExists, false); + } finally { + context.close(); + } + }); +}); From e87a3289fa45634ac3cb6df9423f0f695c381132 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 2 Aug 2026 17:25:25 +0200 Subject: [PATCH 38/59] Include external models in project snapshots --- AGENTS.md | 15 +- docs/current-architecture.md | 19 +- .../spec/revit-csv-phase-14-audit-and-plan.md | 4 + ...roject-state-restore-command.repository.ts | 29 ++ .../project-state-snapshot.persistence.ts | 4 + .../project-transfer.repository.ts | 19 ++ .../models/project-state-snapshot.model.ts | 273 +++++++++++++++++- src/domain/models/project-transfer.model.ts | 69 +++++ tests/external-model-foundation.test.ts | 237 +++++++++++++++ tests/project-state-snapshot.test.ts | 31 ++ 10 files changed, 685 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index dd0a5d7..a08ca7c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -250,9 +250,11 @@ not change the project revision or undo/redo stacks. Restoring a server-stored snapshot verifies its checksum and the current-state hash, replaces supported project data atomically and records a new `restore` revision with a complete inverse command. Restore can therefore be undone and redone after a restart. -Current snapshot schema version 3 additionally stores the optional project-wide -Revit CSV configuration. Version 2 snapshots are upgraded with an empty external -configuration. Supported baseline version 1 snapshots are additionally upgraded +Current snapshot schema version 4 additionally stores the complete confirmed +external-model source, import batches, room mappings and objects. Version 3 +snapshots are upgraded with an empty external-model state; version 2 snapshots +are additionally upgraded with an empty external CSV configuration. Supported +baseline version 1 snapshots are additionally upgraded with `isPublicBuilding = false`; pre-baseline snapshots remain unsupported. All supported versions contain circuit-group identity, distribution-board components and the separate circuit/component protection records. Portable duplicate imports @@ -261,7 +263,7 @@ project graph. Project-wide Revit CSV configuration uses `external-csv-configuration.update`. Its complete expected/target snapshot is persisted through the shared project-command transaction, so create, update, -delete, Undo and Redo are revision-safe. Snapshot schema 3, restore and portable +delete, Undo and Redo are revision-safe. Snapshot schema 4, restore and portable project duplication preserve the configuration; duplication remaps its internal id and project link. The project page exposes the current Phase 14.1 Revit CSV modal. It edits the @@ -274,8 +276,9 @@ immutable import batches with original bytes plus the classified cell matrix, normalized source-room mappings and external objects unique by `(sourceId, ifcGuid)`. Source values, planning values and overrides are separate. The current repository is read-only; no runtime import command writes these -tables yet, and snapshot/transfer schema remains version 3 until the next work -package adds version 4 atomically. +tables yet. Snapshot/restore and portable project transfer schema version 4 +capture the complete external state and remap every internal UUID and link while +preserving IFCGUIDs, source values, original bytes and the classified matrix. The central revision boundary creates an automatic logical snapshot after each 25 new revisions and retains only the newest 12 automatic snapshots per project. Named snapshots are never removed by this retention policy. diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 832b354..243503d 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -416,13 +416,15 @@ wieder her. verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und ist für bestehende sowie neu angelegte Verteilungen standardmäßig `1`. -Snapshot-Schema 3 enthält zusätzlich die projektweite Revit-CSV-Konfiguration, -die Gebäudekennzeichnung sowie den Gleichzeitigkeitsfaktor, +Snapshot-Schema 4 enthält zusätzlich den vollständigen externen Modellzustand +aus Quelle, Batches, Raum-Mappings und Objekten. Es enthält außerdem die +projektweite Revit-CSV-Konfiguration, die Gebäudekennzeichnung sowie den Gleichzeitigkeitsfaktor, Gruppenkategorie und -nummer, Verteilerkomponenten sowie die getrennten 1:1-Schutzgerätedaten. Capture, benannte und automatische Snapshots, Wiederherstellung, Undo/Redo und beide JSON-Importmodi verwenden denselben vollständigen Zustand. Die unterstützte -Version 2 wird mit leerer externer Konfiguration hochgestuft. Die unterstützte +Version 3 wird mit leerem externem Modellzustand und Version 2 zusätzlich mit +leerer externer Konfiguration hochgestuft. Die unterstützte Baseline-Version 1 wird zusätzlich mit `isPublicBuilding = false` hochgestuft; Formate vor dieser Baseline werden nicht eingelesen. Persistente Insert-, Update- und Delete-Commands versionieren Anlage, @@ -475,7 +477,7 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät. Ein triggergeführtes Register erzwingt bereits eine normalisierte, stromkreislistenweite BMK-Eindeutigkeit über Stromkreise und Verteilerkomponenten. Snapshot- und Transfer-Integration verwenden aktuell - Snapshot-Schema 3. Persistente Insert/Delete/Update-Commands für + Snapshot-Schema 4. Persistente Insert/Delete/Update-Commands für veränderliche Verteilerkomponenten, Gruppen einschließlich befüllter Unterbäume sowie vollständige Gruppensortierung sind integriert. Der Editor zeigt die geschützte Struktur an und bearbeitet veränderliche Gruppen- und @@ -498,7 +500,7 @@ einschließlich BOM, CRLF und Quotierung bytegleich zurück. Der lokale Befehl `external_csv_configurations` enthält höchstens eine versionierte Konfiguration je Projekt. `external-csv-configuration.update` schreibt Anlage, Änderung oder -Entfernung zusammen mit Revision und persistentem Undo/Redo. Snapshot-Schema 3, +Entfernung zusammen mit Revision und persistentem Undo/Redo. Snapshot-Schema 4, Restore und portabler Projekttransfer enthalten diesen Zustand; beim Duplizieren werden Konfigurations-UUID und Projektlink remapped. @@ -512,8 +514,11 @@ und CircuitDeviceRow bleiben nullable. `ExternalModelStateRepository` liest den vollständigen Zustand deterministisch und unterscheidet unbekannte von noch nicht importierten Projekten. Der letzte bestätigte Batch wird aus der Batchreihenfolge abgeleitet statt redundant an der Quelle gespeichert. -Snapshot/Transfer v4, Import-Command und bestätigte Importübernahme sind noch -nicht implementiert; bloße Vorschau erzeugt weiterhin keine dieser Zeilen. +Snapshot/Transfer v4 erfasst Originalbytes, Matrix und alle internen Links, +stellt sie in Fremdschlüsselreihenfolge wieder her und remappt beim +Projektduplikat jede interne UUID; IFCGUID und Quelltransport bleiben +unverändert. Import-Command und bestätigte Importübernahme sind noch nicht +implementiert; bloße Vorschau erzeugt weiterhin keine dieser Zeilen. `GET` und `PUT /api/projects/:projectId/external-csv/configuration` lesen oder ändern die Konfiguration; der PUT plant Identität und nächsten Konfigurationsstand serverseitig und verwendet den typisierten Command. diff --git a/docs/spec/revit-csv-phase-14-audit-and-plan.md b/docs/spec/revit-csv-phase-14-audit-and-plan.md index 3d22b12..4570a5a 100644 --- a/docs/spec/revit-csv-phase-14-audit-and-plan.md +++ b/docs/spec/revit-csv-phase-14-audit-and-plan.md @@ -273,6 +273,10 @@ einen Commit aufgenommen. exaktes IFCGUID-, Raum- und Familie-und-Typ-Matching sind vorhanden. Es gibt weiterhin keinen Import-Write außerhalb eines Commands. 2. Snapshot v4, Restore und Projektduplikat samt UUID-Remapping ergänzen. + **Erledigt:** Version 3 wird mit leerem externem Zustand hochgestuft; + Capture, Restore, Undo/Redo und beide Transfermodi erhalten Originalbytes, + Matrix und sämtliche internen Links. Duplikate remappen alle internen UUIDs, + lassen IFCGUID und Quelltransport jedoch unverändert. 3. Erstimport-Command mit echten SQLite-Commit-/Rollback-Tests ergänzen. 4. Raum-, Verteilungs-, Klassifizierungs- und ProjectDevice-Schritte im Wizard ergänzen; der Import selbst erzeugt keine CircuitDeviceRow. diff --git a/src/db/repositories/project-state-restore-command.repository.ts b/src/db/repositories/project-state-restore-command.repository.ts index 34422b3..dcae184 100644 --- a/src/db/repositories/project-state-restore-command.repository.ts +++ b/src/db/repositories/project-state-restore-command.repository.ts @@ -27,6 +27,10 @@ import { projectChangeSets } from "../schema/project-change-sets.js"; import { projects } from "../schema/projects.js"; import { rooms } from "../schema/rooms.js"; import { externalCsvConfigurations } from "../schema/external-csv-configurations.js"; +import { externalImportBatches } from "../schema/external-import-batches.js"; +import { externalModelObjects } from "../schema/external-model-objects.js"; +import { externalModelSources } from "../schema/external-model-sources.js"; +import { externalRoomMappings } from "../schema/external-room-mappings.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; import { readProjectStateSnapshot, @@ -163,6 +167,12 @@ function canonicalProjectState(state: ProjectStateSnapshot) { floors: byId(state.floors), rooms: byId(state.rooms), externalCsvConfiguration: state.externalCsvConfiguration, + externalModel: { + source: state.externalModel.source, + importBatches: byId(state.externalModel.importBatches), + roomMappings: byId(state.externalModel.roomMappings), + objects: byId(state.externalModel.objects), + }, }); } @@ -185,6 +195,10 @@ function replaceProjectState( database: AppDatabase, state: ProjectStateSnapshot ) { + database + .delete(externalModelSources) + .where(eq(externalModelSources.projectId, state.project.id)) + .run(); database .delete(externalCsvConfigurations) .where(eq(externalCsvConfigurations.projectId, state.project.id)) @@ -263,6 +277,21 @@ function replaceProjectState( circuitDeviceRows, state.circuits.flatMap((circuit) => circuit.deviceRows) ); + if (state.externalModel.source !== null) { + database.insert(externalModelSources).values(state.externalModel.source).run(); + } + insertMany( + database, + externalImportBatches, + state.externalModel.importBatches.map( + ({ originalContentBase64, ...batch }) => ({ + ...batch, + originalBytes: Buffer.from(originalContentBase64, "base64"), + }) + ) + ); + insertMany(database, externalRoomMappings, state.externalModel.roomMappings); + insertMany(database, externalModelObjects, state.externalModel.objects); } function insertMany[0]>( diff --git a/src/db/repositories/project-state-snapshot.persistence.ts b/src/db/repositories/project-state-snapshot.persistence.ts index 03f35ab..1fb96da 100644 --- a/src/db/repositories/project-state-snapshot.persistence.ts +++ b/src/db/repositories/project-state-snapshot.persistence.ts @@ -20,6 +20,7 @@ import { projectDevices } from "../schema/project-devices.js"; import { projects } from "../schema/projects.js"; import { rooms } from "../schema/rooms.js"; import { externalCsvConfigurations } from "../schema/external-csv-configurations.js"; +import { ExternalModelStateRepository } from "./external-model-state.repository.js"; export interface PersistedProjectStateSnapshot { currentRevision: number; @@ -65,6 +66,8 @@ export function readProjectStateSnapshot( .from(externalCsvConfigurations) .where(eq(externalCsvConfigurations.projectId, projectId)) .get() ?? null; + const externalModel = new ExternalModelStateRepository(database) + .getByProject(projectId).state; const listRows = database .select() .from(circuitLists) @@ -248,6 +251,7 @@ export function readProjectStateSnapshot( }, distributionBoards: boardRows, externalCsvConfiguration, + externalModel, circuitLists: listRows, circuitSections: sectionRows, distributionBoardComponents: componentRows, diff --git a/src/db/repositories/project-transfer.repository.ts b/src/db/repositories/project-transfer.repository.ts index 8bb7274..07a5067 100644 --- a/src/db/repositories/project-transfer.repository.ts +++ b/src/db/repositories/project-transfer.repository.ts @@ -21,6 +21,10 @@ import { projectDevices } from "../schema/project-devices.js"; import { projects } from "../schema/projects.js"; import { rooms } from "../schema/rooms.js"; import { externalCsvConfigurations } from "../schema/external-csv-configurations.js"; +import { externalImportBatches } from "../schema/external-import-batches.js"; +import { externalModelObjects } from "../schema/external-model-objects.js"; +import { externalModelSources } from "../schema/external-model-sources.js"; +import { externalRoomMappings } from "../schema/external-room-mappings.js"; import { hashProjectStatePayload, readProjectStateSnapshot, @@ -157,6 +161,21 @@ function insertProjectState( circuitDeviceRows, state.circuits.flatMap((circuit) => circuit.deviceRows) ); + if (state.externalModel.source !== null) { + database.insert(externalModelSources).values(state.externalModel.source).run(); + } + insertMany( + database, + externalImportBatches, + state.externalModel.importBatches.map( + ({ originalContentBase64, ...batch }) => ({ + ...batch, + originalBytes: Buffer.from(originalContentBase64, "base64"), + }) + ) + ); + insertMany(database, externalRoomMappings, state.externalModel.roomMappings); + insertMany(database, externalModelObjects, state.externalModel.objects); } function insertMany[0]>( diff --git a/src/domain/models/project-state-snapshot.model.ts b/src/domain/models/project-state-snapshot.model.ts index 8dc2e7e..e0ee77f 100644 --- a/src/domain/models/project-state-snapshot.model.ts +++ b/src/domain/models/project-state-snapshot.model.ts @@ -20,12 +20,22 @@ import { } from "../services/project-voltage.service.js"; import { validateExternalCsvConfiguration } from "../../external-model/csv/external-csv-configuration.js"; import type { ExternalCsvConfiguration } from "../../external-model/csv/external-csv-contracts.js"; +import { + externalImportKinds, + externalModelSourceTypes, + externalObjectPresenceStatuses, +} from "../../external-model/domain/external-model-contracts.js"; -export const projectStateSnapshotSchemaVersion = 3 as const; -const previousProjectStateSnapshotSchemaVersion = 2 as const; +export const projectStateSnapshotSchemaVersion = 4 as const; +const previousProjectStateSnapshotSchemaVersion = 3 as const; +const legacyProjectStateSnapshotSchemaVersion = 2 as const; const baselineProjectStateSnapshotSchemaVersion = 1 as const; const idSchema = z.string().trim().min(1); +const externalIfcGuidSchema = z.string().min(1).refine( + (value) => value === value.trim(), + "Snapshot IFCGUID must not contain outer whitespace." +); const nullableStringSchema = z.string().nullable(); const finiteNumberSchema = z.number().finite(); @@ -263,6 +273,147 @@ const externalCsvConfigurationSnapshotSchema = z }) .strict(); +const externalCsvDialectSchema = z.object({ + encoding: z.literal("utf-8"), + hasBom: z.boolean(), + delimiter: z.enum([";", ",", "\t"]), + lineEnding: z.enum(["\r\n", "\n", "\r"]), + quoteCharacter: z.literal('"'), + quoteAllFields: z.boolean(), + hasTrailingLineEnding: z.boolean(), +}).strict(); + +const externalCsvDocumentSchema = z.object({ + dialect: externalCsvDialectSchema, + headerRowIndex: z.number().int().nonnegative(), + rows: z.array(z.object({ + index: z.number().int().nonnegative(), + cells: z.array(z.object({ + value: z.string(), + wasQuoted: z.boolean(), + }).strict()), + classification: z.enum([ + "metadata", + "header", + "passthrough", + "object", + "suspect-object", + ]), + }).strict()).min(1), +}).strict().superRefine((document, context) => { + if (document.rows[document.headerRowIndex]?.classification !== "header") { + context.addIssue({ + code: "custom", + message: "Snapshot external CSV document header is invalid.", + }); + } + document.rows.forEach((row, index) => { + if (row.index !== index) { + context.addIssue({ + code: "custom", + message: "Snapshot external CSV row indexes must be contiguous.", + }); + } + }); +}); + +const externalModelSourceSchema = z.object({ + id: idSchema, + projectId: idSchema, + name: z.string().trim().min(1), + sourceType: z.enum(externalModelSourceTypes), +}).strict(); + +const externalImportBatchSchema = z.object({ + id: idSchema, + projectId: idSchema, + sourceId: idSchema, + importKind: z.enum(externalImportKinds), + importedAtIso: z.string().refine((value) => Number.isFinite(Date.parse(value))), + fileName: z.string().trim().min(1), + sha256: z.string().regex(/^[a-f0-9]{64}$/), + appliedProjectRevision: z.number().int().nonnegative(), + configurationSnapshot: z.custom( + (value) => validateExternalCsvConfiguration(value).success + ), + originalContentBase64: z.string().min(1).regex( + /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/ + ), + document: externalCsvDocumentSchema, +}).strict(); + +const externalRoomMappingSchema = z.object({ + id: idSchema, + projectId: idSchema, + sourceId: idSchema, + normalizedSourceRoomKey: z.string().trim().min(1), + sourceFloorName: nullableStringSchema, + sourceRoomNumber: z.string(), + sourceRoomName: z.string(), + roomId: idSchema.nullable(), + defaultDistributionBoardId: idSchema.nullable(), +}).strict(); + +const externalPlanningFieldNames = [ + "displayName", + "internalDeviceType", + "category", + "connectionKind", + "effectiveQuantity", + "powerPerUnitW", + "simultaneityFactor", + "cosPhi", + "costGroup", + "remark", +] as const; + +const externalModelObjectSchema = z.object({ + id: idSchema, + projectId: idSchema, + sourceId: idSchema, + ifcGuid: externalIfcGuidSchema, + lastSeenImportBatchId: idSchema, + lastAcceptedImportBatchId: idSchema, + acceptedSourceValues: z.object({ + rowNumber: z.number().int().positive(), + roomNumber: z.string(), + roomName: z.string(), + familyAndType: z.string(), + selectionMarker: z.string(), + circuitIdentifier: z.string(), + power: z.string(), + quantity: z.string().nullable(), + additionalSourceValues: z.record(z.string(), z.string()), + }).strict(), + planningValues: z.object({ + displayName: nullableStringSchema, + internalDeviceType: nullableStringSchema, + category: z.enum(circuitGroupCategories).nullable(), + connectionKind: nullableStringSchema, + effectiveQuantity: z.number().int().positive(), + powerPerUnitW: finiteNumberSchema.nonnegative().nullable(), + simultaneityFactor: finiteNumberSchema.min(0).max(1), + cosPhi: finiteNumberSchema.positive().max(1).nullable(), + costGroup: nullableStringSchema, + remark: nullableStringSchema, + }).strict(), + overriddenFields: z.array(z.enum(externalPlanningFieldNames)).refine( + (values) => new Set(values).size === values.length + ), + externalRoomMappingId: idSchema.nullable(), + distributionBoardId: idSchema.nullable(), + linkedProjectDeviceId: idSchema.nullable(), + circuitDeviceRowId: idSchema.nullable(), + presenceStatus: z.enum(externalObjectPresenceStatuses), +}).strict(); + +const externalModelStateSchema = z.object({ + source: externalModelSourceSchema.nullable(), + importBatches: z.array(externalImportBatchSchema), + roomMappings: z.array(externalRoomMappingSchema), + objects: z.array(externalModelObjectSchema), +}).strict(); + const projectStateSnapshotContents = { circuitLists: z.array(circuitListSchema), circuitSections: z.array(circuitSectionSchema), @@ -283,6 +434,7 @@ export const projectStateSnapshotSchema = z project: projectSchema, distributionBoards: z.array(distributionBoardSchema), externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(), + externalModel: externalModelStateSchema, ...projectStateSnapshotContents, }) .strict(); @@ -292,6 +444,16 @@ const previousProjectStateSnapshotSchema = z schemaVersion: z.literal(previousProjectStateSnapshotSchemaVersion), project: projectSchema, distributionBoards: z.array(distributionBoardSchema), + externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(), + ...projectStateSnapshotContents, + }) + .strict(); + +const legacyProjectStateSnapshotSchema = z + .object({ + schemaVersion: z.literal(legacyProjectStateSnapshotSchemaVersion), + project: projectSchema, + distributionBoards: z.array(distributionBoardSchema), ...projectStateSnapshotContents, }) .strict(); @@ -335,7 +497,16 @@ function upgradePreviousProjectStateSnapshot(value: unknown): unknown { return { ...previous, schemaVersion: projectStateSnapshotSchemaVersion, + externalModel: emptyExternalModelState(), + }; + } + if (schemaVersion === legacyProjectStateSnapshotSchemaVersion) { + const legacy = legacyProjectStateSnapshotSchema.parse(value); + return { + ...legacy, + schemaVersion: projectStateSnapshotSchemaVersion, externalCsvConfiguration: null, + externalModel: emptyExternalModelState(), }; } if (schemaVersion === baselineProjectStateSnapshotSchemaVersion) { @@ -345,11 +516,21 @@ function upgradePreviousProjectStateSnapshot(value: unknown): unknown { schemaVersion: projectStateSnapshotSchemaVersion, project: { ...baseline.project, isPublicBuilding: false }, externalCsvConfiguration: null, + externalModel: emptyExternalModelState(), }; } return value; } +function emptyExternalModelState() { + return { + source: null, + importBatches: [], + roomMappings: [], + objects: [], + }; +} + function normalizeSnapshotPhaseTypes( snapshot: ProjectStateSnapshot ): ProjectStateSnapshot { @@ -666,6 +847,94 @@ function assertProjectStateSnapshotRelations( ); } } + + assertExternalModelRelations(snapshot, { + boardIds, + deviceRowIds, + projectDeviceIds, + projectId, + roomIds, + }); +} + +function assertExternalModelRelations( + snapshot: ProjectStateSnapshot, + references: { + boardIds: ReadonlySet; + deviceRowIds: ReadonlySet; + projectDeviceIds: ReadonlySet; + projectId: string; + roomIds: ReadonlySet; + } +) { + const { source, importBatches, roomMappings, objects } = snapshot.externalModel; + if (source === null) { + if (importBatches.length || roomMappings.length || objects.length) { + throw new Error("Snapshot external model entries require a source."); + } + return; + } + assertProjectOwnership(source.projectId, references.projectId, "external model source"); + const batchIds = uniqueIds(importBatches, "external import batch"); + const mappingIds = uniqueIds(roomMappings, "external room mapping"); + uniqueIds(objects, "external model object"); + + for (const batch of importBatches) { + assertProjectOwnership(batch.projectId, references.projectId, "external import batch"); + if (batch.sourceId !== source.id) { + throw new Error("Snapshot external import batch belongs to a different source."); + } + } + + const roomKeys = new Set(); + for (const mapping of roomMappings) { + assertProjectOwnership(mapping.projectId, references.projectId, "external room mapping"); + if (mapping.sourceId !== source.id) { + throw new Error("Snapshot external room mapping belongs to a different source."); + } + assertUniqueKey( + roomKeys, + mapping.normalizedSourceRoomKey, + "Snapshot contains duplicate external room keys." + ); + if (mapping.roomId !== null) { + assertReference(references.roomIds, mapping.roomId, "external room mapping room"); + } + if (mapping.defaultDistributionBoardId !== null) { + assertReference( + references.boardIds, + mapping.defaultDistributionBoardId, + "external room mapping distribution board" + ); + } + } + + const ifcGuids = new Set(); + for (const object of objects) { + assertProjectOwnership(object.projectId, references.projectId, "external model object"); + if (object.sourceId !== source.id) { + throw new Error("Snapshot external model object belongs to a different source."); + } + assertUniqueKey( + ifcGuids, + object.ifcGuid, + "Snapshot contains duplicate external IFCGUIDs." + ); + assertReference(batchIds, object.lastSeenImportBatchId, "external object last-seen batch"); + assertReference(batchIds, object.lastAcceptedImportBatchId, "external object accepted batch"); + if (object.externalRoomMappingId !== null) { + assertReference(mappingIds, object.externalRoomMappingId, "external object room mapping"); + } + if (object.distributionBoardId !== null) { + assertReference(references.boardIds, object.distributionBoardId, "external object distribution board"); + } + if (object.linkedProjectDeviceId !== null) { + assertReference(references.projectDeviceIds, object.linkedProjectDeviceId, "external object project device"); + } + if (object.circuitDeviceRowId !== null) { + assertReference(references.deviceRowIds, object.circuitDeviceRowId, "external object device row"); + } + } } function assertComponentPlacement( diff --git a/src/domain/models/project-transfer.model.ts b/src/domain/models/project-transfer.model.ts index 1282ef6..3f28f51 100644 --- a/src/domain/models/project-transfer.model.ts +++ b/src/domain/models/project-transfer.model.ts @@ -91,6 +91,13 @@ export function remapProjectState( circuit.deviceRows.map((row) => [row.id, createId()] as const) ) ); + const externalSourceId = source.externalModel.source ? createId() : null; + const externalBatchIds = idMap(source.externalModel.importBatches, createId); + const externalRoomMappingIds = idMap( + source.externalModel.roomMappings, + createId + ); + const externalObjectIds = idMap(source.externalModel.objects, createId); return parseProjectStateSnapshot({ ...source, project: { ...source.project, id: targetProjectId, name }, @@ -102,6 +109,68 @@ export function remapProjectState( id: createId(), projectId: targetProjectId, }, + externalModel: { + source: source.externalModel.source === null + ? null + : { + ...source.externalModel.source, + id: externalSourceId!, + projectId: targetProjectId, + }, + importBatches: source.externalModel.importBatches.map((batch) => ({ + ...batch, + id: requiredId(externalBatchIds, batch.id), + projectId: targetProjectId, + sourceId: externalSourceId!, + })), + roomMappings: source.externalModel.roomMappings.map((mapping) => ({ + ...mapping, + id: requiredId(externalRoomMappingIds, mapping.id), + projectId: targetProjectId, + sourceId: externalSourceId!, + roomId: + mapping.roomId === null + ? null + : requiredId(roomIds, mapping.roomId), + defaultDistributionBoardId: + mapping.defaultDistributionBoardId === null + ? null + : requiredId(boardIds, mapping.defaultDistributionBoardId), + })), + objects: source.externalModel.objects.map((object) => ({ + ...object, + id: requiredId(externalObjectIds, object.id), + projectId: targetProjectId, + sourceId: externalSourceId!, + lastSeenImportBatchId: requiredId( + externalBatchIds, + object.lastSeenImportBatchId + ), + lastAcceptedImportBatchId: requiredId( + externalBatchIds, + object.lastAcceptedImportBatchId + ), + externalRoomMappingId: + object.externalRoomMappingId === null + ? null + : requiredId( + externalRoomMappingIds, + object.externalRoomMappingId + ), + distributionBoardId: + object.distributionBoardId === null + ? null + : requiredId(boardIds, object.distributionBoardId), + linkedProjectDeviceId: + object.linkedProjectDeviceId === null + ? null + : requiredId(deviceIds, object.linkedProjectDeviceId), + circuitDeviceRowId: + object.circuitDeviceRowId === null + ? null + : requiredId(rowIds, object.circuitDeviceRowId), + })), + }, distributionBoards: source.distributionBoards.map((board) => ({ ...board, id: requiredId(boardIds, board.id), diff --git a/tests/external-model-foundation.test.ts b/tests/external-model-foundation.test.ts index aa67c0a..4bc6708 100644 --- a/tests/external-model-foundation.test.ts +++ b/tests/external-model-foundation.test.ts @@ -4,11 +4,23 @@ import { describe, it } from "node:test"; import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import { createDatabaseContext } from "../src/db/database-context.js"; import { ExternalModelStateRepository } from "../src/db/repositories/external-model-state.repository.js"; +import { ProjectStateRestoreCommandRepository } from "../src/db/repositories/project-state-restore-command.repository.js"; +import { readProjectStateSnapshot } from "../src/db/repositories/project-state-snapshot.persistence.js"; +import { ProjectTransferRepository } from "../src/db/repositories/project-transfer.repository.js"; +import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js"; +import { circuitLists } from "../src/db/schema/circuit-lists.js"; +import { circuitSections } from "../src/db/schema/circuit-sections.js"; +import { circuits } from "../src/db/schema/circuits.js"; +import { distributionBoards } from "../src/db/schema/distribution-boards.js"; import { externalImportBatches } from "../src/db/schema/external-import-batches.js"; import { externalModelObjects } from "../src/db/schema/external-model-objects.js"; import { externalModelSources } from "../src/db/schema/external-model-sources.js"; import { externalRoomMappings } from "../src/db/schema/external-room-mappings.js"; +import { floors } from "../src/db/schema/floors.js"; +import { projectDevices } from "../src/db/schema/project-devices.js"; import { projects } from "../src/db/schema/projects.js"; +import { rooms } from "../src/db/schema/rooms.js"; +import { createProjectStateRestoreCommand } from "../src/domain/models/project-state-restore-command.model.js"; import { createExternalCsvPreview } from "../src/external-model/application/external-csv-preview.js"; import { parseExternalCsv } from "../src/external-model/csv/external-csv-transport.js"; import { @@ -189,3 +201,228 @@ describe("external model state repository", () => { } }); }); + +describe("external model snapshot and transfer", () => { + it("captures, restores and undoes the complete external state", () => { + const context = createDatabaseContext(":memory:"); + migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations") }); + try { + insertLinkedExternalFixture(context.db); + const populated = readProjectStateSnapshot(context.db, "project-1"); + assert.ok(populated); + assert.equal(populated.state.schemaVersion, 4); + assert.equal(populated.state.externalModel.objects[0].circuitDeviceRowId, "row-1"); + + context.db.delete(externalModelSources).run(); + const empty = readProjectStateSnapshot(context.db, "project-1"); + assert.ok(empty); + assert.equal(empty.state.externalModel.source, null); + + const repository = new ProjectStateRestoreCommandRepository(context.db); + const restored = repository.execute({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + command: createProjectStateRestoreCommand( + empty.payloadSha256, + populated.state + ), + }); + assert.equal( + new ExternalModelStateRepository(context.db).getByProject("project-1") + .state.objects[0].linkedProjectDeviceId, + "device-1" + ); + + repository.execute({ + projectId: "project-1", + expectedRevision: 1, + source: "undo", + historyTargetChangeSetId: restored.revision.changeSetId, + command: restored.inverse, + }); + assert.equal( + new ExternalModelStateRepository(context.db).getByProject("project-1") + .state.source, + null + ); + } finally { + context.close(); + } + }); + + it("remaps every internal external-model link for a project duplicate", () => { + const context = createDatabaseContext(":memory:"); + migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations") }); + try { + insertLinkedExternalFixture(context.db); + const transferRepository = new ProjectTransferRepository(context.db); + const transfer = transferRepository.exportProject("project-1"); + assert.ok(transfer); + const duplicate = transferRepository.importDuplicate(transfer); + const copied = new ExternalModelStateRepository(context.db) + .getByProject(duplicate.projectId).state; + + assert.notEqual(copied.source?.id, "source-1"); + assert.notEqual(copied.importBatches[0].id, "batch-1"); + assert.notEqual(copied.roomMappings[0].id, "mapping-1"); + assert.notEqual(copied.objects[0].id, "object-1"); + assert.equal(copied.objects[0].ifcGuid, firstIfcGuid); + assert.equal(copied.objects[0].lastSeenImportBatchId, copied.importBatches[0].id); + assert.equal(copied.objects[0].externalRoomMappingId, copied.roomMappings[0].id); + assert.notEqual(copied.objects[0].distributionBoardId, "board-1"); + assert.notEqual(copied.objects[0].linkedProjectDeviceId, "device-1"); + assert.notEqual(copied.objects[0].circuitDeviceRowId, "row-1"); + assert.equal( + copied.importBatches[0].originalContentBase64, + Buffer.from(utf8Bytes(quotedRevitCsv, true)).toString("base64") + ); + } finally { + context.close(); + } + }); +}); + +function insertLinkedExternalFixture(database: ReturnType["db"]) { + database.insert(projects).values({ id: "project-1", name: "Projekt" }).run(); + database.insert(floors).values({ + id: "floor-1", + projectId: "project-1", + name: "EG", + sortOrder: 10, + }).run(); + database.insert(rooms).values({ + id: "room-1", + projectId: "project-1", + floorId: "floor-1", + roomNumber: "01/101", + roomName: "Technik", + }).run(); + database.insert(projectDevices).values({ + id: "device-1", + projectId: "project-1", + name: "socket", + displayName: "Steckdose", + phaseType: "single_phase", + connectionKind: "socket", + costGroup: null, + category: "single_phase", + quantity: 1, + powerPerUnit: 0.12, + simultaneityFactor: 1, + cosPhi: null, + remark: null, + voltageV: 230, + }).run(); + database.insert(distributionBoards).values({ + id: "board-1", + projectId: "project-1", + name: "UV 1", + floorId: "floor-1", + supplyType: "AV", + simultaneityFactor: 1, + }).run(); + database.insert(circuitLists).values({ + id: "list-1", + projectId: "project-1", + distributionBoardId: "board-1", + name: "UV 1 Stromkreisliste", + }).run(); + database.insert(circuitSections).values({ + id: "section-1", + circuitListId: "list-1", + key: "single_phase", + displayName: "1-phasig 1", + prefix: "-2F1.", + sortOrder: 10, + category: "single_phase", + groupNumber: 1, + }).run(); + database.insert(circuits).values({ + id: "circuit-1", + circuitListId: "list-1", + sectionId: "section-1", + equipmentIdentifier: "-2F1.1", + displayName: "Steckdosen", + sortOrder: 10, + voltage: 230, + isReserve: 0, + }).run(); + database.insert(circuitDeviceRows).values({ + id: "row-1", + circuitId: "circuit-1", + linkedProjectDeviceId: "device-1", + sortOrder: 10, + name: "socket", + displayName: "Steckdose", + phaseType: "single_phase", + connectionKind: "socket", + costGroup: null, + category: "single_phase", + level: null, + roomId: "room-1", + roomNumberSnapshot: "01/101", + roomNameSnapshot: "Technik", + quantity: 2, + powerPerUnit: 0.12, + simultaneityFactor: 1, + cosPhi: null, + remark: null, + overriddenFields: null, + }).run(); + + database.insert(externalModelSources).values({ + id: "source-1", + projectId: "project-1", + name: "Revit-Gesamtmodell", + sourceType: "revit_csv", + }).run(); + const configuration = configuredCsv(); + const bytes = utf8Bytes(quotedRevitCsv, true); + database.insert(externalImportBatches).values({ + id: "batch-1", + projectId: "project-1", + sourceId: "source-1", + importKind: "initial", + importedAtIso: "2026-08-02T15:30:00.000Z", + fileName: "revit.csv", + sha256: "a".repeat(64), + appliedProjectRevision: 0, + configurationSnapshot: configuration, + originalBytes: Buffer.from(bytes), + document: parseExternalCsv(bytes, configuration), + }).run(); + database.insert(externalRoomMappings).values({ + id: "mapping-1", + projectId: "project-1", + sourceId: "source-1", + normalizedSourceRoomKey: "number:01/101", + sourceFloorName: "EG", + sourceRoomNumber: "01/101", + sourceRoomName: "Technik", + roomId: "room-1", + defaultDistributionBoardId: "board-1", + }).run(); + const preview = createExternalCsvPreview({ + fileName: "revit.csv", + bytes, + configuration, + }); + const projected = projectInitialExternalObject(preview.objects[0], configuration); + database.insert(externalModelObjects).values({ + id: "object-1", + projectId: "project-1", + sourceId: "source-1", + ifcGuid: projected.ifcGuid, + lastSeenImportBatchId: "batch-1", + lastAcceptedImportBatchId: "batch-1", + acceptedSourceValues: projected.sourceValues, + planningValues: projected.planningValues, + overriddenFields: [], + externalRoomMappingId: "mapping-1", + distributionBoardId: "board-1", + linkedProjectDeviceId: "device-1", + circuitDeviceRowId: "row-1", + presenceStatus: "present", + }).run(); +} diff --git a/tests/project-state-snapshot.test.ts b/tests/project-state-snapshot.test.ts index 61dc8bd..a786814 100644 --- a/tests/project-state-snapshot.test.ts +++ b/tests/project-state-snapshot.test.ts @@ -32,6 +32,12 @@ function minimalSnapshot() { }, distributionBoards: [], externalCsvConfiguration: null, + externalModel: { + source: null, + importBatches: [], + roomMappings: [], + objects: [], + }, circuitLists: [], circuitSections: [], distributionBoardComponents: [], @@ -149,6 +155,7 @@ describe("project state snapshot model", () => { baseline.schemaVersion = 1 as typeof baseline.schemaVersion; delete (baseline.project as Record).isPublicBuilding; delete (baseline as Record).externalCsvConfiguration; + delete (baseline as Record).externalModel; const upgraded = parseProjectStateSnapshot(baseline); @@ -161,11 +168,35 @@ describe("project state snapshot model", () => { const previous = structuredClone(minimalSnapshot()); previous.schemaVersion = 2 as typeof previous.schemaVersion; delete (previous as Record).externalCsvConfiguration; + delete (previous as Record).externalModel; const upgraded = parseProjectStateSnapshot(previous); assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion); assert.equal(upgraded.externalCsvConfiguration, null); + assert.deepEqual(upgraded.externalModel, { + source: null, + importBatches: [], + roomMappings: [], + objects: [], + }); + }); + + it("upgrades snapshot version 3 with an empty external model", () => { + const previous = structuredClone(minimalSnapshot()); + previous.schemaVersion = 3 as typeof previous.schemaVersion; + delete (previous as Record).externalModel; + + const upgraded = parseProjectStateSnapshot(previous); + + assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion); + assert.equal(upgraded.externalCsvConfiguration, null); + assert.deepEqual(upgraded.externalModel, { + source: null, + importBatches: [], + roomMappings: [], + objects: [], + }); }); it("rejects malformed baseline and unknown snapshot shapes", () => { From c63457001846be740c8d8b53df74c48910ecb713 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 2 Aug 2026 17:33:32 +0200 Subject: [PATCH 39/59] Add atomic Revit initial import --- AGENTS.md | 14 +- docs/current-architecture.md | 17 +- .../spec/revit-csv-phase-14-audit-and-plan.md | 11 +- .../migrations/0004_illegal_the_stranger.sql | 1 + src/db/migrations/meta/0004_snapshot.json | 2396 +++++++++++++++++ src/db/migrations/meta/_journal.json | 7 + ...itial-import-project-command.repository.ts | 250 ++ src/db/schema/external-import-batches.ts | 1 + ...al-initial-import-project-command.model.ts | 134 + .../models/project-state-snapshot.model.ts | 8 + ...al-initial-import-project-command.store.ts | 19 + .../services/project-command.service.ts | 18 +- .../domain/external-model-contracts.ts | 1 + .../composition/project-command-stores.ts | 6 +- ...-import-project-command.repository.test.ts | 273 ++ tests/external-model-foundation.test.ts | 2 + 16 files changed, 3150 insertions(+), 8 deletions(-) create mode 100644 src/db/migrations/0004_illegal_the_stranger.sql create mode 100644 src/db/migrations/meta/0004_snapshot.json create mode 100644 src/db/repositories/external-initial-import-project-command.repository.ts create mode 100644 src/domain/models/external-initial-import-project-command.model.ts create mode 100644 src/domain/ports/external-initial-import-project-command.store.ts create mode 100644 tests/external-initial-import-project-command.repository.test.ts diff --git a/AGENTS.md b/AGENTS.md index a08ca7c..9418415 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -275,10 +275,20 @@ Phase 14.2 migration `0003` provides one implicit Revit CSV source per project, immutable import batches with original bytes plus the classified cell matrix, normalized source-room mappings and external objects unique by `(sourceId, ifcGuid)`. Source values, planning values and overrides are separate. -The current repository is read-only; no runtime import command writes these -tables yet. Snapshot/restore and portable project transfer schema version 4 +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 capture the complete external state and remap every internal UUID and link while preserving IFCGUIDs, source values, original bytes and the classified matrix. +Import batches additionally store the monotonic project CSV configuration +version introduced by additive migration `0004`; it is distinct from the CSV +configuration JSON schema version. +Confirmed initial state is written only through +`external-import.apply-initial`. The command rechecks configuration version, +original-byte SHA-256, parsed matrix, complete IFCGUID/source values, explicit +planning overrides and internal project links in the shared transaction. It +never creates or links a CircuitDeviceRow. Its exact inverse removes the whole +unchanged external state, and Redo restores the same UUIDs and bytes. No public +apply endpoint or staging wizard invokes this command yet. The central revision boundary creates an automatic logical snapshot after each 25 new revisions and retains only the newest 12 automatic snapshots per project. Named snapshots are never removed by this retention policy. diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 243503d..f5ea277 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -508,7 +508,9 @@ Migration `0003` ergänzt die relationale Grundlage für genau eine implizite `revit_csv`-Quelle je Projekt, geordnete Importbatches, normalisierte Quellraummappings und über `(sourceId, ifcGuid)` eindeutige externe Objekte. Batches halten Konfigurationssnapshot, Originalbytes und die klassifizierte -Zellmatrix zusammen. Quellwerte, lokale Planungswerte und Override-Metadaten +Zellmatrix zusammen. Migration `0004` ergänzt die monotone projektweite +Konfigurationsversion am Batch, getrennt von der Schema-Version des +Konfigurationsformats. Quellwerte, lokale Planungswerte und Override-Metadaten der Objekte sind getrennt; optionale Links auf Raum, Verteilung, ProjectDevice und CircuitDeviceRow bleiben nullable. `ExternalModelStateRepository` liest den vollständigen Zustand deterministisch und unterscheidet unbekannte von noch @@ -517,8 +519,17 @@ Batchreihenfolge abgeleitet statt redundant an der Quelle gespeichert. Snapshot/Transfer v4 erfasst Originalbytes, Matrix und alle internen Links, stellt sie in Fremdschlüsselreihenfolge wieder her und remappt beim Projektduplikat jede interne UUID; IFCGUID und Quelltransport bleiben -unverändert. Import-Command und bestätigte Importübernahme sind noch nicht -implementiert; bloße Vorschau erzeugt weiterhin keine dieser Zeilen. +unverändert. `external-import.apply-initial` ist die atomare Schreibgrenze für +einen bestätigten Erstimport. Vor dem Insert prüft sie den aktuellen +Konfigurationsstand, SHA-256 und Base64-Kanonizität, parst die Originalbytes +erneut, vergleicht Matrix, vollständige IFCGUID-Menge und Quellwerte und +validiert alle Raum-, Verteiler- und ProjectDevice-Links gegen das Projekt. +Nicht explizit als Override markierte Planungswerte müssen dem regelbasierten +Vorschlag entsprechen. Der Command erzeugt und verknüpft ausdrücklich keine +CircuitDeviceRow; Undo entfernt den vollständigen unveränderten Importzustand, +Redo verwendet dieselben UUIDs und Bytes. Ein aufrufender Import-Endpunkt und +der mehrstufige Wizard sind noch nicht implementiert; bloße Vorschau erzeugt +weiterhin keine dieser Zeilen. `GET` und `PUT /api/projects/:projectId/external-csv/configuration` lesen oder ändern die Konfiguration; der PUT plant Identität und nächsten Konfigurationsstand serverseitig und verwendet den typisierten Command. diff --git a/docs/spec/revit-csv-phase-14-audit-and-plan.md b/docs/spec/revit-csv-phase-14-audit-and-plan.md index 4570a5a..0388c3a 100644 --- a/docs/spec/revit-csv-phase-14-audit-and-plan.md +++ b/docs/spec/revit-csv-phase-14-audit-and-plan.md @@ -108,7 +108,8 @@ ohne eine zweite veränderbare Historientabelle einzuführen. - stabile UUID, `projectId`, `sourceId`, Importart und Zeitstempel; - Dateiname, SHA-256 und zugehörige Projektrevision; -- verwendeter Konfigurationssnapshot; +- verwendete monotone Konfigurationsversion und vollständiger + Konfigurationssnapshot; - Originaldatei als Bytes; - verlustarme logische Zellmatrix mit Zeilenklassen und Dialektmetadaten. @@ -278,6 +279,14 @@ einen Commit aufgenommen. Matrix und sämtliche internen Links. Duplikate remappen alle internen UUIDs, lassen IFCGUID und Quelltransport jedoch unverändert. 3. Erstimport-Command mit echten SQLite-Commit-/Rollback-Tests ergänzen. + **Erledigt:** `external-import.apply-initial` wechselt ausschließlich + zwischen leerem und vollständig bestätigtem externem Zustand. Er prüft + Konfigurationsversion, Original-Hash, erneut geparste Matrix, vollständige + IFCGUID-Menge, Quellwerte, explizite Overrides und projektinterne Links. + Der Command verbietet CircuitDeviceRow-Links, ist ein atomarer Undo-Schritt + und verwendet für Redo dieselben UUIDs und Bytes. Migration `0004` ergänzt + die zuvor noch fehlende monotone Konfigurationsversion am Batch; `0003` + bleibt unverändert. 4. Raum-, Verteilungs-, Klassifizierungs- und ProjectDevice-Schritte im Wizard ergänzen; der Import selbst erzeugt keine CircuitDeviceRow. diff --git a/src/db/migrations/0004_illegal_the_stranger.sql b/src/db/migrations/0004_illegal_the_stranger.sql new file mode 100644 index 0000000..6588888 --- /dev/null +++ b/src/db/migrations/0004_illegal_the_stranger.sql @@ -0,0 +1 @@ +ALTER TABLE `external_import_batches` ADD `configuration_version` integer NOT NULL; \ No newline at end of file diff --git a/src/db/migrations/meta/0004_snapshot.json b/src/db/migrations/meta/0004_snapshot.json new file mode 100644 index 0000000..57c5292 --- /dev/null +++ b/src/db/migrations/meta/0004_snapshot.json @@ -0,0 +1,2396 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "da7eb583-b86a-440c-a71e-babb5ee24cd1", + "prevId": "6012d737-97f7-420d-a44d-85edbe5e53cf", + "tables": { + "circuit_device_rows": { + "name": "circuit_device_rows", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_id": { + "name": "circuit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "linked_project_device_id": { + "name": "linked_project_device_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase_type": { + "name": "phase_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_kind": { + "name": "connection_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_group": { + "name": "cost_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_number_snapshot": { + "name": "room_number_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_name_snapshot": { + "name": "room_name_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "power_per_unit": { + "name": "power_per_unit", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cos_phi": { + "name": "cos_phi", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "overridden_fields": { + "name": "overridden_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "circuit_device_rows_circuit_id_circuits_id_fk": { + "name": "circuit_device_rows_circuit_id_circuits_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "circuits", + "columnsFrom": [ + "circuit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { + "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "project_devices", + "columnsFrom": [ + "linked_project_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "circuit_device_rows_room_id_rooms_id_fk": { + "name": "circuit_device_rows_room_id_rooms_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_list_equipment_identifiers": { + "name": "circuit_list_equipment_identifiers", + "columns": { + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "circuit_list_equipment_identifiers_owner_unique": { + "name": "circuit_list_equipment_identifiers_owner_unique", + "columns": [ + "owner_type", + "owner_id" + ], + "isUnique": true + }, + "circuit_list_equipment_identifiers_list_identifier_unique": { + "name": "circuit_list_equipment_identifiers_list_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk": { + "name": "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuit_list_equipment_identifiers", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_lists": { + "name": "circuit_lists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "distribution_board_id": { + "name": "distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "circuit_lists_distribution_board_id_unique": { + "name": "circuit_lists_distribution_board_id_unique", + "columns": [ + "distribution_board_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_lists_project_id_projects_id_fk": { + "name": "circuit_lists_project_id_projects_id_fk", + "tableFrom": "circuit_lists", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuit_lists_distribution_board_id_distribution_boards_id_fk": { + "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "circuit_lists", + "tableTo": "distribution_boards", + "columnsFrom": [ + "distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_protection_devices": { + "name": "circuit_protection_devices", + "columns": { + "circuit_id": { + "name": "circuit_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rated_current_a": { + "name": "rated_current_a", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fuse_utilization_category": { + "name": "fuse_utilization_category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trip_characteristic": { + "name": "trip_characteristic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_type": { + "name": "rcd_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rated_residual_current_ma": { + "name": "rated_residual_current_ma", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "circuit_protection_devices_circuit_id_circuits_id_fk": { + "name": "circuit_protection_devices_circuit_id_circuits_id_fk", + "tableFrom": "circuit_protection_devices", + "tableTo": "circuits", + "columnsFrom": [ + "circuit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_sections": { + "name": "circuit_sections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_number": { + "name": "group_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "circuit_sections_list_key_unique": { + "name": "circuit_sections_list_key_unique", + "columns": [ + "circuit_list_id", + "key" + ], + "isUnique": true + }, + "circuit_sections_list_prefix_unique": { + "name": "circuit_sections_list_prefix_unique", + "columns": [ + "circuit_list_id", + "prefix" + ], + "isUnique": true + }, + "circuit_sections_list_category_group_unique": { + "name": "circuit_sections_list_category_group_unique", + "columns": [ + "circuit_list_id", + "category", + "group_number" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_sections_circuit_list_id_circuit_lists_id_fk": { + "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuit_sections", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuits": { + "name": "circuits", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cable_type": { + "name": "cable_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cable_cross_section": { + "name": "cable_cross_section", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cable_length": { + "name": "cable_length", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_assignment": { + "name": "rcd_assignment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_designation": { + "name": "terminal_designation", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voltage": { + "name": "voltage", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "control_requirement": { + "name": "control_requirement", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_reserve": { + "name": "is_reserve", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "circuits_list_equipment_identifier_unique": { + "name": "circuits_list_equipment_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuits_circuit_list_id_circuit_lists_id_fk": { + "name": "circuits_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuits", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuits_section_id_circuit_sections_id_fk": { + "name": "circuits_section_id_circuit_sections_id_fk", + "tableFrom": "circuits", + "tableTo": "circuit_sections", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_board_component_protection_devices": { + "name": "distribution_board_component_protection_devices", + "columns": { + "component_id": { + "name": "component_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rated_current_a": { + "name": "rated_current_a", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fuse_utilization_category": { + "name": "fuse_utilization_category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trip_characteristic": { + "name": "trip_characteristic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_type": { + "name": "rcd_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rated_residual_current_ma": { + "name": "rated_residual_current_ma", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk": { + "name": "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk", + "tableFrom": "distribution_board_component_protection_devices", + "tableTo": "distribution_board_components", + "columnsFrom": [ + "component_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_board_components": { + "name": "distribution_board_components", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "placement": { + "name": "placement", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "distribution_board_components_list_identifier_unique": { + "name": "distribution_board_components_list_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + }, + "distribution_board_components_section_role_unique": { + "name": "distribution_board_components_section_role_unique", + "columns": [ + "section_id", + "role" + ], + "isUnique": true + } + }, + "foreignKeys": { + "distribution_board_components_circuit_list_id_circuit_lists_id_fk": { + "name": "distribution_board_components_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "distribution_board_components", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "distribution_board_components_section_id_circuit_sections_id_fk": { + "name": "distribution_board_components_section_id_circuit_sections_id_fk", + "tableFrom": "distribution_board_components", + "tableTo": "circuit_sections", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_boards": { + "name": "distribution_boards", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "floor_id": { + "name": "floor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supply_type": { + "name": "supply_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "distribution_boards_project_id_projects_id_fk": { + "name": "distribution_boards_project_id_projects_id_fk", + "tableFrom": "distribution_boards", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "distribution_boards_floor_id_floors_id_fk": { + "name": "distribution_boards_floor_id_floors_id_fk", + "tableFrom": "distribution_boards", + "tableTo": "floors", + "columnsFrom": [ + "floor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_csv_configurations": { + "name": "external_csv_configurations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration_version": { + "name": "configuration_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration": { + "name": "configuration", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_csv_configurations_project_id_unique": { + "name": "external_csv_configurations_project_id_unique", + "columns": [ + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "external_csv_configurations_project_id_projects_id_fk": { + "name": "external_csv_configurations_project_id_projects_id_fk", + "tableFrom": "external_csv_configurations", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_import_batches": { + "name": "external_import_batches", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "import_kind": { + "name": "import_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imported_at_iso": { + "name": "imported_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_project_revision": { + "name": "applied_project_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration_version": { + "name": "configuration_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration_snapshot": { + "name": "configuration_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "original_bytes": { + "name": "original_bytes", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "document": { + "name": "document", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_import_batches_project_revision_idx": { + "name": "external_import_batches_project_revision_idx", + "columns": [ + "project_id", + "applied_project_revision" + ], + "isUnique": false + }, + "external_import_batches_source_imported_idx": { + "name": "external_import_batches_source_imported_idx", + "columns": [ + "source_id", + "imported_at_iso" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_import_batches_project_id_projects_id_fk": { + "name": "external_import_batches_project_id_projects_id_fk", + "tableFrom": "external_import_batches", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_import_batches_source_id_external_model_sources_id_fk": { + "name": "external_import_batches_source_id_external_model_sources_id_fk", + "tableFrom": "external_import_batches", + "tableTo": "external_model_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_model_objects": { + "name": "external_model_objects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ifc_guid": { + "name": "ifc_guid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen_import_batch_id": { + "name": "last_seen_import_batch_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_accepted_import_batch_id": { + "name": "last_accepted_import_batch_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accepted_source_values": { + "name": "accepted_source_values", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "planning_values": { + "name": "planning_values", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "overridden_fields": { + "name": "overridden_fields", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_room_mapping_id": { + "name": "external_room_mapping_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "distribution_board_id": { + "name": "distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_project_device_id": { + "name": "linked_project_device_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "circuit_device_row_id": { + "name": "circuit_device_row_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "presence_status": { + "name": "presence_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'present'" + } + }, + "indexes": { + "external_model_objects_source_ifc_guid_unique": { + "name": "external_model_objects_source_ifc_guid_unique", + "columns": [ + "source_id", + "ifc_guid" + ], + "isUnique": true + }, + "external_model_objects_project_presence_idx": { + "name": "external_model_objects_project_presence_idx", + "columns": [ + "project_id", + "presence_status" + ], + "isUnique": false + }, + "external_model_objects_distribution_board_idx": { + "name": "external_model_objects_distribution_board_idx", + "columns": [ + "distribution_board_id" + ], + "isUnique": false + }, + "external_model_objects_circuit_device_row_idx": { + "name": "external_model_objects_circuit_device_row_idx", + "columns": [ + "circuit_device_row_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_model_objects_project_id_projects_id_fk": { + "name": "external_model_objects_project_id_projects_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_model_objects_source_id_external_model_sources_id_fk": { + "name": "external_model_objects_source_id_external_model_sources_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_model_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_model_objects_last_seen_import_batch_id_external_import_batches_id_fk": { + "name": "external_model_objects_last_seen_import_batch_id_external_import_batches_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_import_batches", + "columnsFrom": [ + "last_seen_import_batch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "external_model_objects_last_accepted_import_batch_id_external_import_batches_id_fk": { + "name": "external_model_objects_last_accepted_import_batch_id_external_import_batches_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_import_batches", + "columnsFrom": [ + "last_accepted_import_batch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "external_model_objects_external_room_mapping_id_external_room_mappings_id_fk": { + "name": "external_model_objects_external_room_mapping_id_external_room_mappings_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_room_mappings", + "columnsFrom": [ + "external_room_mapping_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_model_objects_distribution_board_id_distribution_boards_id_fk": { + "name": "external_model_objects_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "distribution_boards", + "columnsFrom": [ + "distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_model_objects_linked_project_device_id_project_devices_id_fk": { + "name": "external_model_objects_linked_project_device_id_project_devices_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "project_devices", + "columnsFrom": [ + "linked_project_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_model_objects_circuit_device_row_id_circuit_device_rows_id_fk": { + "name": "external_model_objects_circuit_device_row_id_circuit_device_rows_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "circuit_device_rows", + "columnsFrom": [ + "circuit_device_row_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_model_sources": { + "name": "external_model_sources", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'revit_csv'" + } + }, + "indexes": { + "external_model_sources_project_type_unique": { + "name": "external_model_sources_project_type_unique", + "columns": [ + "project_id", + "source_type" + ], + "isUnique": true + } + }, + "foreignKeys": { + "external_model_sources_project_id_projects_id_fk": { + "name": "external_model_sources_project_id_projects_id_fk", + "tableFrom": "external_model_sources", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_room_mappings": { + "name": "external_room_mappings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "normalized_source_room_key": { + "name": "normalized_source_room_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_floor_name": { + "name": "source_floor_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_room_number": { + "name": "source_room_number", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_room_name": { + "name": "source_room_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_distribution_board_id": { + "name": "default_distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "external_room_mappings_source_key_unique": { + "name": "external_room_mappings_source_key_unique", + "columns": [ + "source_id", + "normalized_source_room_key" + ], + "isUnique": true + }, + "external_room_mappings_project_room_idx": { + "name": "external_room_mappings_project_room_idx", + "columns": [ + "project_id", + "room_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_room_mappings_project_id_projects_id_fk": { + "name": "external_room_mappings_project_id_projects_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_room_mappings_source_id_external_model_sources_id_fk": { + "name": "external_room_mappings_source_id_external_model_sources_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "external_model_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_room_mappings_room_id_rooms_id_fk": { + "name": "external_room_mappings_room_id_rooms_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_room_mappings_default_distribution_board_id_distribution_boards_id_fk": { + "name": "external_room_mappings_default_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "distribution_boards", + "columnsFrom": [ + "default_distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "floors": { + "name": "floors", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "floors_project_id_projects_id_fk": { + "name": "floors_project_id_projects_id_fk", + "tableFrom": "floors", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "global_devices": { + "name": "global_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "installed_power_per_unit_kw": { + "name": "installed_power_per_unit_kw", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "demand_factor": { + "name": "demand_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "voltage_v": { + "name": "voltage_v", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phase_count": { + "name": "phase_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "power_factor": { + "name": "power_factor", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_change_sets": { + "name": "project_change_sets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_revision_id": { + "name": "project_revision_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_type": { + "name": "command_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_schema_version": { + "name": "payload_schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forward_payload_json": { + "name": "forward_payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inverse_payload_json": { + "name": "inverse_payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_change_sets_revision_unique": { + "name": "project_change_sets_revision_unique", + "columns": [ + "project_revision_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_change_sets_project_revision_id_project_revisions_id_fk": { + "name": "project_change_sets_project_revision_id_project_revisions_id_fk", + "tableFrom": "project_change_sets", + "tableTo": "project_revisions", + "columnsFrom": [ + "project_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_devices": { + "name": "project_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase_type": { + "name": "phase_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'single_phase'" + }, + "connection_kind": { + "name": "connection_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_group": { + "name": "cost_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "power_per_unit": { + "name": "power_per_unit", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "cos_phi": { + "name": "cos_phi", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voltage_v": { + "name": "voltage_v", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "project_devices_project_id_projects_id_fk": { + "name": "project_devices_project_id_projects_id_fk", + "tableFrom": "project_devices", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_history_stack_entries": { + "name": "project_history_stack_entries", + "columns": { + "change_set_id": { + "name": "change_set_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stack": { + "name": "stack", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_history_stack_entries_position_unique": { + "name": "project_history_stack_entries_position_unique", + "columns": [ + "project_id", + "stack", + "position" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { + "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", + "tableFrom": "project_history_stack_entries", + "tableTo": "project_change_sets", + "columnsFrom": [ + "change_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_history_stack_entries_project_id_projects_id_fk": { + "name": "project_history_stack_entries_project_id_projects_id_fk", + "tableFrom": "project_history_stack_entries", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "project_history_stack_entries_stack_check": { + "name": "project_history_stack_entries_stack_check", + "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" + } + } + }, + "project_revisions": { + "name": "project_revisions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "project_revisions_project_number_unique": { + "name": "project_revisions_project_number_unique", + "columns": [ + "project_id", + "revision_number" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_revisions_project_id_projects_id_fk": { + "name": "project_revisions_project_id_projects_id_fk", + "tableFrom": "project_revisions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_snapshots": { + "name": "project_snapshots", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_revision": { + "name": "source_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'named'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_sha256": { + "name": "payload_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "project_snapshots_project_created_idx": { + "name": "project_snapshots_project_created_idx", + "columns": [ + "project_id", + "created_at_iso" + ], + "isUnique": false + }, + "project_snapshots_project_kind_revision_idx": { + "name": "project_snapshots_project_kind_revision_idx", + "columns": [ + "project_id", + "kind", + "source_revision" + ], + "isUnique": false + }, + "project_snapshots_project_name_unique": { + "name": "project_snapshots_project_name_unique", + "columns": [ + "project_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_snapshots_project_id_projects_id_fk": { + "name": "project_snapshots_project_id_projects_id_fk", + "tableFrom": "project_snapshots", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_project_number": { + "name": "internal_project_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_project_number": { + "name": "external_project_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "building_owner": { + "name": "building_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_public_building": { + "name": "is_public_building", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "single_phase_voltage_v": { + "name": "single_phase_voltage_v", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 230 + }, + "three_phase_voltage_v": { + "name": "three_phase_voltage_v", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 400 + }, + "enabled_distribution_board_supply_types": { + "name": "enabled_distribution_board_supply_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[\"AV\",\"SV\",\"EV\",\"USV\",\"MSR\",\"SiBe\"]'" + }, + "current_revision": { + "name": "current_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rooms": { + "name": "rooms", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "floor_id": { + "name": "floor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_number": { + "name": "room_number", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "room_name": { + "name": "room_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "rooms_project_id_projects_id_fk": { + "name": "rooms_project_id_projects_id_fk", + "tableFrom": "rooms", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rooms_floor_id_floors_id_fk": { + "name": "rooms_floor_id_floors_id_fk", + "tableFrom": "rooms", + "tableTo": "floors", + "columnsFrom": [ + "floor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 1ac144a..e3f76b7 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1785683710155, "tag": "0003_outstanding_maddog", "breakpoints": true + }, + { + "idx": 4, + "version": "6", + "when": 1785684457208, + "tag": "0004_illegal_the_stranger", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/repositories/external-initial-import-project-command.repository.ts b/src/db/repositories/external-initial-import-project-command.repository.ts new file mode 100644 index 0000000..9d2895a --- /dev/null +++ b/src/db/repositories/external-initial-import-project-command.repository.ts @@ -0,0 +1,250 @@ +import crypto from "node:crypto"; +import { eq } from "drizzle-orm"; +import { + assertExternalInitialImportProjectCommand, + createExternalInitialImportProjectCommand, + isEmptyExternalModelState, +} from "../../domain/models/external-initial-import-project-command.model.js"; +import type { ExternalInitialImportProjectCommandStore } from "../../domain/ports/external-initial-import-project-command.store.js"; +import { createExternalCsvPreview } from "../../external-model/application/external-csv-preview.js"; +import { parseExternalCsv } from "../../external-model/csv/external-csv-transport.js"; +import type { ExternalModelStateSnapshot } from "../../external-model/domain/external-model-contracts.js"; +import { + createExternalRoomKey, + indexExternalObjectsByIfcGuid, + projectInitialExternalObject, +} from "../../external-model/domain/external-model-matching.js"; +import type { AppDatabase } from "../database-context.js"; +import { distributionBoards } from "../schema/distribution-boards.js"; +import { externalCsvConfigurations } from "../schema/external-csv-configurations.js"; +import { externalImportBatches } from "../schema/external-import-batches.js"; +import { externalModelObjects } from "../schema/external-model-objects.js"; +import { externalModelSources } from "../schema/external-model-sources.js"; +import { externalRoomMappings } from "../schema/external-room-mappings.js"; +import { projectDevices } from "../schema/project-devices.js"; +import { rooms } from "../schema/rooms.js"; +import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { ExternalModelStateRepository } from "./external-model-state.repository.js"; + +export class ExternalInitialImportProjectCommandRepository + implements ExternalInitialImportProjectCommandStore +{ + constructor(private readonly database: AppDatabase) {} + + execute(input: Parameters[0]) { + assertExternalInitialImportProjectCommand(input.command); + return executeProjectCommandTransaction(this.database, input, (tx) => { + const currentResult = new ExternalModelStateRepository(tx) + .getByProject(input.projectId); + if (!currentResult.projectExists) throw new Error("Project not found."); + const expected = input.command.payload.expected; + if (!sameState(currentResult.state, expected)) { + throw new Error("External model state changed before initial import."); + } + const target = input.command.payload.target; + if (!isEmptyExternalModelState(target)) { + validatePopulatedTarget(tx, input, target); + } + + replaceExternalState(tx, input.projectId, target); + const persisted = new ExternalModelStateRepository(tx) + .getByProject(input.projectId).state; + if (!sameState(persisted, target)) { + throw new Error("Persisted external initial import differs from its target."); + } + return createExternalInitialImportProjectCommand(target, currentResult.state); + }); + } +} + +function validatePopulatedTarget( + database: AppDatabase, + input: Parameters[0], + target: ExternalModelStateSnapshot +) { + const source = target.source!; + const batch = target.importBatches[0]; + if (source.projectId !== input.projectId) { + throw new Error("External initial import belongs to a different project."); + } + if ( + input.source === "user" && + batch.appliedProjectRevision !== input.expectedRevision + 1 + ) { + throw new Error("External initial import revision is invalid."); + } + const configuration = database + .select() + .from(externalCsvConfigurations) + .where(eq(externalCsvConfigurations.projectId, input.projectId)) + .get(); + if ( + !configuration || + configuration.configurationVersion !== batch.configurationVersion || + canonicalJson(configuration.configuration) !== + canonicalJson(batch.configurationSnapshot) + ) { + throw new Error("External initial import configuration changed before apply."); + } + + const bytes = decodeCanonicalBase64(batch.originalContentBase64); + const sha256 = crypto.createHash("sha256").update(bytes).digest("hex"); + if (sha256 !== batch.sha256) { + throw new Error("External initial import checksum is invalid."); + } + const parsedDocument = parseExternalCsv(bytes, batch.configurationSnapshot); + if (canonicalJson(parsedDocument) !== canonicalJson(batch.document)) { + throw new Error("External initial import document differs from the original bytes."); + } + const preview = createExternalCsvPreview({ + fileName: batch.fileName, + bytes, + configuration: batch.configurationSnapshot, + }); + const targetByIfcGuid = indexExternalObjectsByIfcGuid(target.objects); + if (targetByIfcGuid.size !== preview.objects.length) { + throw new Error("External initial import must contain every CSV object exactly once."); + } + const mappingsById = new Map( + target.roomMappings.map((mapping) => [mapping.id, mapping]) + ); + const expectedRoomKeys = new Set(); + for (const candidate of preview.objects) { + const object = targetByIfcGuid.get(candidate.ifcGuid); + if (!object) { + throw new Error(`External initial import is missing IFCGUID ${candidate.ifcGuid}.`); + } + const projected = projectInitialExternalObject( + candidate, + batch.configurationSnapshot + ); + if (canonicalJson(object.acceptedSourceValues) !== canonicalJson(projected.sourceValues)) { + throw new Error("External initial import source values differ from the CSV."); + } + for (const field of Object.keys(projected.planningValues) as Array) { + if ( + !object.overriddenFields.includes(field) && + canonicalJson(object.planningValues[field]) !== + canonicalJson(projected.planningValues[field]) + ) { + throw new Error(`External initial planning field ${field} changed without an override.`); + } + } + const roomKey = createExternalRoomKey(candidate.roomNumber, candidate.roomName); + if (roomKey === null) { + if (object.externalRoomMappingId !== null) { + throw new Error("External object without a source room must not use a room mapping."); + } + } else { + expectedRoomKeys.add(roomKey); + const mapping = object.externalRoomMappingId === null + ? null + : mappingsById.get(object.externalRoomMappingId); + if (!mapping || mapping.normalizedSourceRoomKey !== roomKey) { + throw new Error("External object source room mapping is incomplete."); + } + } + } + if ( + target.roomMappings.length !== expectedRoomKeys.size || + target.roomMappings.some( + (mapping) => !expectedRoomKeys.has(mapping.normalizedSourceRoomKey) + ) + ) { + throw new Error("External initial import room mappings do not match CSV rooms."); + } + validateInternalLinks(database, input.projectId, target); +} + +function validateInternalLinks( + database: AppDatabase, + projectId: string, + target: ExternalModelStateSnapshot +) { + const roomIds = new Set( + database.select({ id: rooms.id }).from(rooms) + .where(eq(rooms.projectId, projectId)).all().map((row) => row.id) + ); + const boardIds = new Set( + database.select({ id: distributionBoards.id }).from(distributionBoards) + .where(eq(distributionBoards.projectId, projectId)).all().map((row) => row.id) + ); + const deviceIds = new Set( + database.select({ id: projectDevices.id }).from(projectDevices) + .where(eq(projectDevices.projectId, projectId)).all().map((row) => row.id) + ); + for (const mapping of target.roomMappings) { + if (mapping.roomId !== null && !roomIds.has(mapping.roomId)) { + throw new Error("External room mapping references another project."); + } + if ( + mapping.defaultDistributionBoardId !== null && + !boardIds.has(mapping.defaultDistributionBoardId) + ) { + throw new Error("External room mapping distribution board belongs to another project."); + } + } + for (const object of target.objects) { + if (object.distributionBoardId !== null && !boardIds.has(object.distributionBoardId)) { + throw new Error("External object distribution board belongs to another project."); + } + if (object.linkedProjectDeviceId !== null && !deviceIds.has(object.linkedProjectDeviceId)) { + throw new Error("External object project device belongs to another project."); + } + } +} + +function replaceExternalState( + database: AppDatabase, + projectId: string, + target: ExternalModelStateSnapshot +) { + database.delete(externalModelSources) + .where(eq(externalModelSources.projectId, projectId)).run(); + if (target.source === null) return; + database.insert(externalModelSources).values(target.source).run(); + database.insert(externalImportBatches).values( + target.importBatches.map(({ originalContentBase64, ...batch }) => ({ + ...batch, + originalBytes: Buffer.from(originalContentBase64, "base64"), + })) + ).run(); + if (target.roomMappings.length) { + database.insert(externalRoomMappings).values(target.roomMappings).run(); + } + database.insert(externalModelObjects).values(target.objects).run(); +} + +function decodeCanonicalBase64(value: string) { + const bytes = Buffer.from(value, "base64"); + if (bytes.toString("base64") !== value) { + throw new Error("External initial import bytes are not canonical base64."); + } + return bytes; +} + +function sameState(left: ExternalModelStateSnapshot, right: ExternalModelStateSnapshot) { + return canonicalJson(sortState(left)) === canonicalJson(sortState(right)); +} + +function sortState(state: ExternalModelStateSnapshot): ExternalModelStateSnapshot { + const byId = (entries: readonly T[]) => + [...entries].sort((left, right) => left.id.localeCompare(right.id)); + return { + source: state.source, + importBatches: byId(state.importBatches), + roomMappings: byId(state.roomMappings), + objects: byId(state.objects), + }; +} + +function canonicalJson(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; + if (value !== null && typeof value === "object") { + const record = value as Record; + return `{${Object.keys(record).sort().map( + (key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}` + ).join(",")}}`; + } + return JSON.stringify(value); +} diff --git a/src/db/schema/external-import-batches.ts b/src/db/schema/external-import-batches.ts index bf317e9..125077e 100644 --- a/src/db/schema/external-import-batches.ts +++ b/src/db/schema/external-import-batches.ts @@ -22,6 +22,7 @@ export const externalImportBatches = sqliteTable( fileName: text("file_name").notNull(), sha256: text("sha256").notNull(), appliedProjectRevision: integer("applied_project_revision").notNull(), + configurationVersion: integer("configuration_version").notNull(), configurationSnapshot: text("configuration_snapshot", { mode: "json" }) .$type() .notNull(), diff --git a/src/domain/models/external-initial-import-project-command.model.ts b/src/domain/models/external-initial-import-project-command.model.ts new file mode 100644 index 0000000..2004cfc --- /dev/null +++ b/src/domain/models/external-initial-import-project-command.model.ts @@ -0,0 +1,134 @@ +import type { ExternalModelStateSnapshot } from "../../external-model/domain/external-model-contracts.js"; +import { parseExternalModelStateSnapshot } from "./project-state-snapshot.model.js"; +import type { SerializedProjectCommand } from "./project-command.model.js"; + +export const externalInitialImportCommandType = + "external-import.apply-initial" as const; +export const externalInitialImportCommandSchemaVersion = 1 as const; + +export interface ExternalInitialImportPayload { + expected: ExternalModelStateSnapshot; + target: ExternalModelStateSnapshot; +} + +export interface ExternalInitialImportProjectCommand + extends SerializedProjectCommand { + schemaVersion: typeof externalInitialImportCommandSchemaVersion; + type: typeof externalInitialImportCommandType; +} + +export function createExternalInitialImportProjectCommand( + expected: ExternalModelStateSnapshot, + target: ExternalModelStateSnapshot +): ExternalInitialImportProjectCommand { + const command: ExternalInitialImportProjectCommand = { + schemaVersion: externalInitialImportCommandSchemaVersion, + type: externalInitialImportCommandType, + payload: { expected, target }, + }; + assertExternalInitialImportProjectCommand(command); + return command; +} + +export function assertExternalInitialImportProjectCommand( + command: SerializedProjectCommand +): asserts command is ExternalInitialImportProjectCommand { + if ( + command.schemaVersion !== externalInitialImportCommandSchemaVersion || + command.type !== externalInitialImportCommandType || + !isRecord(command.payload) || + Object.keys(command.payload).length !== 2 + ) { + throw new Error("Unsupported external initial import command."); + } + const expected = parseExternalModelStateSnapshot(command.payload.expected); + const target = parseExternalModelStateSnapshot(command.payload.target); + const expectedIsEmpty = isEmptyExternalModelState(expected); + const targetIsEmpty = isEmptyExternalModelState(target); + if (expectedIsEmpty === targetIsEmpty) { + throw new Error("External initial import must transition between empty and populated state."); + } + assertPopulatedInitialState(expectedIsEmpty ? target : expected); +} + +export function createEmptyExternalModelState(): ExternalModelStateSnapshot { + return { source: null, importBatches: [], roomMappings: [], objects: [] }; +} + +export function isEmptyExternalModelState(state: ExternalModelStateSnapshot) { + return ( + state.source === null && + state.importBatches.length === 0 && + state.roomMappings.length === 0 && + state.objects.length === 0 + ); +} + +function assertPopulatedInitialState(state: ExternalModelStateSnapshot) { + if (state.source === null || state.importBatches.length !== 1 || state.objects.length === 0) { + throw new Error("External initial import requires one source, one batch and at least one object."); + } + const source = state.source; + const batch = state.importBatches[0]; + if ( + batch.importKind !== "initial" || + batch.projectId !== source.projectId || + batch.sourceId !== source.id + ) { + throw new Error("External initial import batch identity is invalid."); + } + const mappingIds = uniqueIds(state.roomMappings, "room mapping"); + const roomKeys = new Set(); + for (const mapping of state.roomMappings) { + if (mapping.projectId !== source.projectId || mapping.sourceId !== source.id) { + throw new Error("External initial room mapping identity is invalid."); + } + if (roomKeys.has(mapping.normalizedSourceRoomKey)) { + throw new Error("External initial import contains duplicate room keys."); + } + roomKeys.add(mapping.normalizedSourceRoomKey); + } + uniqueIds(state.objects, "object"); + const ifcGuids = new Set(); + for (const object of state.objects) { + if ( + object.projectId !== source.projectId || + object.sourceId !== source.id || + object.lastSeenImportBatchId !== batch.id || + object.lastAcceptedImportBatchId !== batch.id + ) { + throw new Error("External initial object identity is invalid."); + } + if (ifcGuids.has(object.ifcGuid)) { + throw new Error("External initial import contains duplicate IFCGUIDs."); + } + ifcGuids.add(object.ifcGuid); + if ( + object.externalRoomMappingId !== null && + !mappingIds.has(object.externalRoomMappingId) + ) { + throw new Error("External initial object room mapping is invalid."); + } + if (object.circuitDeviceRowId !== null) { + throw new Error("External initial import must not assign circuit device rows."); + } + if (object.presenceStatus !== "present") { + throw new Error("External initial import objects must be present."); + } + } +} + +function uniqueIds(entries: ReadonlyArray<{ id: string }>, label: string) { + const ids = new Set(); + for (const entry of entries) { + if (ids.has(entry.id)) { + throw new Error(`External initial import contains duplicate ${label} ids.`); + } + ids.add(entry.id); + } + return ids; +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} diff --git a/src/domain/models/project-state-snapshot.model.ts b/src/domain/models/project-state-snapshot.model.ts index e0ee77f..77ad47f 100644 --- a/src/domain/models/project-state-snapshot.model.ts +++ b/src/domain/models/project-state-snapshot.model.ts @@ -24,6 +24,7 @@ import { externalImportKinds, externalModelSourceTypes, externalObjectPresenceStatuses, + type ExternalModelStateSnapshot, } from "../../external-model/domain/external-model-contracts.js"; export const projectStateSnapshotSchemaVersion = 4 as const; @@ -333,6 +334,7 @@ const externalImportBatchSchema = z.object({ fileName: z.string().trim().min(1), sha256: z.string().regex(/^[a-f0-9]{64}$/), appliedProjectRevision: z.number().int().nonnegative(), + configurationVersion: z.number().int().positive(), configurationSnapshot: z.custom( (value) => validateExternalCsvConfiguration(value).success ), @@ -414,6 +416,12 @@ const externalModelStateSchema = z.object({ objects: z.array(externalModelObjectSchema), }).strict(); +export function parseExternalModelStateSnapshot( + value: unknown +): ExternalModelStateSnapshot { + return externalModelStateSchema.parse(value); +} + const projectStateSnapshotContents = { circuitLists: z.array(circuitListSchema), circuitSections: z.array(circuitSectionSchema), diff --git a/src/domain/ports/external-initial-import-project-command.store.ts b/src/domain/ports/external-initial-import-project-command.store.ts new file mode 100644 index 0000000..db1917e --- /dev/null +++ b/src/domain/ports/external-initial-import-project-command.store.ts @@ -0,0 +1,19 @@ +import type { ExternalInitialImportProjectCommand } from "../models/external-initial-import-project-command.model.js"; +import type { AppendedProjectRevision, ProjectRevisionSource } from "./project-revision.store.js"; + +export interface ExecuteExternalInitialImportCommandInput { + projectId: string; + expectedRevision: number; + source: ProjectRevisionSource; + description?: string; + actorId?: string; + historyTargetChangeSetId?: string; + command: ExternalInitialImportProjectCommand; +} + +export interface ExternalInitialImportProjectCommandStore { + execute(input: ExecuteExternalInitialImportCommandInput): { + revision: AppendedProjectRevision; + inverse: ExternalInitialImportProjectCommand; + }; +} diff --git a/src/domain/services/project-command.service.ts b/src/domain/services/project-command.service.ts index 6900c88..ef6a85a 100644 --- a/src/domain/services/project-command.service.ts +++ b/src/domain/services/project-command.service.ts @@ -166,6 +166,11 @@ import { externalCsvConfigurationUpdateCommandType, } from "../models/external-csv-configuration-project-command.model.js"; import type { ExternalCsvConfigurationProjectCommandStore } from "../ports/external-csv-configuration-project-command.store.js"; +import { + assertExternalInitialImportProjectCommand, + externalInitialImportCommandType, +} from "../models/external-initial-import-project-command.model.js"; +import type { ExternalInitialImportProjectCommandStore } from "../ports/external-initial-import-project-command.store.js"; interface DispatchProjectCommandInput { projectId: string; @@ -201,7 +206,8 @@ export class ProjectCommandService implements ProjectCommandExecutor { private readonly circuitGroupSubtreeStore: CircuitGroupSubtreeProjectCommandStore, private readonly circuitProtectionStore: CircuitProtectionProjectCommandStore, private readonly historyStore: ProjectHistoryStore, - private readonly externalCsvConfigurationStore?: ExternalCsvConfigurationProjectCommandStore + private readonly externalCsvConfigurationStore?: ExternalCsvConfigurationProjectCommandStore, + private readonly externalInitialImportStore?: ExternalInitialImportProjectCommandStore ) {} executeUser( @@ -568,6 +574,16 @@ export class ProjectCommandService implements ProjectCommandExecutor { command: input.command, }).revision; } + case externalInitialImportCommandType: { + assertExternalInitialImportProjectCommand(input.command); + if (!this.externalInitialImportStore) { + throw new Error("External initial import store is not available."); + } + return this.externalInitialImportStore.execute({ + ...input, + command: input.command, + }).revision; + } case projectStateRestoreCommandType: { assertProjectStateRestoreCommand(input.command); return this.projectStateRestoreStore.execute({ diff --git a/src/external-model/domain/external-model-contracts.ts b/src/external-model/domain/external-model-contracts.ts index e18d2e5..b06e2fb 100644 --- a/src/external-model/domain/external-model-contracts.ts +++ b/src/external-model/domain/external-model-contracts.ts @@ -30,6 +30,7 @@ export interface ExternalImportBatchSnapshot { fileName: string; sha256: string; appliedProjectRevision: number; + configurationVersion: number; configurationSnapshot: ExternalCsvConfiguration; originalContentBase64: string; document: ExternalCsvDocument; diff --git a/src/server/composition/project-command-stores.ts b/src/server/composition/project-command-stores.ts index c61838a..129ea1c 100644 --- a/src/server/composition/project-command-stores.ts +++ b/src/server/composition/project-command-stores.ts @@ -23,6 +23,7 @@ import { ProjectSettingsProjectCommandRepository } from "../../db/repositories/p import { ProjectStateRestoreCommandRepository } from "../../db/repositories/project-state-restore-command.repository.js"; import { ProjectCommandService } from "../../domain/services/project-command.service.js"; import { ExternalCsvConfigurationProjectCommandRepository } from "../../db/repositories/external-csv-configuration-project-command.repository.js"; +import { ExternalInitialImportProjectCommandRepository } from "../../db/repositories/external-initial-import-project-command.repository.js"; export const circuitProjectCommandStore = new CircuitProjectCommandRepository(db); export const circuitDeviceRowProjectCommandStore = @@ -68,6 +69,8 @@ export const projectStateRestoreCommandStore = export const projectHistoryStore = new ProjectHistoryRepository(db); export const externalCsvConfigurationProjectCommandStore = new ExternalCsvConfigurationProjectCommandRepository(db); +export const externalInitialImportProjectCommandStore = + new ExternalInitialImportProjectCommandRepository(db); export const projectCommandService = new ProjectCommandService( circuitProjectCommandStore, circuitDeviceRowProjectCommandStore, @@ -91,5 +94,6 @@ export const projectCommandService = new ProjectCommandService( circuitGroupSubtreeProjectCommandStore, circuitProtectionProjectCommandStore, projectHistoryStore, - externalCsvConfigurationProjectCommandStore + externalCsvConfigurationProjectCommandStore, + externalInitialImportProjectCommandStore ); diff --git a/tests/external-initial-import-project-command.repository.test.ts b/tests/external-initial-import-project-command.repository.test.ts new file mode 100644 index 0000000..3d129f8 --- /dev/null +++ b/tests/external-initial-import-project-command.repository.test.ts @@ -0,0 +1,273 @@ +import crypto from "node:crypto"; +import path from "node:path"; +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { migrate } from "drizzle-orm/better-sqlite3/migrator"; +import { createDatabaseContext } from "../src/db/database-context.js"; +import { ExternalInitialImportProjectCommandRepository } from "../src/db/repositories/external-initial-import-project-command.repository.js"; +import { ExternalModelStateRepository } from "../src/db/repositories/external-model-state.repository.js"; +import { externalCsvConfigurations } from "../src/db/schema/external-csv-configurations.js"; +import { projects } from "../src/db/schema/projects.js"; +import { + createEmptyExternalModelState, + createExternalInitialImportProjectCommand, +} from "../src/domain/models/external-initial-import-project-command.model.js"; +import { createExternalCsvPreview } from "../src/external-model/application/external-csv-preview.js"; +import { parseExternalCsv } from "../src/external-model/csv/external-csv-transport.js"; +import type { ExternalModelStateSnapshot } from "../src/external-model/domain/external-model-contracts.js"; +import { + createExternalRoomKey, + projectInitialExternalObject, +} from "../src/external-model/domain/external-model-matching.js"; +import { + externalCsvTestConfiguration, + quotedRevitCsv, + utf8Bytes, +} from "./fixtures/revit-csv-fixtures.js"; + +function createTestContext() { + const context = createDatabaseContext(":memory:"); + migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations") }); + context.db.insert(projects).values({ id: "project-1", name: "Projekt" }).run(); + const configuration = configuredCsv(); + context.db.insert(externalCsvConfigurations).values({ + id: "config-1", + projectId: "project-1", + configurationVersion: 1, + configuration, + }).run(); + return { context, configuration }; +} + +function configuredCsv() { + const configuration = structuredClone(externalCsvTestConfiguration); + configuration.familyTypeRules.push( + { + exactFamilyAndType: "Steckdose: Doppelsteckdose", + internalDeviceType: "Steckdose", + connectionKind: "socket", + category: "single_phase", + quantityRule: { kind: "mapped-column" }, + displayNameSuggestion: { kind: "selection-marker" }, + }, + { + exactFamilyAndType: "Steckdose: Standard", + internalDeviceType: "Steckdose", + connectionKind: "socket", + category: "single_phase", + quantityRule: { kind: "fixed", quantity: 1 }, + displayNameSuggestion: { kind: "selection-marker" }, + } + ); + return configuration; +} + +function initialState( + configuration: ReturnType +): ExternalModelStateSnapshot { + const bytes = utf8Bytes(quotedRevitCsv, true); + const preview = createExternalCsvPreview({ + fileName: "revit.csv", + bytes, + configuration, + }); + const roomKeys = [...new Set(preview.objects.map((object) => + createExternalRoomKey(object.roomNumber, object.roomName) + ).filter((key): key is string => key !== null))]; + const roomMappings = roomKeys.map((key, index) => { + const object = preview.objects.find( + (candidate) => createExternalRoomKey(candidate.roomNumber, candidate.roomName) === key + )!; + return { + id: `mapping-${index + 1}`, + projectId: "project-1", + sourceId: "source-1", + normalizedSourceRoomKey: key, + sourceFloorName: null, + sourceRoomNumber: object.roomNumber, + sourceRoomName: object.roomName, + roomId: null, + defaultDistributionBoardId: null, + }; + }); + const mappingByKey = new Map( + roomMappings.map((mapping) => [mapping.normalizedSourceRoomKey, mapping.id]) + ); + return { + source: { + id: "source-1", + projectId: "project-1", + name: "Revit-Gesamtmodell", + sourceType: "revit_csv", + }, + importBatches: [{ + id: "batch-1", + projectId: "project-1", + sourceId: "source-1", + importKind: "initial", + importedAtIso: "2026-08-02T16:00:00.000Z", + fileName: "revit.csv", + sha256: crypto.createHash("sha256").update(bytes).digest("hex"), + appliedProjectRevision: 1, + configurationVersion: 1, + configurationSnapshot: configuration, + originalContentBase64: Buffer.from(bytes).toString("base64"), + document: parseExternalCsv(bytes, configuration), + }], + roomMappings, + objects: preview.objects.map((candidate, index) => { + const projected = projectInitialExternalObject(candidate, configuration); + const roomKey = createExternalRoomKey(candidate.roomNumber, candidate.roomName); + return { + id: `object-${index + 1}`, + projectId: "project-1", + sourceId: "source-1", + ifcGuid: projected.ifcGuid, + lastSeenImportBatchId: "batch-1", + lastAcceptedImportBatchId: "batch-1", + acceptedSourceValues: projected.sourceValues, + planningValues: projected.planningValues, + overriddenFields: [], + externalRoomMappingId: roomKey === null ? null : mappingByKey.get(roomKey)!, + distributionBoardId: null, + linkedProjectDeviceId: null, + circuitDeviceRowId: null, + presenceStatus: "present", + }; + }), + }; +} + +describe("external initial import project command", () => { + it("commits the complete import and supports persisted undo and redo", () => { + const { context, configuration } = createTestContext(); + try { + const target = initialState(configuration); + const repository = new ExternalInitialImportProjectCommandRepository(context.db); + const inserted = repository.execute({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + description: "Revit-Erstimport übernehmen", + command: createExternalInitialImportProjectCommand( + createEmptyExternalModelState(), + target + ), + }); + assert.equal(inserted.revision.revisionNumber, 1); + assert.equal( + new ExternalModelStateRepository(context.db).getByProject("project-1") + .state.objects.length, + 2 + ); + assert.ok( + new ExternalModelStateRepository(context.db).getByProject("project-1") + .state.objects.every((object) => object.circuitDeviceRowId === null) + ); + + const undone = repository.execute({ + projectId: "project-1", + expectedRevision: 1, + source: "undo", + historyTargetChangeSetId: inserted.revision.changeSetId, + command: inserted.inverse, + }); + assert.equal(undone.revision.revisionNumber, 2); + assert.equal( + new ExternalModelStateRepository(context.db).getByProject("project-1") + .state.source, + null + ); + + repository.execute({ + projectId: "project-1", + expectedRevision: 2, + source: "redo", + historyTargetChangeSetId: inserted.revision.changeSetId, + command: undone.inverse, + }); + const redone = new ExternalModelStateRepository(context.db) + .getByProject("project-1").state; + assert.deepEqual(redone.objects.map((object) => object.id), ["object-1", "object-2"]); + assert.equal(redone.importBatches[0].configurationVersion, 1); + } finally { + context.close(); + } + }); + + it("rejects changed state, checksum drift and row assignment", () => { + const { context, configuration } = createTestContext(); + try { + const target = initialState(configuration); + const repository = new ExternalInitialImportProjectCommandRepository(context.db); + const assigned = structuredClone(target); + assigned.objects[0].circuitDeviceRowId = "row-1"; + assert.throws( + () => createExternalInitialImportProjectCommand( + createEmptyExternalModelState(), + assigned + ), + /must not assign circuit device rows/ + ); + + const invalidHash = structuredClone(target); + invalidHash.importBatches[0].sha256 = "b".repeat(64); + assert.throws( + () => repository.execute({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + command: createExternalInitialImportProjectCommand( + createEmptyExternalModelState(), + invalidHash + ), + }), + /checksum is invalid/ + ); + assert.equal( + new ExternalModelStateRepository(context.db).getByProject("project-1") + .state.source, + null + ); + } finally { + context.close(); + } + }); + + it("rolls back all external rows when late history persistence fails", () => { + const { context, configuration } = createTestContext(); + try { + context.sqlite.exec(` + CREATE TRIGGER fail_external_import_history + BEFORE INSERT ON project_change_sets + BEGIN + SELECT RAISE(ABORT, 'late history failure'); + END; + `); + const repository = new ExternalInitialImportProjectCommandRepository(context.db); + assert.throws( + () => repository.execute({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + command: createExternalInitialImportProjectCommand( + createEmptyExternalModelState(), + initialState(configuration) + ), + }), + /late history failure/ + ); + assert.equal( + new ExternalModelStateRepository(context.db).getByProject("project-1") + .state.source, + null + ); + assert.equal( + context.db.select({ revision: projects.currentRevision }).from(projects).get()?.revision, + 0 + ); + } finally { + context.close(); + } + }); +}); diff --git a/tests/external-model-foundation.test.ts b/tests/external-model-foundation.test.ts index 4bc6708..46eed56 100644 --- a/tests/external-model-foundation.test.ts +++ b/tests/external-model-foundation.test.ts @@ -126,6 +126,7 @@ describe("external model state repository", () => { fileName: "revit.csv", sha256: "hash", appliedProjectRevision: 1, + configurationVersion: 1, configurationSnapshot: configuration, originalBytes: Buffer.from(bytes), document, @@ -388,6 +389,7 @@ function insertLinkedExternalFixture(database: ReturnType Date: Sun, 2 Aug 2026 17:38:17 +0200 Subject: [PATCH 40/59] Add Revit initial import planning --- AGENTS.md | 11 +- docs/current-architecture.md | 9 + .../spec/revit-csv-phase-14-audit-and-plan.md | 8 +- .../external-initial-import-plan.ts | 164 ++++++++++++++++++ .../composition/application-repositories.ts | 2 + .../controllers/external-csv.controller.ts | 63 ++++++- src/server/routes/project.routes.ts | 5 + src/shared/validation/external-csv.schemas.ts | 2 + tests/external-csv-api-contract.test.ts | 16 ++ tests/external-initial-import-plan.test.ts | 69 ++++++++ 10 files changed, 345 insertions(+), 4 deletions(-) create mode 100644 src/external-model/application/external-initial-import-plan.ts create mode 100644 tests/external-initial-import-plan.test.ts diff --git a/AGENTS.md b/AGENTS.md index 9418415..3dbce5b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -287,8 +287,15 @@ Confirmed initial state is written only through original-byte SHA-256, parsed matrix, complete IFCGUID/source values, explicit planning overrides and internal project links in the shared transaction. It never creates or links a CircuitDeviceRow. Its exact inverse removes the whole -unchanged external state, and Redo restores the same UUIDs and bytes. No public -apply endpoint or staging wizard invokes this command yet. +unchanged external state, and Redo restores the same UUIDs and bytes. No +dedicated file-and-decisions apply endpoint or staging wizard invokes this +command yet. +`POST /api/projects/:projectId/external-csv/initial-import/plan` is a stateless +read path for the first wizard stage. It rejects projects with an existing +external source and returns source-room groups, exact room-number suggestions, +exact family/type groups, projected object values, issue counts and the current +floor/room/board/ProjectDevice catalogs. It creates no draft, revision or +domain row; the decision UI and public apply endpoint are still pending. The central revision boundary creates an automatic logical snapshot after each 25 new revisions and retains only the newest 12 automatic snapshots per project. Named snapshots are never removed by this retention policy. diff --git a/docs/current-architecture.md b/docs/current-architecture.md index f5ea277..45fd6cf 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -537,6 +537,15 @@ Konfigurationsstand serverseitig und verwendet den typisierten Command. Base64-kodierte CSV bis 18 MB entgegen. Der zustandsfreie Service liefert Hash, Dialekt, Header, Klassifikationszahlen, Verdachtszeilen und gemappte Objektquellwerte. Er schreibt weder Entwurf noch Projektdaten. +`POST /api/projects/:projectId/external-csv/initial-import/plan` verwendet +denselben begrenzten Transport, setzt eine gespeicherte Konfiguration und einen +noch leeren externen Projektzustand voraus und bleibt ebenfalls zustandsfrei. +Die Antwort gruppiert Quellräume, schlägt bei genau einer identischen +Raumnummer den vorhandenen Raum vor, gruppiert exakte Familie-und-Typ-Werte, +liefert regelbasierte Objektplanungswerte und Konfliktzahlen und enthält die +vorhandenen Ebenen, Räume, Verteilungen und ProjectDevices als Auswahlkataloge. +Unbekannte Familien, ungültige Leistungen/Mengen und Objekte ohne Raum bleiben +sichtbare Warnungen; es wird keine Entscheidung automatisch bestätigt. Die Projektseite öffnet über „Revit-CSV“ ein deutsches Modal für Dialekt, Spaltenzuordnung, zusätzliche Quellfelder und exakte Familie-und-Typ-Regeln. Nach dem revisionierten Speichern kann eine lokale CSV gewählt und über den diff --git a/docs/spec/revit-csv-phase-14-audit-and-plan.md b/docs/spec/revit-csv-phase-14-audit-and-plan.md index 0388c3a..c4afaa5 100644 --- a/docs/spec/revit-csv-phase-14-audit-and-plan.md +++ b/docs/spec/revit-csv-phase-14-audit-and-plan.md @@ -288,7 +288,13 @@ einen Commit aufgenommen. die zuvor noch fehlende monotone Konfigurationsversion am Batch; `0003` bleibt unverändert. 4. Raum-, Verteilungs-, Klassifizierungs- und ProjectDevice-Schritte im Wizard - ergänzen; der Import selbst erzeugt keine CircuitDeviceRow. + ergänzen; der Import selbst erzeugt keine CircuitDeviceRow. **Begonnen:** + Der zustandsfreie Planungs-Endpunkt liefert gruppierte Quellräume mit + exaktem Raumnummernvorschlag, Familie-und-Typ-Gruppen, Objektvorschläge, + Warnungszahlen sowie vorhandene Ebenen, Räume, Verteilungen und + ProjectDevices. Er blockiert nach einem bereits bestätigten Erstimport und + schreibt weder Entwurf noch Projektzustand. Entscheidungs-UI und Apply- + Endpunkt stehen noch aus. Vor Beginn werden die oben vorgeschlagenen Namen, die erneute Dateiübertragung statt serverseitiger Entwürfe, die Konfigurationsversionierung und die diff --git a/src/external-model/application/external-initial-import-plan.ts b/src/external-model/application/external-initial-import-plan.ts new file mode 100644 index 0000000..bc916f8 --- /dev/null +++ b/src/external-model/application/external-initial-import-plan.ts @@ -0,0 +1,164 @@ +import type { ExternalCsvConfiguration } from "../csv/external-csv-contracts.js"; +import { + createExternalRoomKey, + normalizeExternalRoomPart, + projectInitialExternalObject, +} from "../domain/external-model-matching.js"; +import { createExternalCsvPreview } from "./external-csv-preview.js"; + +interface ExistingRoom { + id: string; + floorId: string | null; + roomNumber: string; + roomName: string; +} + +interface ExistingFloor { + id: string; + name: string; + sortOrder: number; +} + +interface ExistingDistributionBoard { + id: string; + name: string; + floorId: string | null; +} + +interface ExistingProjectDevice { + id: string; + name: string; + displayName: string; + category: string | null; + connectionKind: string | null; + powerPerUnit: number; +} + +export function createExternalInitialImportPlan(input: { + fileName: string; + bytes: Uint8Array; + configurationVersion: number; + configuration: ExternalCsvConfiguration; + rooms: ExistingRoom[]; + floors: ExistingFloor[]; + distributionBoards: ExistingDistributionBoard[]; + projectDevices: ExistingProjectDevice[]; +}) { + const preview = createExternalCsvPreview(input); + const roomCandidatesByNumber = new Map(); + for (const room of input.rooms) { + const key = normalizeExternalRoomPart(room.roomNumber); + if (!key) continue; + const candidates = roomCandidatesByNumber.get(key) ?? []; + candidates.push(room); + roomCandidatesByNumber.set(key, candidates); + } + + const objects = preview.objects.map((object) => { + const projected = projectInitialExternalObject(object, input.configuration); + return { + ifcGuid: projected.ifcGuid, + sourceRoomKey: createExternalRoomKey(object.roomNumber, object.roomName), + sourceValues: projected.sourceValues, + suggestedPlanningValues: projected.planningValues, + issues: projected.issues, + }; + }); + const sourceRooms = new Map(); + for (const object of objects) { + if (object.sourceRoomKey === null) continue; + const existing = sourceRooms.get(object.sourceRoomKey); + if (existing) { + existing.objectCount += 1; + } else { + sourceRooms.set(object.sourceRoomKey, { + sourceRoomKey: object.sourceRoomKey, + roomNumber: object.sourceValues.roomNumber, + roomName: object.sourceValues.roomName, + objectCount: 1, + }); + } + } + const roomPlans = [...sourceRooms.values()] + .map((room) => { + const candidates = roomCandidatesByNumber.get( + normalizeExternalRoomPart(room.roomNumber) + ) ?? []; + return { + ...room, + suggestedRoomId: candidates.length === 1 ? candidates[0].id : null, + matchStatus: + candidates.length === 1 + ? "exact-number" + : candidates.length > 1 + ? "ambiguous-number" + : "new", + candidateRoomIds: candidates.map((candidate) => candidate.id), + } as const; + }) + .sort((left, right) => + left.roomNumber.localeCompare(right.roomNumber, "de", { numeric: true }) || + left.roomName.localeCompare(right.roomName, "de") + ); + + const familyGroups = new Map(); + for (const object of objects) { + const key = object.sourceValues.familyAndType; + const group = familyGroups.get(key); + if (group) { + group.objectCount += 1; + } else { + familyGroups.set(key, { + familyAndType: key, + objectCount: 1, + classified: !object.issues.includes("unknown-family-and-type"), + category: object.suggestedPlanningValues.category, + connectionKind: object.suggestedPlanningValues.connectionKind, + internalDeviceType: object.suggestedPlanningValues.internalDeviceType, + }); + } + } + + return { + configurationVersion: input.configurationVersion, + fileName: preview.fileName, + sha256: preview.sha256, + byteLength: preview.byteLength, + rowCount: preview.rowCount, + objectCount: preview.objectCount, + passthroughCount: preview.passthroughCount, + suspectObjectCount: preview.suspectObjectCount, + suspectRowNumbers: preview.suspectRowNumbers, + sourceRooms: roomPlans, + familyGroups: [...familyGroups.values()].sort((left, right) => + left.familyAndType.localeCompare(right.familyAndType, "de") + ), + issueCounts: { + unknownFamilyAndType: objects.filter((object) => + object.issues.includes("unknown-family-and-type") + ).length, + invalidPower: objects.filter((object) => object.issues.includes("invalid-power")).length, + invalidQuantity: objects.filter((object) => object.issues.includes("invalid-quantity")).length, + missingRoom: objects.filter((object) => object.issues.includes("missing-room")).length, + }, + objects, + existing: { + floors: input.floors, + rooms: input.rooms, + distributionBoards: input.distributionBoards, + projectDevices: input.projectDevices, + }, + }; +} diff --git a/src/server/composition/application-repositories.ts b/src/server/composition/application-repositories.ts index ebeaf5f..a3a9457 100644 --- a/src/server/composition/application-repositories.ts +++ b/src/server/composition/application-repositories.ts @@ -12,6 +12,7 @@ import { ProjectDeviceRepository } from "../../db/repositories/project-device.re import { ProjectRepository } from "../../db/repositories/project.repository.js"; import { RoomRepository } from "../../db/repositories/room.repository.js"; import { ExternalCsvConfigurationRepository } from "../../db/repositories/external-csv-configuration.repository.js"; +import { ExternalModelStateRepository } from "../../db/repositories/external-model-state.repository.js"; export const circuitDeviceRowRepository = new CircuitDeviceRowRepository(db); @@ -31,3 +32,4 @@ export const projectRepository = new ProjectRepository(db); export const roomRepository = new RoomRepository(db); export const externalCsvConfigurationRepository = new ExternalCsvConfigurationRepository(db); +export const externalModelStateRepository = new ExternalModelStateRepository(db); diff --git a/src/server/controllers/external-csv.controller.ts b/src/server/controllers/external-csv.controller.ts index 592bcd0..0fd0a83 100644 --- a/src/server/controllers/external-csv.controller.ts +++ b/src/server/controllers/external-csv.controller.ts @@ -3,12 +3,21 @@ import type { Request, Response } from "express"; import { assertExternalCsvConfiguration } from "../../external-model/csv/external-csv-configuration.js"; import { ExternalCsvParseError } from "../../external-model/csv/external-csv-transport.js"; import { createExternalCsvPreview } from "../../external-model/application/external-csv-preview.js"; +import { createExternalInitialImportPlan } from "../../external-model/application/external-initial-import-plan.js"; import { createExternalCsvConfigurationUpdateProjectCommand } from "../../domain/models/external-csv-configuration-project-command.model.js"; import { previewExternalCsvSchema, + planExternalInitialImportSchema, updateExternalCsvConfigurationSchema, } from "../../shared/validation/external-csv.schemas.js"; -import { externalCsvConfigurationRepository } from "../composition/application-repositories.js"; +import { + distributionBoardRepository, + externalCsvConfigurationRepository, + externalModelStateRepository, + floorRepository, + projectDeviceRepository, + roomRepository, +} from "../composition/application-repositories.js"; import { projectCommandService } from "../composition/project-command-stores.js"; import { respondWithProjectCommandError } from "./project-command.controller.js"; @@ -93,6 +102,58 @@ export function previewExternalCsv(req: Request, res: Response) { } } +export async function planExternalInitialImport(req: Request, res: Response) { + const projectId = getProjectId(req, res); + if (!projectId) return; + const parsed = planExternalInitialImportSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ error: parsed.error.flatten() }); + } + const stored = externalCsvConfigurationRepository.getByProject(projectId); + if (!stored.projectExists) { + return res.status(404).json({ error: "Project not found" }); + } + if (!stored.configuration) { + return res.status(409).json({ + error: "Für das Projekt ist noch keine Revit-CSV-Konfiguration gespeichert.", + code: "EXTERNAL_CSV_CONFIGURATION_REQUIRED", + }); + } + const externalState = externalModelStateRepository.getByProject(projectId); + if (externalState.state.source !== null) { + return res.status(409).json({ + error: "Für dieses Projekt wurde bereits ein Revit-Modell importiert.", + code: "EXTERNAL_INITIAL_IMPORT_ALREADY_APPLIED", + }); + } + try { + const bytes = decodeBase64(parsed.data.contentBase64); + const [floors, rooms, distributionBoards, projectDevices] = await Promise.all([ + floorRepository.listByProject(projectId), + roomRepository.listByProject(projectId), + distributionBoardRepository.listByProject(projectId), + projectDeviceRepository.listByProject(projectId), + ]); + return res.json(createExternalInitialImportPlan({ + fileName: parsed.data.fileName, + bytes, + configurationVersion: stored.configuration.configurationVersion, + configuration: stored.configuration.configuration, + floors, + rooms, + distributionBoards, + projectDevices, + })); + } catch (error) { + if (error instanceof ExternalCsvParseError) { + return res.status(400).json({ error: error.message, code: error.code }); + } + return res.status(400).json({ + error: error instanceof Error ? error.message : "Erstimport-Planung fehlgeschlagen.", + }); + } +} + function decodeBase64(value: string) { if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value) || value.length % 4 !== 0) { throw new Error("CSV-Inhalt ist nicht gültig Base64-kodiert."); diff --git a/src/server/routes/project.routes.ts b/src/server/routes/project.routes.ts index 89909d0..133af1f 100644 --- a/src/server/routes/project.routes.ts +++ b/src/server/routes/project.routes.ts @@ -38,6 +38,7 @@ import { import { getExternalCsvConfiguration, previewExternalCsv, + planExternalInitialImport, updateExternalCsvConfiguration, } from "../controllers/external-csv.controller.js"; @@ -52,6 +53,10 @@ projectRouter.post("/:projectId/import", importProjectTransfer); projectRouter.get("/:projectId/external-csv/configuration", getExternalCsvConfiguration); projectRouter.put("/:projectId/external-csv/configuration", updateExternalCsvConfiguration); projectRouter.post("/:projectId/external-csv/preview", previewExternalCsv); +projectRouter.post( + "/:projectId/external-csv/initial-import/plan", + planExternalInitialImport +); projectRouter.get("/:projectId/history", getProjectHistory); projectRouter.get("/:projectId/history/revisions", listProjectRevisions); projectRouter.post("/:projectId/commands", executeProjectCommand); diff --git a/src/shared/validation/external-csv.schemas.ts b/src/shared/validation/external-csv.schemas.ts index 5650b09..7842964 100644 --- a/src/shared/validation/external-csv.schemas.ts +++ b/src/shared/validation/external-csv.schemas.ts @@ -13,3 +13,5 @@ export const previewExternalCsvSchema = z contentBase64: z.string().min(1).max(24_000_000), }) .strict(); + +export const planExternalInitialImportSchema = previewExternalCsvSchema; diff --git a/tests/external-csv-api-contract.test.ts b/tests/external-csv-api-contract.test.ts index 261f962..3b3591e 100644 --- a/tests/external-csv-api-contract.test.ts +++ b/tests/external-csv-api-contract.test.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { + planExternalInitialImportSchema, previewExternalCsvSchema, updateExternalCsvConfigurationSchema, } from "../src/shared/validation/external-csv.schemas.js"; @@ -23,6 +24,13 @@ describe("external CSV API contracts", () => { }).success, true ); + assert.equal( + planExternalInitialImportSchema.safeParse({ + fileName: "revit.csv", + contentBase64: "YWJj", + }).success, + true + ); }); it("rejects stale-shaped, oversized and unknown request fields", () => { @@ -41,6 +49,14 @@ describe("external CSV API contracts", () => { }).success, false ); + assert.equal( + planExternalInitialImportSchema.safeParse({ + fileName: "revit.csv", + contentBase64: "YWJj", + decisions: [], + }).success, + false + ); assert.equal( previewExternalCsvSchema.safeParse({ fileName: "revit.csv", diff --git a/tests/external-initial-import-plan.test.ts b/tests/external-initial-import-plan.test.ts new file mode 100644 index 0000000..32c70dc --- /dev/null +++ b/tests/external-initial-import-plan.test.ts @@ -0,0 +1,69 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { createExternalInitialImportPlan } from "../src/external-model/application/external-initial-import-plan.js"; +import { + externalCsvTestConfiguration, + firstIfcGuid, + quotedRevitCsv, + utf8Bytes, +} from "./fixtures/revit-csv-fixtures.js"; + +describe("external initial import plan", () => { + it("groups source rooms and exposes exact room and classification suggestions", () => { + const configuration = structuredClone(externalCsvTestConfiguration); + configuration.familyTypeRules.push({ + exactFamilyAndType: "Steckdose: Doppelsteckdose", + internalDeviceType: "Steckdose", + connectionKind: "socket", + category: "single_phase", + quantityRule: { kind: "mapped-column" }, + displayNameSuggestion: { kind: "selection-marker" }, + }); + + const plan = createExternalInitialImportPlan({ + fileName: "revit.csv", + bytes: utf8Bytes(quotedRevitCsv, true), + configurationVersion: 4, + configuration, + floors: [{ id: "floor-1", name: "EG", sortOrder: 10 }], + rooms: [{ + id: "room-1", + floorId: "floor-1", + roomNumber: "01/101", + roomName: "Technik Bestand", + }], + distributionBoards: [{ id: "board-1", name: "UV 1", floorId: "floor-1" }], + projectDevices: [{ + id: "device-1", + name: "socket", + displayName: "Steckdose", + category: "single_phase", + connectionKind: "socket", + powerPerUnit: 0.12, + }], + }); + + assert.equal(plan.configurationVersion, 4); + assert.equal(plan.objectCount, 2); + assert.equal(plan.sourceRooms.length, 2); + assert.deepEqual( + plan.sourceRooms.map((room) => ({ + number: room.roomNumber, + status: room.matchStatus, + suggested: room.suggestedRoomId, + })), + [ + { number: "01/101", status: "exact-number", suggested: "room-1" }, + { number: "01/103", status: "new", suggested: null }, + ] + ); + assert.equal(plan.familyGroups.length, 2); + assert.equal(plan.issueCounts.unknownFamilyAndType, 1); + assert.equal(plan.issueCounts.invalidPower, 0); + assert.equal(plan.issueCounts.invalidQuantity, 0); + assert.equal(plan.objects[0].ifcGuid, firstIfcGuid); + assert.equal(plan.objects[0].suggestedPlanningValues.effectiveQuantity, 2); + assert.equal(plan.existing.distributionBoards[0].id, "board-1"); + assert.equal(plan.existing.projectDevices[0].id, "device-1"); + }); +}); From 8646f346e624b6ce7723ec5ac04711ea18feddd4 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 2 Aug 2026 17:42:29 +0200 Subject: [PATCH 41/59] Add Revit initial import apply API --- AGENTS.md | 11 +- docs/current-architecture.md | 9 ++ .../spec/revit-csv-phase-14-audit-and-plan.md | 8 +- .../external-initial-import-target.ts | 130 ++++++++++++++++++ .../controllers/external-csv.controller.ts | 97 +++++++++++++ src/server/routes/project.routes.ts | 5 + src/shared/validation/external-csv.schemas.ts | 18 +++ tests/external-csv-api-contract.test.ts | 14 ++ tests/external-initial-import-plan.test.ts | 80 +++++++++++ 9 files changed, 370 insertions(+), 2 deletions(-) create mode 100644 src/external-model/application/external-initial-import-target.ts diff --git a/AGENTS.md b/AGENTS.md index 3dbce5b..b892a9a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -295,7 +295,16 @@ read path for the first wizard stage. It rejects projects with an existing external source and returns source-room groups, exact room-number suggestions, exact family/type groups, projected object values, issue counts and the current floor/room/board/ProjectDevice catalogs. It creates no draft, revision or -domain row; the decision UI and public apply endpoint are still pending. +domain row; the decision UI is still pending. +The dedicated +`POST /api/projects/:projectId/external-csv/initial-import/apply` endpoint +retransmits and replans the file against the expected hash, configuration +version and project revision. It requires one explicit decision per source-room +and exact family/type group, blocks unclassified families, creates all stable +external UUIDs server-side and invokes `external-import.apply-initial`. +Room/default-board and optional ProjectDevice links are supported; CSV circuit +values remain source-only and every `circuitDeviceRowId` remains null. The +decision UI is still pending. The central revision boundary creates an automatic logical snapshot after each 25 new revisions and retains only the newest 12 automatic snapshots per project. Named snapshots are never removed by this retention policy. diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 45fd6cf..fd94776 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -546,6 +546,15 @@ liefert regelbasierte Objektplanungswerte und Konfliktzahlen und enthält die vorhandenen Ebenen, Räume, Verteilungen und ProjectDevices als Auswahlkataloge. Unbekannte Familien, ungültige Leistungen/Mengen und Objekte ohne Raum bleiben sichtbare Warnungen; es wird keine Entscheidung automatisch bestätigt. +`POST /api/projects/:projectId/external-csv/initial-import/apply` nimmt Datei, +erwarteten Hash, Konfigurationsversion und Projektrevision erneut entgegen. +Vollständige Entscheidungen ordnen jeden Quellraum optional einem vorhandenen +Raum und einer Standardverteilung sowie jede exakte Familiengruppe optional +einem vorhandenen ProjectDevice zu. Unklassifizierte Familien oder fehlende +Gruppenentscheidungen blockieren. Der Server parst und plant erneut, erzeugt +stabile interne UUIDs und übergibt den vollständigen Zustand an +`external-import.apply-initial`; CSV-Stromkreiswerte bleiben reine Quellwerte +und alle `circuitDeviceRowId` bleiben `null`. Die Projektseite öffnet über „Revit-CSV“ ein deutsches Modal für Dialekt, Spaltenzuordnung, zusätzliche Quellfelder und exakte Familie-und-Typ-Regeln. Nach dem revisionierten Speichern kann eine lokale CSV gewählt und über den diff --git a/docs/spec/revit-csv-phase-14-audit-and-plan.md b/docs/spec/revit-csv-phase-14-audit-and-plan.md index c4afaa5..645c42d 100644 --- a/docs/spec/revit-csv-phase-14-audit-and-plan.md +++ b/docs/spec/revit-csv-phase-14-audit-and-plan.md @@ -294,7 +294,13 @@ einen Commit aufgenommen. Warnungszahlen sowie vorhandene Ebenen, Räume, Verteilungen und ProjectDevices. Er blockiert nach einem bereits bestätigten Erstimport und schreibt weder Entwurf noch Projektzustand. Entscheidungs-UI und Apply- - Endpunkt stehen noch aus. + Endpunkt stehen noch aus. **Backend-Apply erledigt:** Der dedizierte + Endpunkt verlangt denselben Datei-Hash und Konfigurationsstand wie die + Planung, vollständige Entscheidungen für alle Quellräume und exakten + Familie-und-Typ-Gruppen und baut daraus serverseitig stabile UUIDs. Nicht + klassifizierte Familien blockieren. Raum-/Standardverteiler- und optionale + ProjectDevice-Links werden bestätigt, Stromkreis- und Row-Links bleiben + ausgeschlossen. Die Entscheidungs-UI steht noch aus. Vor Beginn werden die oben vorgeschlagenen Namen, die erneute Dateiübertragung statt serverseitiger Entwürfe, die Konfigurationsversionierung und die diff --git a/src/external-model/application/external-initial-import-target.ts b/src/external-model/application/external-initial-import-target.ts new file mode 100644 index 0000000..be0925e --- /dev/null +++ b/src/external-model/application/external-initial-import-target.ts @@ -0,0 +1,130 @@ +import type { ExternalCsvConfiguration, ExternalCsvDocument } from "../csv/external-csv-contracts.js"; +import type { ExternalModelStateSnapshot } from "../domain/external-model-contracts.js"; +import type { createExternalInitialImportPlan } from "./external-initial-import-plan.js"; + +type ExternalInitialImportPlan = ReturnType; + +export interface ExternalRoomDecision { + sourceRoomKey: string; + roomId: string | null; + defaultDistributionBoardId: string | null; +} + +export interface ExternalFamilyProjectDeviceDecision { + familyAndType: string; + projectDeviceId: string | null; +} + +export function buildExternalInitialImportTarget(input: { + projectId: string; + expectedRevision: number; + sourceName: string; + importedAtIso: string; + configurationVersion: number; + configuration: ExternalCsvConfiguration; + originalContentBase64: string; + document: ExternalCsvDocument; + plan: ExternalInitialImportPlan; + roomDecisions: ExternalRoomDecision[]; + familyProjectDeviceDecisions: ExternalFamilyProjectDeviceDecision[]; + createId: () => string; +}): ExternalModelStateSnapshot { + if (input.plan.familyGroups.some((group) => !group.classified)) { + throw new Error("Alle Familie-und-Typ-Werte müssen vor dem Erstimport klassifiziert sein."); + } + const roomDecisionByKey = exactDecisionMap( + input.plan.sourceRooms.map((room) => room.sourceRoomKey), + input.roomDecisions, + (decision) => decision.sourceRoomKey, + "Quellraum" + ); + const familyDecisionByName = exactDecisionMap( + input.plan.familyGroups.map((group) => group.familyAndType), + input.familyProjectDeviceDecisions, + (decision) => decision.familyAndType, + "Familie und Typ" + ); + const sourceId = input.createId(); + const batchId = input.createId(); + const roomMappingIdByKey = new Map( + input.plan.sourceRooms.map((room) => [room.sourceRoomKey, input.createId()]) + ); + return { + source: { + id: sourceId, + projectId: input.projectId, + name: input.sourceName.trim(), + sourceType: "revit_csv", + }, + importBatches: [{ + id: batchId, + projectId: input.projectId, + sourceId, + importKind: "initial", + importedAtIso: input.importedAtIso, + fileName: input.plan.fileName, + sha256: input.plan.sha256, + appliedProjectRevision: input.expectedRevision + 1, + configurationVersion: input.configurationVersion, + configurationSnapshot: input.configuration, + originalContentBase64: input.originalContentBase64, + document: input.document, + }], + roomMappings: input.plan.sourceRooms.map((room) => { + const decision = roomDecisionByKey.get(room.sourceRoomKey)!; + return { + id: roomMappingIdByKey.get(room.sourceRoomKey)!, + projectId: input.projectId, + sourceId, + normalizedSourceRoomKey: room.sourceRoomKey, + sourceFloorName: null, + sourceRoomNumber: room.roomNumber, + sourceRoomName: room.roomName, + roomId: decision.roomId, + defaultDistributionBoardId: decision.defaultDistributionBoardId, + }; + }), + objects: input.plan.objects.map((object) => ({ + id: input.createId(), + projectId: input.projectId, + sourceId, + ifcGuid: object.ifcGuid, + lastSeenImportBatchId: batchId, + lastAcceptedImportBatchId: batchId, + acceptedSourceValues: object.sourceValues, + planningValues: object.suggestedPlanningValues, + overriddenFields: [], + externalRoomMappingId: + object.sourceRoomKey === null + ? null + : roomMappingIdByKey.get(object.sourceRoomKey)!, + distributionBoardId: null, + linkedProjectDeviceId: + familyDecisionByName.get(object.sourceValues.familyAndType)! + .projectDeviceId, + circuitDeviceRowId: null, + presenceStatus: "present", + })), + }; +} + +function exactDecisionMap( + expectedKeys: string[], + decisions: T[], + keyOf: (decision: T) => string, + label: string +) { + const expected = new Set(expectedKeys); + const result = new Map(); + for (const decision of decisions) { + const key = keyOf(decision); + if (!expected.has(key) || result.has(key)) { + throw new Error(`${label}-Entscheidungen sind unvollständig oder doppelt.`); + } + result.set(key, decision); + } + if (result.size !== expected.size) { + throw new Error(`${label}-Entscheidungen sind unvollständig oder doppelt.`); + } + return result; +} diff --git a/src/server/controllers/external-csv.controller.ts b/src/server/controllers/external-csv.controller.ts index 0fd0a83..986d7de 100644 --- a/src/server/controllers/external-csv.controller.ts +++ b/src/server/controllers/external-csv.controller.ts @@ -4,10 +4,17 @@ import { assertExternalCsvConfiguration } from "../../external-model/csv/externa import { ExternalCsvParseError } from "../../external-model/csv/external-csv-transport.js"; import { createExternalCsvPreview } from "../../external-model/application/external-csv-preview.js"; import { createExternalInitialImportPlan } from "../../external-model/application/external-initial-import-plan.js"; +import { buildExternalInitialImportTarget } from "../../external-model/application/external-initial-import-target.js"; +import { parseExternalCsv } from "../../external-model/csv/external-csv-transport.js"; +import { + createEmptyExternalModelState, + createExternalInitialImportProjectCommand, +} from "../../domain/models/external-initial-import-project-command.model.js"; import { createExternalCsvConfigurationUpdateProjectCommand } from "../../domain/models/external-csv-configuration-project-command.model.js"; import { previewExternalCsvSchema, planExternalInitialImportSchema, + applyExternalInitialImportSchema, updateExternalCsvConfigurationSchema, } from "../../shared/validation/external-csv.schemas.js"; import { @@ -154,6 +161,96 @@ export async function planExternalInitialImport(req: Request, res: Response) { } } +export async function applyExternalInitialImport(req: Request, res: Response) { + const projectId = getProjectId(req, res); + if (!projectId) return; + const parsed = applyExternalInitialImportSchema.safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ error: parsed.error.flatten() }); + } + const stored = externalCsvConfigurationRepository.getByProject(projectId); + if (!stored.projectExists) { + return res.status(404).json({ error: "Project not found" }); + } + if (!stored.configuration) { + return res.status(409).json({ + error: "Für das Projekt ist noch keine Revit-CSV-Konfiguration gespeichert.", + code: "EXTERNAL_CSV_CONFIGURATION_REQUIRED", + }); + } + if ( + stored.configuration.configurationVersion !== + parsed.data.expectedConfigurationVersion + ) { + return res.status(409).json({ + error: "Die Revit-CSV-Konfiguration wurde seit der Planung geändert.", + code: "EXTERNAL_CSV_CONFIGURATION_CHANGED", + }); + } + try { + const bytes = decodeBase64(parsed.data.contentBase64); + const [floors, rooms, distributionBoards, projectDevices] = await Promise.all([ + floorRepository.listByProject(projectId), + roomRepository.listByProject(projectId), + distributionBoardRepository.listByProject(projectId), + projectDeviceRepository.listByProject(projectId), + ]); + const plan = createExternalInitialImportPlan({ + fileName: parsed.data.fileName, + bytes, + configurationVersion: stored.configuration.configurationVersion, + configuration: stored.configuration.configuration, + floors, + rooms, + distributionBoards, + projectDevices, + }); + if (plan.sha256 !== parsed.data.expectedSha256) { + return res.status(409).json({ + error: "Die CSV-Datei stimmt nicht mehr mit der geprüften Planung überein.", + code: "EXTERNAL_CSV_FILE_CHANGED", + }); + } + const target = buildExternalInitialImportTarget({ + projectId, + expectedRevision: parsed.data.expectedRevision, + sourceName: parsed.data.sourceName, + importedAtIso: new Date().toISOString(), + configurationVersion: stored.configuration.configurationVersion, + configuration: stored.configuration.configuration, + originalContentBase64: parsed.data.contentBase64, + document: parseExternalCsv(bytes, stored.configuration.configuration), + plan, + roomDecisions: parsed.data.roomDecisions, + familyProjectDeviceDecisions: + parsed.data.familyProjectDeviceDecisions, + createId: () => crypto.randomUUID(), + }); + const result = projectCommandService.executeUser({ + projectId, + expectedRevision: parsed.data.expectedRevision, + description: `Revit-Erstimport ${parsed.data.fileName}`, + command: createExternalInitialImportProjectCommand( + createEmptyExternalModelState(), + target + ), + }); + return res.status(201).json({ + ...result, + import: { + sourceId: target.source!.id, + importBatchId: target.importBatches[0].id, + objectCount: target.objects.length, + }, + }); + } catch (error) { + if (error instanceof ExternalCsvParseError) { + return res.status(400).json({ error: error.message, code: error.code }); + } + return respondWithProjectCommandError(error, res); + } +} + function decodeBase64(value: string) { if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value) || value.length % 4 !== 0) { throw new Error("CSV-Inhalt ist nicht gültig Base64-kodiert."); diff --git a/src/server/routes/project.routes.ts b/src/server/routes/project.routes.ts index 133af1f..2ac20a5 100644 --- a/src/server/routes/project.routes.ts +++ b/src/server/routes/project.routes.ts @@ -37,6 +37,7 @@ import { } from "../controllers/project-transfer.controller.js"; import { getExternalCsvConfiguration, + applyExternalInitialImport, previewExternalCsv, planExternalInitialImport, updateExternalCsvConfiguration, @@ -57,6 +58,10 @@ projectRouter.post( "/:projectId/external-csv/initial-import/plan", planExternalInitialImport ); +projectRouter.post( + "/:projectId/external-csv/initial-import/apply", + applyExternalInitialImport +); projectRouter.get("/:projectId/history", getProjectHistory); projectRouter.get("/:projectId/history/revisions", listProjectRevisions); projectRouter.post("/:projectId/commands", executeProjectCommand); diff --git a/src/shared/validation/external-csv.schemas.ts b/src/shared/validation/external-csv.schemas.ts index 7842964..60f9b5a 100644 --- a/src/shared/validation/external-csv.schemas.ts +++ b/src/shared/validation/external-csv.schemas.ts @@ -15,3 +15,21 @@ export const previewExternalCsvSchema = z .strict(); export const planExternalInitialImportSchema = previewExternalCsvSchema; + +export const applyExternalInitialImportSchema = z.object({ + expectedRevision: z.number().int().nonnegative(), + expectedConfigurationVersion: z.number().int().positive(), + expectedSha256: z.string().regex(/^[a-f0-9]{64}$/), + fileName: z.string().trim().min(1).max(255), + contentBase64: z.string().min(1).max(24_000_000), + sourceName: z.string().trim().min(1).max(200), + roomDecisions: z.array(z.object({ + sourceRoomKey: z.string().trim().min(1), + roomId: z.string().trim().min(1).nullable(), + defaultDistributionBoardId: z.string().trim().min(1).nullable(), + }).strict()), + familyProjectDeviceDecisions: z.array(z.object({ + familyAndType: z.string(), + projectDeviceId: z.string().trim().min(1).nullable(), + }).strict()), +}).strict(); diff --git a/tests/external-csv-api-contract.test.ts b/tests/external-csv-api-contract.test.ts index 3b3591e..2555e8a 100644 --- a/tests/external-csv-api-contract.test.ts +++ b/tests/external-csv-api-contract.test.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { + applyExternalInitialImportSchema, planExternalInitialImportSchema, previewExternalCsvSchema, updateExternalCsvConfigurationSchema, @@ -31,6 +32,19 @@ describe("external CSV API contracts", () => { }).success, true ); + assert.equal( + applyExternalInitialImportSchema.safeParse({ + expectedRevision: 5, + expectedConfigurationVersion: 2, + expectedSha256: "a".repeat(64), + fileName: "revit.csv", + contentBase64: "YWJj", + sourceName: "Revit-Gesamtmodell", + roomDecisions: [], + familyProjectDeviceDecisions: [], + }).success, + true + ); }); it("rejects stale-shaped, oversized and unknown request fields", () => { diff --git a/tests/external-initial-import-plan.test.ts b/tests/external-initial-import-plan.test.ts index 32c70dc..7fc1a43 100644 --- a/tests/external-initial-import-plan.test.ts +++ b/tests/external-initial-import-plan.test.ts @@ -1,6 +1,8 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { createExternalInitialImportPlan } from "../src/external-model/application/external-initial-import-plan.js"; +import { buildExternalInitialImportTarget } from "../src/external-model/application/external-initial-import-target.js"; +import { parseExternalCsv } from "../src/external-model/csv/external-csv-transport.js"; import { externalCsvTestConfiguration, firstIfcGuid, @@ -66,4 +68,82 @@ describe("external initial import plan", () => { assert.equal(plan.existing.distributionBoards[0].id, "board-1"); assert.equal(plan.existing.projectDevices[0].id, "device-1"); }); + + it("builds a stable row-free target only after every family is classified", () => { + const configuration = structuredClone(externalCsvTestConfiguration); + for (const exactFamilyAndType of [ + "Steckdose: Doppelsteckdose", + "Steckdose: Standard", + ]) { + configuration.familyTypeRules.push({ + exactFamilyAndType, + internalDeviceType: "Steckdose", + connectionKind: "socket", + category: "single_phase", + quantityRule: exactFamilyAndType.includes("Doppel") + ? { kind: "mapped-column" } + : { kind: "fixed", quantity: 1 }, + displayNameSuggestion: { kind: "selection-marker" }, + }); + } + const bytes = utf8Bytes(quotedRevitCsv, true); + const plan = createExternalInitialImportPlan({ + fileName: "revit.csv", + bytes, + configurationVersion: 2, + configuration, + floors: [], + rooms: [], + distributionBoards: [], + projectDevices: [], + }); + let id = 0; + const target = buildExternalInitialImportTarget({ + projectId: "project-1", + expectedRevision: 7, + sourceName: "Revit-Gesamtmodell", + importedAtIso: "2026-08-02T17:00:00.000Z", + configurationVersion: 2, + configuration, + originalContentBase64: Buffer.from(bytes).toString("base64"), + document: parseExternalCsv(bytes, configuration), + plan, + roomDecisions: plan.sourceRooms.map((room) => ({ + sourceRoomKey: room.sourceRoomKey, + roomId: null, + defaultDistributionBoardId: null, + })), + familyProjectDeviceDecisions: plan.familyGroups.map((group) => ({ + familyAndType: group.familyAndType, + projectDeviceId: null, + })), + createId: () => `generated-${++id}`, + }); + + assert.equal(target.importBatches[0].appliedProjectRevision, 8); + assert.equal(target.importBatches[0].configurationVersion, 2); + assert.equal(target.objects.length, 2); + assert.ok(target.objects.every((object) => object.circuitDeviceRowId === null)); + assert.ok(target.objects.every((object) => object.overriddenFields.length === 0)); + assert.throws( + () => buildExternalInitialImportTarget({ + projectId: "project-1", + expectedRevision: 7, + sourceName: "Revit-Gesamtmodell", + importedAtIso: "2026-08-02T17:00:00.000Z", + configurationVersion: 2, + configuration, + originalContentBase64: Buffer.from(bytes).toString("base64"), + document: parseExternalCsv(bytes, configuration), + plan, + roomDecisions: [], + familyProjectDeviceDecisions: plan.familyGroups.map((group) => ({ + familyAndType: group.familyAndType, + projectDeviceId: null, + })), + createId: () => "unused", + }), + /Quellraum-Entscheidungen/ + ); + }); }); From ab2449419b02a6253d0357201ab04fc4e33a1393 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 2 Aug 2026 17:52:21 +0200 Subject: [PATCH 42/59] Add Revit initial import wizard --- AGENTS.md | 23 +- docs/circuit-list-editor-api.md | 14 +- docs/circuit-list-editor-known-limitations.md | 3 +- docs/current-architecture.md | 16 +- docs/spec/07-implementation-phases-todo.md | 4 +- .../spec/revit-csv-phase-14-audit-and-plan.md | 9 +- src/frontend/components/revit-csv-modal.tsx | 290 +++++++++++++++++- src/frontend/types.ts | 31 ++ src/frontend/utils/api.ts | 27 ++ tests/revit-csv-frontend-api.test.ts | 96 +++++- 10 files changed, 481 insertions(+), 32 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b892a9a..5f6473d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -266,11 +266,12 @@ persisted through the shared project-command transaction, so create, update, delete, Undo and Redo are revision-safe. Snapshot schema 4, restore and portable project duplication preserve the configuration; duplication remaps its internal id and project link. -The project page exposes the current Phase 14.1 Revit CSV modal. It edits the -complete transport/column/family-rule configuration through the revision-safe -endpoint and requests a stateless preview. Preview responses include all mapped -objects, while the UI renders only the first 25. No preview creates external -objects, rows, revisions or server-side drafts. +The project page exposes the Revit CSV modal. It edits the complete +transport/column/family-rule configuration through the revision-safe endpoint, +requests a stateless preview and then shows the Phase 14.2 initial-import +decisions. Preview responses include all mapped objects, while the UI renders +only the first 25. No preview or plan creates external objects, rows, revisions +or server-side drafts. Phase 14.2 migration `0003` provides one implicit Revit CSV source per project, immutable import batches with original bytes plus the classified cell matrix, normalized source-room mappings and external objects unique by @@ -287,15 +288,14 @@ Confirmed initial state is written only through original-byte SHA-256, parsed matrix, complete IFCGUID/source values, explicit planning overrides and internal project links in the shared transaction. It never creates or links a CircuitDeviceRow. Its exact inverse removes the whole -unchanged external state, and Redo restores the same UUIDs and bytes. No -dedicated file-and-decisions apply endpoint or staging wizard invokes this -command yet. +unchanged external state, and Redo restores the same UUIDs and bytes. `POST /api/projects/:projectId/external-csv/initial-import/plan` is a stateless read path for the first wizard stage. It rejects projects with an existing external source and returns source-room groups, exact room-number suggestions, exact family/type groups, projected object values, issue counts and the current floor/room/board/ProjectDevice catalogs. It creates no draft, revision or -domain row; the decision UI is still pending. +domain row. The wide modal renders these groups and catalogs as explicit +decisions and identifies exact, ambiguous and new room matches. The dedicated `POST /api/projects/:projectId/external-csv/initial-import/apply` endpoint retransmits and replans the file against the expected hash, configuration @@ -303,8 +303,9 @@ version and project revision. It requires one explicit decision per source-room and exact family/type group, blocks unclassified families, creates all stable external UUIDs server-side and invokes `external-import.apply-initial`. Room/default-board and optional ProjectDevice links are supported; CSV circuit -values remain source-only and every `circuitDeviceRowId` remains null. The -decision UI is still pending. +values remain source-only and every `circuitDeviceRowId` remains null. The UI +blocks unclassified families, requires a final confirmation and applies the +whole import as one persistent Undo/Redo step. The central revision boundary creates an automatic logical snapshot after each 25 new revisions and retains only the newest 12 automatic snapshots per project. Named snapshots are never removed by this retention policy. diff --git a/docs/circuit-list-editor-api.md b/docs/circuit-list-editor-api.md index 206f967..70b5bb5 100644 --- a/docs/circuit-list-editor-api.md +++ b/docs/circuit-list-editor-api.md @@ -404,7 +404,17 @@ project device never triggers synchronization implicitly. - returns transport metadata, SHA-256, row classifications, suspect row numbers and mapped source values for every recognized IFC object - does not persist a draft, mutate project state or create a revision +- `POST /projects/:projectId/external-csv/initial-import/plan` + - reparses the file and returns grouped room/family decisions plus current + room, distribution-board and ProjectDevice catalogs + - is stateless and rejects an already imported external source +- `POST /projects/:projectId/external-csv/initial-import/apply` + - requires the expected revision, configuration version, file hash and one + decision for every room and exact family/type group + - reparses the file, blocks unknown family/type values and executes + `external-import.apply-initial` as one undoable project revision + - creates no Circuit or CircuitDeviceRow Parser errors expose stable codes such as `header-not-found`, -`ambiguous-header`, `invalid-ifc-guid` and `duplicate-ifc-guid`. A later -confirmed import must upload and parse the file again and verify its hash. +`ambiguous-header`, `invalid-ifc-guid` and `duplicate-ifc-guid`. The confirmed +import uploads and parses the file again and verifies its hash. diff --git a/docs/circuit-list-editor-known-limitations.md b/docs/circuit-list-editor-known-limitations.md index 85fd206..52949b1 100644 --- a/docs/circuit-list-editor-known-limitations.md +++ b/docs/circuit-list-editor-known-limitations.md @@ -9,7 +9,8 @@ - Named and automatic logical project snapshots can be listed and restored through the project page with persistent Undo/Redo. Automatic snapshots are created every 25 revisions and only their newest 12 entries are retained. -- No Revit/CSV/IFCGUID import and export workflow is implemented yet. +- The Revit/CSV foundation and confirmed initial import are implemented. Row + assignment, follow-up import, conflict review and return export are pending. - Persistence currently targets local SQLite; PostgreSQL is an architectural option, not an implemented runtime. - The global device library supports basic CRUD and copy operations, but has no versioning, permissions or controlled synchronization model. diff --git a/docs/current-architecture.md b/docs/current-architecture.md index fd94776..a09cda3 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -527,9 +527,8 @@ validiert alle Raum-, Verteiler- und ProjectDevice-Links gegen das Projekt. Nicht explizit als Override markierte Planungswerte müssen dem regelbasierten Vorschlag entsprechen. Der Command erzeugt und verknüpft ausdrücklich keine CircuitDeviceRow; Undo entfernt den vollständigen unveränderten Importzustand, -Redo verwendet dieselben UUIDs und Bytes. Ein aufrufender Import-Endpunkt und -der mehrstufige Wizard sind noch nicht implementiert; bloße Vorschau erzeugt -weiterhin keine dieser Zeilen. +Redo verwendet dieselben UUIDs und Bytes. Bloße Vorschau und Importplanung +erzeugen weiterhin keine dieser Zeilen. `GET` und `PUT /api/projects/:projectId/external-csv/configuration` lesen oder ändern die Konfiguration; der PUT plant Identität und nächsten Konfigurationsstand serverseitig und verwendet den typisierten Command. @@ -560,13 +559,18 @@ Spaltenzuordnung, zusätzliche Quellfelder und exakte Familie-und-Typ-Regeln. Nach dem revisionierten Speichern kann eine lokale CSV gewählt und über den zustandsfreien Endpunkt geprüft werden. Die Oberfläche zeigt Kennzahlen und nur die ersten 25 erkannten Objekte; die vollständigen Objekte bleiben in der -Response verfügbar, ohne alle Zeilen in den DOM einzufügen. +Response verfügbar, ohne alle Zeilen in den DOM einzufügen. Der anschließende +Erstimport-Wizard gruppiert Quellräume und exakte Familie-und-Typ-Werte. Er +zeigt eindeutige, mehrdeutige und neue Raumtreffer, lässt vorhandene Räume, +Standardverteilungen und ProjectDevices explizit zuordnen und blockiert die +Übernahme bei unklassifizierten Familien. Erst nach einem Bestätigungsdialog +wird der vollständige Import als eine persistente, rückgängig machbare +Projektrevision angewendet. Dabei entstehen keine Stromkreise oder Gerätezeilen. ## Noch nicht unterstützt - Mehrbenutzerbetrieb und Konfliktauflösung -- bestätigter Revit-Erstimport und vollständiger CSV-/IFCGUID-Round-trip über - die vorhandene Transport- und Persistenzgrundlage hinaus +- Folgeimport, Editor-Zuordnung und strukturtreuer Revit-Rückexport - vollständige elektrische Dimensionierung - Produktionsdeployment diff --git a/docs/spec/07-implementation-phases-todo.md b/docs/spec/07-implementation-phases-todo.md index 84e0132..b665e85 100644 --- a/docs/spec/07-implementation-phases-todo.md +++ b/docs/spec/07-implementation-phases-todo.md @@ -532,8 +532,8 @@ Acceptance criteria: ## Phase 14: External Model Round-Trip -Status: Phase 14.0 (Audit und Verträge) ist dokumentiert und wartet vor der -Laufzeitimplementierung auf Freigabe. Siehe +Status: Phase 14.2 (persistente externe Objekte und bestätigter Erstimport) ist +implementiert. Editor-Zuordnung, Rückexport und Folgeimport folgen. Siehe [Phase-14-Audit und Implementierungsplan](revit-csv-phase-14-audit-and-plan.md) und die verbindlichen [Revit-CSV-Anforderungen](revit-csv-integration-requirements.md). diff --git a/docs/spec/revit-csv-phase-14-audit-and-plan.md b/docs/spec/revit-csv-phase-14-audit-and-plan.md index 645c42d..ee2b2cc 100644 --- a/docs/spec/revit-csv-phase-14-audit-and-plan.md +++ b/docs/spec/revit-csv-phase-14-audit-and-plan.md @@ -288,19 +288,20 @@ einen Commit aufgenommen. die zuvor noch fehlende monotone Konfigurationsversion am Batch; `0003` bleibt unverändert. 4. Raum-, Verteilungs-, Klassifizierungs- und ProjectDevice-Schritte im Wizard - ergänzen; der Import selbst erzeugt keine CircuitDeviceRow. **Begonnen:** + ergänzen; der Import selbst erzeugt keine CircuitDeviceRow. **Erledigt:** Der zustandsfreie Planungs-Endpunkt liefert gruppierte Quellräume mit exaktem Raumnummernvorschlag, Familie-und-Typ-Gruppen, Objektvorschläge, Warnungszahlen sowie vorhandene Ebenen, Räume, Verteilungen und ProjectDevices. Er blockiert nach einem bereits bestätigten Erstimport und - schreibt weder Entwurf noch Projektzustand. Entscheidungs-UI und Apply- - Endpunkt stehen noch aus. **Backend-Apply erledigt:** Der dedizierte + schreibt weder Entwurf noch Projektzustand. Der dedizierte Endpunkt verlangt denselben Datei-Hash und Konfigurationsstand wie die Planung, vollständige Entscheidungen für alle Quellräume und exakten Familie-und-Typ-Gruppen und baut daraus serverseitig stabile UUIDs. Nicht klassifizierte Familien blockieren. Raum-/Standardverteiler- und optionale ProjectDevice-Links werden bestätigt, Stromkreis- und Row-Links bleiben - ausgeschlossen. Die Entscheidungs-UI steht noch aus. + ausgeschlossen. Das breite Projektmodal zeigt Raumtreffer und Warnungen, + bietet die vorhandenen Raum-, Verteiler- und ProjectDevice-Kataloge an und + verlangt vor der atomaren Übernahme eine ausdrückliche Bestätigung. Vor Beginn werden die oben vorgeschlagenen Namen, die erneute Dateiübertragung statt serverseitiger Entwürfe, die Konfigurationsversionierung und die diff --git a/src/frontend/components/revit-csv-modal.tsx b/src/frontend/components/revit-csv-modal.tsx index 35affc1..f56f0f3 100644 --- a/src/frontend/components/revit-csv-modal.tsx +++ b/src/frontend/components/revit-csv-modal.tsx @@ -6,9 +6,11 @@ import { type ExternalCsvConfiguration, type ExternalCsvFamilyTypeRule, } from "../../external-model/csv/external-csv-contracts"; -import type { ExternalCsvPreviewDto } from "../types"; +import type { ExternalCsvPreviewDto, ExternalInitialImportPlanDto } from "../types"; import { + applyExternalInitialImport, getExternalCsvConfiguration, + planExternalInitialImport, previewExternalCsv, updateExternalCsvConfiguration, } from "../utils/api"; @@ -62,9 +64,16 @@ export function RevitCsvModal({ const [savedConfiguration, setSavedConfiguration] = useState(null); const [file, setFile] = useState(null); const [preview, setPreview] = useState(null); + const [importPlan, setImportPlan] = useState(null); + const [roomDecisions, setRoomDecisions] = useState>({}); + const [familyLinks, setFamilyLinks] = useState>({}); + const [sourceName, setSourceName] = useState("Revit-Gesamtmodell"); + const [success, setSuccess] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [isPreviewing, setIsPreviewing] = useState(false); + const [isPlanning, setIsPlanning] = useState(false); + const [isApplying, setIsApplying] = useState(false); const [error, setError] = useState(null); useEffect(() => { @@ -85,6 +94,8 @@ export function RevitCsvModal({ function replaceConfiguration(next: ExternalCsvConfiguration) { setConfiguration(next); setPreview(null); + setImportPlan(null); + setSuccess(null); } async function handleSave(event: FormEvent) { @@ -98,6 +109,7 @@ export function RevitCsvModal({ configuration ); setSavedConfiguration(JSON.stringify(result.configuration.configuration)); + setImportPlan(null); onRevisionChange(result.history.currentRevision); } catch (reason) { setError(reason instanceof Error ? reason.message : "Konfiguration konnte nicht gespeichert werden."); @@ -113,6 +125,8 @@ export function RevitCsvModal({ try { const contentBase64 = await fileToBase64(file); setPreview(await previewExternalCsv(projectId, file.name, contentBase64)); + setImportPlan(null); + setSuccess(null); } catch (reason) { setPreview(null); setError(reason instanceof Error ? reason.message : "CSV-Vorschau konnte nicht erstellt werden."); @@ -121,26 +135,93 @@ export function RevitCsvModal({ } } + async function handlePlanImport() { + if (!file || !preview || isDirty || savedConfiguration === null) return; + setIsPlanning(true); + setError(null); + try { + const contentBase64 = await fileToBase64(file); + const plan = await planExternalInitialImport(projectId, file.name, contentBase64); + if (plan.sha256 !== preview.sha256) { + throw new Error("Die Datei stimmt nicht mehr mit der Vorschau überein."); + } + setImportPlan(plan); + setRoomDecisions(Object.fromEntries(plan.sourceRooms.map((room) => [ + room.sourceRoomKey, + { roomId: room.suggestedRoomId, defaultDistributionBoardId: null }, + ]))); + setFamilyLinks(Object.fromEntries(plan.familyGroups.map((group) => [ + group.familyAndType, + null, + ]))); + } catch (reason) { + setImportPlan(null); + setError(reason instanceof Error ? reason.message : "Erstimport konnte nicht geplant werden."); + } finally { + setIsPlanning(false); + } + } + + async function handleApplyImport() { + if (!file || !importPlan) return; + if (!window.confirm( + `${importPlan.objectCount} Revit-Objekte jetzt als einen rückgängig machbaren Import übernehmen?` + )) return; + setIsApplying(true); + setError(null); + try { + const contentBase64 = await fileToBase64(file); + const result = await applyExternalInitialImport(projectId, { + expectedRevision: currentRevision, + expectedConfigurationVersion: importPlan.configurationVersion, + expectedSha256: importPlan.sha256, + fileName: file.name, + contentBase64, + sourceName, + roomDecisions: importPlan.sourceRooms.map((room) => ({ + sourceRoomKey: room.sourceRoomKey, + roomId: roomDecisions[room.sourceRoomKey]?.roomId ?? null, + defaultDistributionBoardId: + roomDecisions[room.sourceRoomKey]?.defaultDistributionBoardId ?? null, + })), + familyProjectDeviceDecisions: importPlan.familyGroups.map((group) => ({ + familyAndType: group.familyAndType, + projectDeviceId: familyLinks[group.familyAndType] ?? null, + })), + }); + onRevisionChange(result.history.currentRevision); + setSuccess(`${result.import.objectCount} Revit-Objekte wurden übernommen.`); + setImportPlan(null); + setPreview(null); + } catch (reason) { + setError(reason instanceof Error ? reason.message : "Erstimport konnte nicht übernommen werden."); + } finally { + setIsApplying(false); + } + } + function updateColumn(key: (typeof columnFields)[number][0], value: string) { setConfiguration((current) => ({ ...current, columns: { ...current.columns, [key]: key === "quantity" && !value.trim() ? null : value }, })); setPreview(null); + setImportPlan(null); } return ( {error ?
{error}
: null} + {success ?
{success}
: null} {isLoading ?

Konfiguration wird geladen …

: (
@@ -172,13 +253,27 @@ export function RevitCsvModal({

CSV prüfen

Die Vorschau speichert keine Datei und verändert keine Projektdaten.

- { setFile(event.target.files?.[0] ?? null); setPreview(null); }} style={{ maxWidth: 480 }} type="file" /> + { setFile(event.target.files?.[0] ?? null); setPreview(null); setImportPlan(null); setSuccess(null); }} style={{ maxWidth: 480 }} type="file" /> +
{isDirty ?
Vor der Vorschau muss die Konfiguration gespeichert werden.
: null}
{preview ? : null} + {importPlan ? ( + setFamilyLinks((current) => ({ ...current, [familyAndType]: projectDeviceId }))} + onRoomDecisionChange={(sourceRoomKey, decision) => setRoomDecisions((current) => ({ ...current, [sourceRoomKey]: decision }))} + plan={importPlan} + roomDecisions={roomDecisions} + setSourceName={setSourceName} + sourceName={sourceName} + /> + ) : null}
)}
@@ -204,6 +299,193 @@ function PreviewResult({ preview }: { preview: ExternalCsvPreviewDto }) { return

Prüfergebnis

SHA-256: {preview.sha256}

{visibleObjects.map((object) => )}
ZeileIFC-GUIDRaumFamilie und TypAuswahlkennerStromkreis
{object.rowNumber}{object.ifcGuid}{[object.roomNumber, object.roomName].filter(Boolean).join(" · ") || "–"}{object.familyAndType || "–"}{object.selectionMarker || "–"}{object.circuitIdentifier || "–"}
{preview.objects.length > visibleObjects.length ?

Es werden die ersten {visibleObjects.length} von {preview.objects.length} Objekten angezeigt.

: null}
; } +interface RoomDecision { + roomId: string | null; + defaultDistributionBoardId: string | null; +} + +export function RevitCsvImportPlan({ + familyLinks, + isApplying, + onApply, + onFamilyLinkChange, + onRoomDecisionChange, + plan, + roomDecisions, + setSourceName, + sourceName, +}: { + familyLinks: Record; + isApplying: boolean; + onApply: () => void; + onFamilyLinkChange: (familyAndType: string, projectDeviceId: string | null) => void; + onRoomDecisionChange: (sourceRoomKey: string, decision: RoomDecision) => void; + plan: ExternalInitialImportPlanDto; + roomDecisions: Record; + setSourceName: (value: string) => void; + sourceName: string; +}) { + const hasBlockingIssues = plan.issueCounts.unknownFamilyAndType > 0; + const warningCount = + plan.issueCounts.invalidPower + + plan.issueCounts.invalidQuantity + + plan.issueCounts.missingRoom + + plan.suspectObjectCount; + const floorsById = new Map(plan.existing.floors.map((floor) => [floor.id, floor])); + + return ( +
+
+
+

Erstimport vorbereiten

+

+ Raum- und Gerätekatalog-Zuordnungen werden zusammen mit {plan.objectCount} Revit-Objekten gespeichert. + Stromkreise und Gerätezeilen entstehen dabei noch nicht. +

+
+ Konfiguration {plan.configurationVersion} +
+ +
+ + + + +
+ + {hasBlockingIssues ? ( +
+ {plan.issueCounts.unknownFamilyAndType} Objekte besitzen noch keine exakte Familie-und-Typ-Regel. + Ergänze die Regeln oben, speichere die Konfiguration und plane den Import erneut. +
+ ) : null} + {plan.issueCounts.invalidPower > 0 ?
Bei {plan.issueCounts.invalidPower} Objekten ist die Leistung ungültig.
: null} + {plan.issueCounts.invalidQuantity > 0 ?
Bei {plan.issueCounts.invalidQuantity} Objekten ist die Menge ungültig.
: null} + {plan.issueCounts.missingRoom > 0 ?
{plan.issueCounts.missingRoom} Objekte besitzen keine Raumangabe.
: null} + {plan.suspectObjectCount > 0 ?
{plan.suspectObjectCount} Objektzeilen wurden als verdächtig erkannt.
: null} + + + +

Räume zuordnen

+
+ + + + {plan.sourceRooms.map((sourceRoom) => { + const decision = roomDecisions[sourceRoom.sourceRoomKey] ?? { + roomId: sourceRoom.suggestedRoomId, + defaultDistributionBoardId: null, + }; + return ( + + + + + + + + ); + })} + +
QuellraumObjekteTrefferInterner RaumStandardverteilung
{[sourceRoom.roomNumber, sourceRoom.roomName].filter(Boolean).join(" · ")}{sourceRoom.objectCount} + + + +
+
+ +

Familien und Typen zuordnen

+
+ + + + {plan.familyGroups.map((group) => ( + + + + + + + ))} + +
Familie und TypObjekteKlassifizierungProjectDevice (optional)
{group.familyAndType || "–"}{group.objectCount} + {group.classified + ? `${group.internalDeviceType ?? "–"} · ${categoryLabel(group.category)}` + : Regel fehlt} + + +
+
+ +
+ +
+
+ ); +} + +function RoomMatchBadge({ status }: { status: ExternalInitialImportPlanDto["sourceRooms"][number]["matchStatus"] }) { + if (status === "exact-number") return Eindeutig; + if (status === "ambiguous-number") return Mehrdeutig; + return Neu; +} + +function categoryLabel(category: string | null) { + if (category === "lighting") return "Beleuchtung"; + if (category === "single_phase") return "1-phasig"; + if (category === "three_phase") return "3-phasig"; + return "Ohne Kategorie"; +} + function Stat({ label, value }: { label: string; value: number }) { return
{label}
{value}
; } function fileToBase64(file: File) { diff --git a/src/frontend/types.ts b/src/frontend/types.ts index d950986..feea899 100644 --- a/src/frontend/types.ts +++ b/src/frontend/types.ts @@ -14,6 +14,7 @@ import type { ExternalCsvConfiguration, ExternalCsvDialect, } from "../external-model/csv/external-csv-contracts"; +import type { createExternalInitialImportPlan } from "../external-model/application/external-initial-import-plan"; export interface ExternalCsvConfigurationSnapshotDto { id: string; @@ -51,6 +52,36 @@ export interface ExternalCsvPreviewDto { suspectRowNumbers: number[]; } +export type ExternalInitialImportPlanDto = ReturnType< + typeof createExternalInitialImportPlan +>; + +export interface ApplyExternalInitialImportInput { + expectedRevision: number; + expectedConfigurationVersion: number; + expectedSha256: string; + fileName: string; + contentBase64: string; + sourceName: string; + roomDecisions: Array<{ + sourceRoomKey: string; + roomId: string | null; + defaultDistributionBoardId: string | null; + }>; + familyProjectDeviceDecisions: Array<{ + familyAndType: string; + projectDeviceId: string | null; + }>; +} + +export interface ExternalInitialImportResultDto extends ProjectCommandResultDto { + import: { + sourceId: string; + importBatchId: string; + objectCount: number; + }; +} + export interface ProjectDto { id: string; name: string; diff --git a/src/frontend/utils/api.ts b/src/frontend/utils/api.ts index a98a56a..fcc8e94 100644 --- a/src/frontend/utils/api.ts +++ b/src/frontend/utils/api.ts @@ -28,6 +28,9 @@ import type { CircuitTreeResponseDto, ExternalCsvConfigurationSnapshotDto, ExternalCsvPreviewDto, + ExternalInitialImportPlanDto, + ApplyExternalInitialImportInput, + ExternalInitialImportResultDto, } from "../types"; import type { ExternalCsvConfiguration } from "../../external-model/csv/external-csv-contracts"; import type { ProjectDeviceSyncField } from "../../shared/constants/project-device-sync-fields"; @@ -370,6 +373,30 @@ export function previewExternalCsv( ); } +export function planExternalInitialImport( + projectId: string, + fileName: string, + contentBase64: string +) { + return request( + `/api/projects/${projectId}/external-csv/initial-import/plan`, + { + method: "POST", + body: JSON.stringify({ fileName, contentBase64 }), + } + ); +} + +export function applyExternalInitialImport( + projectId: string, + input: ApplyExternalInitialImportInput +) { + return request( + `/api/projects/${projectId}/external-csv/initial-import/apply`, + { method: "POST", body: JSON.stringify(input) } + ); +} + export function copyDistributionBoard( projectId: string, distributionBoardId: string, diff --git a/tests/revit-csv-frontend-api.test.ts b/tests/revit-csv-frontend-api.test.ts index e08c1e2..85846d7 100644 --- a/tests/revit-csv-frontend-api.test.ts +++ b/tests/revit-csv-frontend-api.test.ts @@ -2,17 +2,23 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; -import { RevitCsvModal } from "../src/frontend/components/revit-csv-modal.js"; import { + RevitCsvImportPlan, + RevitCsvModal, +} from "../src/frontend/components/revit-csv-modal.js"; +import { + applyExternalInitialImport, getExternalCsvConfiguration, + planExternalInitialImport, previewExternalCsv, updateExternalCsvConfiguration, } from "../src/frontend/utils/api.js"; +import type { ExternalInitialImportPlanDto } from "../src/frontend/types.js"; import { createDefaultExternalCsvConfiguration } from "../src/external-model/csv/external-csv-contracts.js"; import { externalCsvTestColumns } from "./fixtures/revit-csv-fixtures.js"; describe("Revit CSV frontend API", () => { - it("uses the project-scoped configuration and preview endpoints", async () => { + it("uses the project-scoped configuration, preview and import endpoints", async () => { const requests: Array<{ url: string; method: string; body: unknown }> = []; const originalFetch = globalThis.fetch; globalThis.fetch = async (input, init) => { @@ -32,6 +38,17 @@ describe("Revit CSV frontend API", () => { await getExternalCsvConfiguration("project-1"); await updateExternalCsvConfiguration("project-1", 7, configuration); await previewExternalCsv("project-1", "revit.csv", "YWJj"); + await planExternalInitialImport("project-1", "revit.csv", "YWJj"); + await applyExternalInitialImport("project-1", { + expectedRevision: 8, + expectedConfigurationVersion: 2, + expectedSha256: "sha", + fileName: "revit.csv", + contentBase64: "YWJj", + sourceName: "Revit-Gesamtmodell", + roomDecisions: [{ sourceRoomKey: "101|büro", roomId: "room-1", defaultDistributionBoardId: "board-1" }], + familyProjectDeviceDecisions: [{ familyAndType: "Steckdose : Standard", projectDeviceId: "device-1" }], + }); } finally { globalThis.fetch = originalFetch; } @@ -52,6 +69,25 @@ describe("Revit CSV frontend API", () => { method: "POST", body: { fileName: "revit.csv", contentBase64: "YWJj" }, }, + { + url: "/api/projects/project-1/external-csv/initial-import/plan", + method: "POST", + body: { fileName: "revit.csv", contentBase64: "YWJj" }, + }, + { + url: "/api/projects/project-1/external-csv/initial-import/apply", + method: "POST", + body: { + expectedRevision: 8, + expectedConfigurationVersion: 2, + expectedSha256: "sha", + fileName: "revit.csv", + contentBase64: "YWJj", + sourceName: "Revit-Gesamtmodell", + roomDecisions: [{ sourceRoomKey: "101|büro", roomId: "room-1", defaultDistributionBoardId: "board-1" }], + familyProjectDeviceDecisions: [{ familyAndType: "Steckdose : Standard", projectDeviceId: "device-1" }], + }, + }, ]); }); }); @@ -70,4 +106,60 @@ describe("Revit CSV modal presentation", () => { assert.match(markup, /max-width:calc\(100vw - 2rem\)/); assert.doesNotMatch(markup, /modal-lg/); }); + + it("renders the initial import decisions without individual object rows", () => { + const plan: ExternalInitialImportPlanDto = { + configurationVersion: 2, + fileName: "revit.csv", + sha256: "sha", + byteLength: 100, + rowCount: 2, + objectCount: 1, + passthroughCount: 1, + suspectObjectCount: 0, + suspectRowNumbers: [], + sourceRooms: [{ + sourceRoomKey: "101|büro", + roomNumber: "101", + roomName: "Büro", + objectCount: 1, + suggestedRoomId: "room-1", + matchStatus: "exact-number", + candidateRoomIds: ["room-1"], + }], + familyGroups: [{ + familyAndType: "Steckdose : Standard", + objectCount: 1, + classified: true, + category: "single_phase", + connectionKind: null, + internalDeviceType: "socket", + }], + issueCounts: { unknownFamilyAndType: 0, invalidPower: 0, invalidQuantity: 0, missingRoom: 0 }, + objects: [], + existing: { + floors: [{ id: "floor-1", name: "EG", sortOrder: 0 }], + rooms: [{ id: "room-1", floorId: "floor-1", roomNumber: "101", roomName: "Büro" }], + distributionBoards: [{ id: "board-1", name: "UV-01", floorId: "floor-1" }], + projectDevices: [{ id: "device-1", name: "Steckdose", displayName: "Steckdose", category: "single_phase", connectionKind: null, powerPerUnit: 100 }], + }, + }; + const markup = renderToStaticMarkup(createElement(RevitCsvImportPlan, { + familyLinks: { "Steckdose : Standard": "device-1" }, + isApplying: false, + onApply: () => undefined, + onFamilyLinkChange: () => undefined, + onRoomDecisionChange: () => undefined, + plan, + roomDecisions: { "101|büro": { roomId: "room-1", defaultDistributionBoardId: "board-1" } }, + setSourceName: () => undefined, + sourceName: "Revit-Gesamtmodell", + })); + + assert.match(markup, /Erstimport vorbereiten/); + assert.match(markup, /101 · Büro/); + assert.match(markup, /Steckdose : Standard/); + assert.match(markup, /Erstimport übernehmen/); + assert.doesNotMatch(markup, /IFC-GUID/); + }); }); From bc2bc7ece52f1ea4c9aedc0bea3091fbd2db3c3d Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 2 Aug 2026 18:08:18 +0200 Subject: [PATCH 43/59] Add Revit catalog creation flow --- AGENTS.md | 5 +- docs/current-architecture.md | 6 + .../spec/revit-csv-phase-14-audit-and-plan.md | 5 +- src/app/projects/[projectId]/page.tsx | 120 +++------- .../components/project-device-modal.tsx | 29 ++- src/frontend/components/revit-csv-modal.tsx | 217 +++++++++++++++--- src/frontend/components/room-modal.tsx | 107 +++++++++ tests/revit-csv-frontend-api.test.ts | 79 +++++++ 8 files changed, 435 insertions(+), 133 deletions(-) create mode 100644 src/frontend/components/room-modal.tsx diff --git a/AGENTS.md b/AGENTS.md index 5f6473d..e15bf4c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -295,7 +295,10 @@ external source and returns source-room groups, exact room-number suggestions, exact family/type groups, projected object values, issue counts and the current floor/room/board/ProjectDevice catalogs. It creates no draft, revision or domain row. The wide modal renders these groups and catalogs as explicit -decisions and identifies exact, ambiguous and new room matches. +decisions and identifies exact, ambiguous and new room matches. Missing rooms +and ProjectDevices are created through the reused project forms with mapped +Revit values as visible defaults. Each catalog creation remains its own project +command; the wizard adds and selects the result without discarding its plan. The dedicated `POST /api/projects/:projectId/external-csv/initial-import/apply` endpoint retransmits and replans the file against the expected hash, configuration diff --git a/docs/current-architecture.md b/docs/current-architecture.md index a09cda3..cb46030 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -566,6 +566,12 @@ Standardverteilungen und ProjectDevices explizit zuordnen und blockiert die Übernahme bei unklassifizierten Familien. Erst nach einem Bestätigungsdialog wird der vollständige Import als eine persistente, rückgängig machbare Projektrevision angewendet. Dabei entstehen keine Stromkreise oder Gerätezeilen. +Fehlende Räume und ProjectDevices können direkt aus der jeweiligen Gruppe über +die regulären Projektformulare angelegt werden. Revit-Werte befüllen diese +Formulare vor, werden aber vor dem Speichern sichtbar vom Nutzer bestätigt. +Die Kataloganlage ist jeweils ein eigener Projekt-Command; der Wizard übernimmt +den neuen Datensatz anschließend in seinen lokalen Auswahlkatalog und markiert +ihn für die betroffene Gruppe. ## Noch nicht unterstützt diff --git a/docs/spec/revit-csv-phase-14-audit-and-plan.md b/docs/spec/revit-csv-phase-14-audit-and-plan.md index ee2b2cc..f959dc4 100644 --- a/docs/spec/revit-csv-phase-14-audit-and-plan.md +++ b/docs/spec/revit-csv-phase-14-audit-and-plan.md @@ -301,7 +301,10 @@ einen Commit aufgenommen. ProjectDevice-Links werden bestätigt, Stromkreis- und Row-Links bleiben ausgeschlossen. Das breite Projektmodal zeigt Raumtreffer und Warnungen, bietet die vorhandenen Raum-, Verteiler- und ProjectDevice-Kataloge an und - verlangt vor der atomaren Übernahme eine ausdrückliche Bestätigung. + verlangt vor der atomaren Übernahme eine ausdrückliche Bestätigung. Fehlende + Räume und ProjectDevices lassen sich über die regulären, vorausgefüllten + Projektformulare anlegen und werden danach unmittelbar ausgewählt. Diese + Kataloganlagen bleiben eigenständige, rückgängig machbare Projektrevisionen. Vor Beginn werden die oben vorgeschlagenen Namen, die erneute Dateiübertragung statt serverseitiger Entwürfe, die Konfigurationsversionierung und die diff --git a/src/app/projects/[projectId]/page.tsx b/src/app/projects/[projectId]/page.tsx index d5a6b87..c675340 100644 --- a/src/app/projects/[projectId]/page.tsx +++ b/src/app/projects/[projectId]/page.tsx @@ -35,6 +35,7 @@ import { } from "../../../frontend/utils/api"; import type { CircuitListDto, + CreateRoomInput, CreateProjectDeviceInput, DistributionBoardDto, FloorDto, @@ -60,6 +61,7 @@ import { import { FormModal } from "../../../frontend/components/form-modal"; import { ProjectDeviceModal } from "../../../frontend/components/project-device-modal"; import { RevitCsvModal } from "../../../frontend/components/revit-csv-modal"; +import { RoomModal } from "../../../frontend/components/room-modal"; const projectDeviceSyncFieldLabels: Record = { name: "Technischer Name", @@ -101,9 +103,6 @@ export default function ProjectDetailPage() { const [editingBoardCopyName, setEditingBoardCopyName] = useState(""); const [floorName, setFloorName] = useState(""); const [editingFloor, setEditingFloor] = useState(null); - const [roomNumber, setRoomNumber] = useState(""); - const [roomName, setRoomName] = useState(""); - const [roomFloorId, setRoomFloorId] = useState(""); const [editingRoom, setEditingRoom] = useState(null); const [isProjectSettingsOpen, setIsProjectSettingsOpen] = useState(false); const [isRevitCsvOpen, setIsRevitCsvOpen] = useState(false); @@ -285,24 +284,11 @@ export default function ProjectDetailPage() { } } - async function handleSaveRoom(event: FormEvent) { - event.preventDefault(); - if ( - !projectId || - !project || - !roomNumber.trim() || - !roomName.trim() - ) { - return; - } + async function handleSaveRoom(input: CreateRoomInput) { + if (!projectId || !project) return; setIsSaving(true); setError(null); try { - const input = { - floorId: roomFloorId || undefined, - roomNumber: roomNumber.trim(), - roomName: roomName.trim(), - }; const result = editingRoom ? await updateRoom( projectId, @@ -319,9 +305,6 @@ export default function ProjectDetailPage() { : [...current, result.room] ); applyProjectRevision(result.history.currentRevision); - setRoomNumber(""); - setRoomName(""); - setRoomFloorId(""); setEditingRoom(null); setStructureModal(null); } catch (err) { @@ -500,6 +483,22 @@ export default function ProjectDetailPage() { } } + async function handleCreateRevitRoom(input: CreateRoomInput) { + if (!projectId || !project) throw new Error("Projekt ist nicht geladen."); + const result = await createRoom(projectId, input, project.currentRevision); + setRooms((current) => [...current, result.room]); + applyProjectRevision(result.history.currentRevision); + return result; + } + + async function handleCreateRevitProjectDevice(input: CreateProjectDeviceInput) { + if (!projectId || !project) throw new Error("Projekt ist nicht geladen."); + const result = await createProjectDevice(projectId, input, project.currentRevision); + setProjectDevices((current) => [...current, result.projectDevice]); + applyProjectRevision(result.history.currentRevision); + return result; + } + function openCreateFloor() { setEditingFloor(null); setFloorName(""); @@ -514,17 +513,11 @@ export default function ProjectDetailPage() { function openCreateRoom() { setEditingRoom(null); - setRoomNumber(""); - setRoomName(""); - setRoomFloorId(""); setStructureModal("room"); } function openEditRoom(room: RoomDto) { setEditingRoom(room); - setRoomNumber(room.roomNumber); - setRoomName(room.roomName); - setRoomFloorId(room.floorId ?? ""); setStructureModal("room"); } @@ -557,9 +550,6 @@ export default function ProjectDetailPage() { setRooms((current) => current.filter((room) => room.id !== editingRoom.id)); applyProjectRevision(result.history.currentRevision); setEditingRoom(null); - setRoomNumber(""); - setRoomName(""); - setRoomFloorId(""); setStructureModal(null); } catch (err) { setError(err instanceof Error ? err.message : "Raum konnte nicht gelöscht werden."); @@ -1585,73 +1575,17 @@ export default function ProjectDetailPage() { ) : null} {structureModal === "room" ? ( - { setStructureModal(null); setEditingRoom(null); }} - onSubmit={handleSaveRoom} - submitDisabled={!roomNumber.trim() || !roomName.trim()} - submitLabel={editingRoom ? "Änderungen speichern" : "Raum anlegen"} - title={editingRoom ? "Raum bearbeiten" : "Raum hinzufügen"} - > -
-
- - setRoomNumber(event.target.value)} - required - value={roomNumber} - /> -
-
- - setRoomName(event.target.value)} - required - value={roomName} - /> -
-
- - -
-
- {editingRoom ? ( -
-

- In Stromkreisen verwendete Räume können nicht gelöscht werden. -

- -
- ) : null} -
+ onDelete={editingRoom ? () => void handleDeleteRoom() : undefined} + onSave={handleSaveRoom} + /> ) : null} {isProjectDeviceModalOpen ? ( @@ -1692,6 +1626,8 @@ export default function ProjectDetailPage() { setIsRevitCsvOpen(false)} + onCreateProjectDevice={handleCreateRevitProjectDevice} + onCreateRoom={handleCreateRevitRoom} onRevisionChange={applyProjectRevision} projectId={projectId} /> diff --git a/src/frontend/components/project-device-modal.tsx b/src/frontend/components/project-device-modal.tsx index 55c9d25..1707da9 100644 --- a/src/frontend/components/project-device-modal.tsx +++ b/src/frontend/components/project-device-modal.tsx @@ -16,6 +16,7 @@ import { interface ProjectDeviceModalProps { globalDevices: GlobalDeviceDto[]; initialDevice?: ProjectDeviceDto; + initialValues?: Partial; isSaving: boolean; onClose: () => void; onImportGlobal: (globalDeviceId: string) => Promise; @@ -25,12 +26,13 @@ interface ProjectDeviceModalProps { export function ProjectDeviceModal({ globalDevices, initialDevice, + initialValues, isSaving, onClose, onImportGlobal, onSave, }: ProjectDeviceModalProps) { - const [values, setValues] = useState(() => toFormValues(initialDevice)); + const [values, setValues] = useState(() => toFormValues(initialDevice, initialValues)); const [selectedGlobalDeviceId, setSelectedGlobalDeviceId] = useState(""); function update(key: keyof typeof values, value: string) { @@ -254,18 +256,21 @@ function NumberField({ ); } -function toFormValues(device?: ProjectDeviceDto) { +function toFormValues( + device?: ProjectDeviceDto, + initialValues?: Partial +) { return { - name: device?.name ?? "", - displayName: device?.displayName ?? "", - connectionKind: device?.connectionKind ?? "", - costGroup: device?.costGroup ?? "", - category: device?.category ?? "single_phase", - quantity: String(device?.quantity ?? 1), - powerPerUnit: String(device?.powerPerUnit ?? 0.1), - simultaneityFactor: String(device?.simultaneityFactor ?? 1), - cosPhi: String(device?.cosPhi ?? 1), - remark: device?.remark ?? "", + name: device?.name ?? initialValues?.name ?? "", + displayName: device?.displayName ?? initialValues?.displayName ?? "", + connectionKind: device?.connectionKind ?? initialValues?.connectionKind ?? "", + costGroup: device?.costGroup ?? initialValues?.costGroup ?? "", + category: device?.category ?? initialValues?.category ?? "single_phase", + quantity: String(device?.quantity ?? initialValues?.quantity ?? 1), + powerPerUnit: String(device?.powerPerUnit ?? initialValues?.powerPerUnit ?? 0.1), + simultaneityFactor: String(device?.simultaneityFactor ?? initialValues?.simultaneityFactor ?? 1), + cosPhi: String(device?.cosPhi ?? initialValues?.cosPhi ?? 1), + remark: device?.remark ?? initialValues?.remark ?? "", }; } diff --git a/src/frontend/components/revit-csv-modal.tsx b/src/frontend/components/revit-csv-modal.tsx index f56f0f3..3602bd8 100644 --- a/src/frontend/components/revit-csv-modal.tsx +++ b/src/frontend/components/revit-csv-modal.tsx @@ -6,7 +6,14 @@ import { type ExternalCsvConfiguration, type ExternalCsvFamilyTypeRule, } from "../../external-model/csv/external-csv-contracts"; -import type { ExternalCsvPreviewDto, ExternalInitialImportPlanDto } from "../types"; +import type { + CreateProjectDeviceInput, + CreateRoomInput, + ExternalCsvPreviewDto, + ExternalInitialImportPlanDto, + ProjectDeviceCommandResultDto, + ProjectRoomCommandResultDto, +} from "../types"; import { applyExternalInitialImport, getExternalCsvConfiguration, @@ -15,10 +22,16 @@ import { updateExternalCsvConfiguration, } from "../utils/api"; import { FormModal } from "./form-modal"; +import { ProjectDeviceModal } from "./project-device-modal"; +import { RoomModal } from "./room-modal"; interface RevitCsvModalProps { currentRevision: number; onClose: () => void; + onCreateProjectDevice: ( + input: CreateProjectDeviceInput + ) => Promise; + onCreateRoom: (input: CreateRoomInput) => Promise; onRevisionChange: (currentRevision: number) => void; projectId: string; } @@ -55,6 +68,8 @@ const columnFields = [ export function RevitCsvModal({ currentRevision, onClose, + onCreateProjectDevice, + onCreateRoom, onRevisionChange, projectId, }: RevitCsvModalProps) { @@ -74,6 +89,9 @@ export function RevitCsvModal({ const [isPreviewing, setIsPreviewing] = useState(false); const [isPlanning, setIsPlanning] = useState(false); const [isApplying, setIsApplying] = useState(false); + const [isCreatingCatalogEntry, setIsCreatingCatalogEntry] = useState(false); + const [newRoomSourceKey, setNewRoomSourceKey] = useState(null); + const [newProjectDeviceFamily, setNewProjectDeviceFamily] = useState(null); const [error, setError] = useState(null); useEffect(() => { @@ -209,6 +227,93 @@ export function RevitCsvModal({ setImportPlan(null); } + async function handleCreateRoom(input: CreateRoomInput) { + if (!newRoomSourceKey) return; + setIsCreatingCatalogEntry(true); + setError(null); + try { + const result = await onCreateRoom(input); + setImportPlan((current) => current ? { + ...current, + existing: { + ...current.existing, + rooms: [...current.existing.rooms, result.room], + }, + } : current); + setRoomDecisions((current) => ({ + ...current, + [newRoomSourceKey]: { + roomId: result.room.id, + defaultDistributionBoardId: + current[newRoomSourceKey]?.defaultDistributionBoardId ?? null, + }, + })); + setNewRoomSourceKey(null); + } catch (reason) { + setError(reason instanceof Error ? reason.message : "Raum konnte nicht angelegt werden."); + setNewRoomSourceKey(null); + } finally { + setIsCreatingCatalogEntry(false); + } + } + + async function handleCreateProjectDevice(input: CreateProjectDeviceInput) { + if (!newProjectDeviceFamily) return; + setIsCreatingCatalogEntry(true); + setError(null); + try { + const result = await onCreateProjectDevice(input); + setImportPlan((current) => current ? { + ...current, + existing: { + ...current.existing, + projectDevices: [...current.existing.projectDevices, result.projectDevice], + }, + } : current); + setFamilyLinks((current) => ({ + ...current, + [newProjectDeviceFamily]: result.projectDevice.id, + })); + setNewProjectDeviceFamily(null); + } catch (reason) { + setError(reason instanceof Error ? reason.message : "ProjectDevice konnte nicht angelegt werden."); + setNewProjectDeviceFamily(null); + } finally { + setIsCreatingCatalogEntry(false); + } + } + + if (newRoomSourceKey && importPlan) { + const sourceRoom = importPlan.sourceRooms.find((room) => room.sourceRoomKey === newRoomSourceKey); + if (sourceRoom) { + return ( + ({ ...floor, projectId }))} + initialValues={{ roomNumber: sourceRoom.roomNumber, roomName: sourceRoom.roomName }} + isSaving={isCreatingCatalogEntry} + onClose={() => setNewRoomSourceKey(null)} + onSave={handleCreateRoom} + /> + ); + } + } + + if (newProjectDeviceFamily && importPlan) { + const group = importPlan.familyGroups.find((entry) => entry.familyAndType === newProjectDeviceFamily); + if (group) { + return ( + setNewProjectDeviceFamily(null)} + onImportGlobal={async () => undefined} + onSave={handleCreateProjectDevice} + /> + ); + } + } + return ( setFamilyLinks((current) => ({ ...current, [familyAndType]: projectDeviceId }))} onRoomDecisionChange={(sourceRoomKey, decision) => setRoomDecisions((current) => ({ ...current, [sourceRoomKey]: decision }))} plan={importPlan} @@ -308,6 +415,8 @@ export function RevitCsvImportPlan({ familyLinks, isApplying, onApply, + onCreateProjectDevice, + onCreateRoom, onFamilyLinkChange, onRoomDecisionChange, plan, @@ -318,6 +427,8 @@ export function RevitCsvImportPlan({ familyLinks: Record; isApplying: boolean; onApply: () => void; + onCreateProjectDevice: (familyAndType: string) => void; + onCreateRoom: (sourceRoomKey: string) => void; onFamilyLinkChange: (familyAndType: string, projectDeviceId: string | null) => void; onRoomDecisionChange: (sourceRoomKey: string, decision: RoomDecision) => void; plan: ExternalInitialImportPlanDto; @@ -389,21 +500,31 @@ export function RevitCsvImportPlan({ {sourceRoom.objectCount} - +
+ + +
onFamilyLinkChange(group.familyAndType, event.target.value || null)} - value={familyLinks[group.familyAndType] ?? ""} - > - - {plan.existing.projectDevices.map((device) => ( - - ))} - +
+ + +
))} @@ -486,6 +618,37 @@ function categoryLabel(category: string | null) { return "Ohne Kategorie"; } +export function createRevitProjectDevicePrefill( + plan: ExternalInitialImportPlanDto, + familyAndType: string +): Partial { + const group = plan.familyGroups.find((entry) => entry.familyAndType === familyAndType); + const firstObject = plan.objects.find( + (object) => object.sourceValues.familyAndType === familyAndType + ); + const category = group?.category; + return { + name: group?.internalDeviceType || familyAndType || "Revit-Gerät", + displayName: firstObject?.suggestedPlanningValues.displayName || familyAndType, + connectionKind: group?.connectionKind ?? undefined, + category: + category === "lighting" || category === "three_phase" + ? category + : "single_phase", + quantity: firstObject?.suggestedPlanningValues.effectiveQuantity ?? 1, + powerPerUnit: + firstObject?.suggestedPlanningValues.powerPerUnitW === null || + firstObject?.suggestedPlanningValues.powerPerUnitW === undefined + ? 0.1 + : firstObject.suggestedPlanningValues.powerPerUnitW / 1000, + simultaneityFactor: + firstObject?.suggestedPlanningValues.simultaneityFactor ?? 1, + cosPhi: firstObject?.suggestedPlanningValues.cosPhi ?? undefined, + costGroup: firstObject?.suggestedPlanningValues.costGroup ?? undefined, + remark: firstObject?.suggestedPlanningValues.remark ?? undefined, + }; +} + function Stat({ label, value }: { label: string; value: number }) { return
{label}
{value}
; } function fileToBase64(file: File) { diff --git a/src/frontend/components/room-modal.tsx b/src/frontend/components/room-modal.tsx new file mode 100644 index 0000000..e5648e0 --- /dev/null +++ b/src/frontend/components/room-modal.tsx @@ -0,0 +1,107 @@ +"use client"; + +import React, { type FormEvent, useState } from "react"; +import type { CreateRoomInput, FloorDto, RoomDto } from "../types"; +import { FormModal } from "./form-modal"; + +interface RoomModalProps { + floors: FloorDto[]; + initialRoom?: RoomDto; + initialValues?: Partial; + isSaving: boolean; + onClose: () => void; + onDelete?: () => void; + onSave: (input: CreateRoomInput) => Promise; +} + +export function RoomModal({ + floors, + initialRoom, + initialValues, + isSaving, + onClose, + onDelete, + onSave, +}: RoomModalProps) { + const [roomNumber, setRoomNumber] = useState( + initialRoom?.roomNumber ?? initialValues?.roomNumber ?? "" + ); + const [roomName, setRoomName] = useState( + initialRoom?.roomName ?? initialValues?.roomName ?? "" + ); + const [floorId, setFloorId] = useState( + initialRoom?.floorId ?? initialValues?.floorId ?? "" + ); + + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + if (!roomNumber.trim() || !roomName.trim()) return; + await onSave({ + floorId: floorId || undefined, + roomNumber: roomNumber.trim(), + roomName: roomName.trim(), + }); + } + + return ( + +
+
+ + setRoomNumber(event.target.value)} + required + value={roomNumber} + /> +
+
+ + setRoomName(event.target.value)} + required + value={roomName} + /> +
+
+ + +
+
+ {initialRoom && onDelete ? ( +
+

+ In Stromkreisen verwendete Räume können nicht gelöscht werden. +

+ +
+ ) : null} +
+ ); +} diff --git a/tests/revit-csv-frontend-api.test.ts b/tests/revit-csv-frontend-api.test.ts index 85846d7..d3695e5 100644 --- a/tests/revit-csv-frontend-api.test.ts +++ b/tests/revit-csv-frontend-api.test.ts @@ -3,9 +3,12 @@ import { describe, it } from "node:test"; import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { + createRevitProjectDevicePrefill, RevitCsvImportPlan, RevitCsvModal, } from "../src/frontend/components/revit-csv-modal.js"; +import { ProjectDeviceModal } from "../src/frontend/components/project-device-modal.js"; +import { RoomModal } from "../src/frontend/components/room-modal.js"; import { applyExternalInitialImport, getExternalCsvConfiguration, @@ -98,6 +101,8 @@ describe("Revit CSV modal presentation", () => { createElement(RevitCsvModal, { currentRevision: 0, onClose: () => undefined, + onCreateProjectDevice: async () => ({} as never), + onCreateRoom: async () => ({} as never), onRevisionChange: () => undefined, projectId: "project-1", }) @@ -148,6 +153,8 @@ describe("Revit CSV modal presentation", () => { familyLinks: { "Steckdose : Standard": "device-1" }, isApplying: false, onApply: () => undefined, + onCreateProjectDevice: () => undefined, + onCreateRoom: () => undefined, onFamilyLinkChange: () => undefined, onRoomDecisionChange: () => undefined, plan, @@ -160,6 +167,78 @@ describe("Revit CSV modal presentation", () => { assert.match(markup, /101 · Büro/); assert.match(markup, /Steckdose : Standard/); assert.match(markup, /Erstimport übernehmen/); + assert.match(markup, /Raum 101 neu anlegen/); + assert.match(markup, /ProjectDevice für Steckdose : Standard neu anlegen/); assert.doesNotMatch(markup, /IFC-GUID/); }); + + it("prefills the existing room and ProjectDevice forms from Revit decisions", () => { + const roomMarkup = renderToStaticMarkup(createElement(RoomModal, { + floors: [{ id: "floor-1", projectId: "project-1", name: "EG", sortOrder: 0 }], + initialValues: { roomNumber: "101", roomName: "Büro" }, + isSaving: false, + onClose: () => undefined, + onSave: async () => undefined, + })); + const deviceMarkup = renderToStaticMarkup(createElement(ProjectDeviceModal, { + globalDevices: [], + initialValues: { + name: "socket", + displayName: "Steckdose", + category: "single_phase", + quantity: 2, + powerPerUnit: 0.1, + simultaneityFactor: 1, + }, + isSaving: false, + onClose: () => undefined, + onImportGlobal: async () => undefined, + onSave: async () => undefined, + })); + + assert.match(roomMarkup, /value="101"/); + assert.match(roomMarkup, /value="Büro"/); + assert.match(deviceMarkup, /value="socket"/); + assert.match(deviceMarkup, /value="Steckdose"/); + assert.match(deviceMarkup, /value="2"/); + }); + + it("converts mapped Revit watts into ProjectDevice kilowatts", () => { + const plan = { + familyGroups: [{ + familyAndType: "Steckdose : Standard", + internalDeviceType: "socket", + category: "single_phase", + connectionKind: "230 V", + }], + objects: [{ + sourceValues: { familyAndType: "Steckdose : Standard" }, + suggestedPlanningValues: { + displayName: "Arbeitsplatzsteckdose", + effectiveQuantity: 4, + powerPerUnitW: 250, + simultaneityFactor: 0.8, + cosPhi: 0.95, + costGroup: "440", + remark: "Aus Revit", + }, + }], + } as ExternalInitialImportPlanDto; + + assert.deepEqual( + createRevitProjectDevicePrefill(plan, "Steckdose : Standard"), + { + name: "socket", + displayName: "Arbeitsplatzsteckdose", + connectionKind: "230 V", + category: "single_phase", + quantity: 4, + powerPerUnit: 0.25, + simultaneityFactor: 0.8, + cosPhi: 0.95, + costGroup: "440", + remark: "Aus Revit", + } + ); + }); }); From dac19b093c53cebced242b55d3dfa426c00eeda9 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 2 Aug 2026 18:26:22 +0200 Subject: [PATCH 44/59] Add external row quantity foundation --- AGENTS.md | 13 +- docs/current-architecture.md | 19 +- .../spec/revit-csv-phase-14-audit-and-plan.md | 5 + scripts/db-verify-circuit-schema.js | 16 + src/db/migrations/0005_stale_gorilla_man.sql | 3 + src/db/migrations/meta/0005_snapshot.json | 2404 +++++++++++++++++ src/db/migrations/meta/_journal.json | 7 + ...t-device-row-project-command.repository.ts | 8 + ...ow-structure-project-command.repository.ts | 6 +- ...ircuit-device-row-structure.persistence.ts | 8 + .../circuit-device-row.persistence.ts | 4 + .../circuit-device-row.repository.ts | 1 + ...roup-subtree-project-command.repository.ts | 6 +- ...it-structure-project-command.repository.ts | 6 +- ...oard-subtree-project-command.repository.ts | 6 +- .../project-state-snapshot.persistence.ts | 1 + src/db/schema/circuit-device-rows.ts | 1 + .../circuit-device-row-quantity.ts | 43 + ...ircuit-device-row-project-command.model.ts | 16 +- ...ice-row-structure-project-command.model.ts | 20 +- src/domain/models/circuit-device-row.model.ts | 1 + src/domain/models/circuit-tree.model.ts | 1 + .../models/project-state-snapshot.model.ts | 43 +- src/frontend/types.ts | 1 + .../controllers/circuit-tree.controller.ts | 1 + ...ice-row-project-command.repository.test.ts | 24 + tests/circuit-device-row-quantity.test.ts | 62 + tests/external-model-foundation.test.ts | 3 +- tests/project-command.model.test.ts | 10 +- tests/project-state-snapshot.test.ts | 33 + 30 files changed, 2750 insertions(+), 22 deletions(-) create mode 100644 src/db/migrations/0005_stale_gorilla_man.sql create mode 100644 src/db/migrations/meta/0005_snapshot.json create mode 100644 src/domain/calculations/circuit-device-row-quantity.ts create mode 100644 tests/circuit-device-row-quantity.test.ts diff --git a/AGENTS.md b/AGENTS.md index e15bf4c..054b9c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 project data atomically and records a new `restore` revision with a complete 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 snapshots are upgraded with an empty external-model state; version 2 snapshots are additionally upgraded with an empty external CSV configuration. Supported @@ -263,7 +265,7 @@ project graph. Project-wide Revit CSV configuration uses `external-csv-configuration.update`. Its complete expected/target snapshot is 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 id and project link. 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 `(sourceId, ifcGuid)`. Source values, planning values and overrides are separate. 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 preserving IFCGUIDs, source values, original bytes and the classified matrix. Import batches additionally store the monotonic project CSV configuration version introduced by additive migration `0004`; it is distinct from the CSV 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 `external-import.apply-initial`. The command rechecks configuration version, original-byte SHA-256, parsed matrix, complete IFCGUID/source values, explicit diff --git a/docs/current-architecture.md b/docs/current-architecture.md index cb46030..f55baa7 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -416,7 +416,9 @@ wieder her. verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und 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 projektweite Revit-CSV-Konfiguration, die Gebäudekennzeichnung sowie den Gleichzeitigkeitsfaktor, 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, stromkreislistenweite BMK-Eindeutigkeit über Stromkreise und 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 Unterbäume sowie vollständige Gruppensortierung sind integriert. Der Editor 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 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 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 nicht importierten Projekten. Der letzte bestätigte Batch wird aus der 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 Projektduplikat jede interne UUID; IFCGUID und Quelltransport bleiben 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, Redo verwendet dieselben UUIDs und Bytes. Bloße Vorschau und Importplanung 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 ändern die Konfiguration; der PUT plant Identität und nächsten Konfigurationsstand serverseitig und verwendet den typisierten Command. diff --git a/docs/spec/revit-csv-phase-14-audit-and-plan.md b/docs/spec/revit-csv-phase-14-audit-and-plan.md index f959dc4..7fb4f88 100644 --- a/docs/spec/revit-csv-phase-14-audit-and-plan.md +++ b/docs/spec/revit-csv-phase-14-audit-and-plan.md @@ -313,6 +313,11 @@ Kombination aus Originalbytes und Matrix als Entscheidungen bestätigt. ### 14.3 – Editor und explizite Zuordnung 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. 3. Verteilungsbezogenen Drawer, Filter und Vorschau der Mengenwirkung ergänzen. 4. Einzel- und Mehrfach-Drag-and-drop samt Warnungen, Undo/Redo und Reload diff --git a/scripts/db-verify-circuit-schema.js b/scripts/db-verify-circuit-schema.js index 318e549..8680f59 100644 --- a/scripts/db-verify-circuit-schema.js +++ b/scripts/db-verify-circuit-schema.js @@ -40,6 +40,13 @@ const projectDeviceColumns = new Set( const missingProjectDeviceColumns = requiredProjectDeviceColumns.filter( (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 = [ "installed_power_per_unit_kw", "demand_factor", @@ -131,6 +138,7 @@ console.log("Database:", dbPath); console.log("Required tables:", requiredTables.join(", ")); console.log("Existing tables:", [...existing].join(", ") || "(none)"); 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-section columns:", @@ -171,6 +179,14 @@ if (missingCircuitColumns.length > 0) { process.exit(1); } +if (missingCircuitDeviceRowColumns.length > 0) { + console.error( + "Missing circuit-device-row columns:", + missingCircuitDeviceRowColumns.join(", ") + ); + process.exit(1); +} + if (remainingRemovedCircuitColumns.length > 0) { console.error( "Transitional circuit protection columns still present:", diff --git a/src/db/migrations/0005_stale_gorilla_man.sql b/src/db/migrations/0005_stale_gorilla_man.sql new file mode 100644 index 0000000..765d7bc --- /dev/null +++ b/src/db/migrations/0005_stale_gorilla_man.sql @@ -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`; diff --git a/src/db/migrations/meta/0005_snapshot.json b/src/db/migrations/meta/0005_snapshot.json new file mode 100644 index 0000000..acae98c --- /dev/null +++ b/src/db/migrations/meta/0005_snapshot.json @@ -0,0 +1,2404 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "b79934fe-5d05-49da-b7c4-95c954e5fec2", + "prevId": "da7eb583-b86a-440c-a71e-babb5ee24cd1", + "tables": { + "circuit_device_rows": { + "name": "circuit_device_rows", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_id": { + "name": "circuit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "linked_project_device_id": { + "name": "linked_project_device_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase_type": { + "name": "phase_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_kind": { + "name": "connection_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_group": { + "name": "cost_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_number_snapshot": { + "name": "room_number_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_name_snapshot": { + "name": "room_name_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "manual_quantity": { + "name": "manual_quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "power_per_unit": { + "name": "power_per_unit", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cos_phi": { + "name": "cos_phi", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "overridden_fields": { + "name": "overridden_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "circuit_device_rows_circuit_id_circuits_id_fk": { + "name": "circuit_device_rows_circuit_id_circuits_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "circuits", + "columnsFrom": [ + "circuit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { + "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "project_devices", + "columnsFrom": [ + "linked_project_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "circuit_device_rows_room_id_rooms_id_fk": { + "name": "circuit_device_rows_room_id_rooms_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_list_equipment_identifiers": { + "name": "circuit_list_equipment_identifiers", + "columns": { + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "circuit_list_equipment_identifiers_owner_unique": { + "name": "circuit_list_equipment_identifiers_owner_unique", + "columns": [ + "owner_type", + "owner_id" + ], + "isUnique": true + }, + "circuit_list_equipment_identifiers_list_identifier_unique": { + "name": "circuit_list_equipment_identifiers_list_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk": { + "name": "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuit_list_equipment_identifiers", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_lists": { + "name": "circuit_lists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "distribution_board_id": { + "name": "distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "circuit_lists_distribution_board_id_unique": { + "name": "circuit_lists_distribution_board_id_unique", + "columns": [ + "distribution_board_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_lists_project_id_projects_id_fk": { + "name": "circuit_lists_project_id_projects_id_fk", + "tableFrom": "circuit_lists", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuit_lists_distribution_board_id_distribution_boards_id_fk": { + "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "circuit_lists", + "tableTo": "distribution_boards", + "columnsFrom": [ + "distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_protection_devices": { + "name": "circuit_protection_devices", + "columns": { + "circuit_id": { + "name": "circuit_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rated_current_a": { + "name": "rated_current_a", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fuse_utilization_category": { + "name": "fuse_utilization_category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trip_characteristic": { + "name": "trip_characteristic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_type": { + "name": "rcd_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rated_residual_current_ma": { + "name": "rated_residual_current_ma", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "circuit_protection_devices_circuit_id_circuits_id_fk": { + "name": "circuit_protection_devices_circuit_id_circuits_id_fk", + "tableFrom": "circuit_protection_devices", + "tableTo": "circuits", + "columnsFrom": [ + "circuit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_sections": { + "name": "circuit_sections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_number": { + "name": "group_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "circuit_sections_list_key_unique": { + "name": "circuit_sections_list_key_unique", + "columns": [ + "circuit_list_id", + "key" + ], + "isUnique": true + }, + "circuit_sections_list_prefix_unique": { + "name": "circuit_sections_list_prefix_unique", + "columns": [ + "circuit_list_id", + "prefix" + ], + "isUnique": true + }, + "circuit_sections_list_category_group_unique": { + "name": "circuit_sections_list_category_group_unique", + "columns": [ + "circuit_list_id", + "category", + "group_number" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_sections_circuit_list_id_circuit_lists_id_fk": { + "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuit_sections", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuits": { + "name": "circuits", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cable_type": { + "name": "cable_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cable_cross_section": { + "name": "cable_cross_section", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cable_length": { + "name": "cable_length", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_assignment": { + "name": "rcd_assignment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_designation": { + "name": "terminal_designation", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voltage": { + "name": "voltage", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "control_requirement": { + "name": "control_requirement", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_reserve": { + "name": "is_reserve", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "circuits_list_equipment_identifier_unique": { + "name": "circuits_list_equipment_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuits_circuit_list_id_circuit_lists_id_fk": { + "name": "circuits_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuits", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuits_section_id_circuit_sections_id_fk": { + "name": "circuits_section_id_circuit_sections_id_fk", + "tableFrom": "circuits", + "tableTo": "circuit_sections", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_board_component_protection_devices": { + "name": "distribution_board_component_protection_devices", + "columns": { + "component_id": { + "name": "component_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rated_current_a": { + "name": "rated_current_a", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fuse_utilization_category": { + "name": "fuse_utilization_category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trip_characteristic": { + "name": "trip_characteristic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_type": { + "name": "rcd_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rated_residual_current_ma": { + "name": "rated_residual_current_ma", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk": { + "name": "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk", + "tableFrom": "distribution_board_component_protection_devices", + "tableTo": "distribution_board_components", + "columnsFrom": [ + "component_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_board_components": { + "name": "distribution_board_components", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "placement": { + "name": "placement", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "distribution_board_components_list_identifier_unique": { + "name": "distribution_board_components_list_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + }, + "distribution_board_components_section_role_unique": { + "name": "distribution_board_components_section_role_unique", + "columns": [ + "section_id", + "role" + ], + "isUnique": true + } + }, + "foreignKeys": { + "distribution_board_components_circuit_list_id_circuit_lists_id_fk": { + "name": "distribution_board_components_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "distribution_board_components", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "distribution_board_components_section_id_circuit_sections_id_fk": { + "name": "distribution_board_components_section_id_circuit_sections_id_fk", + "tableFrom": "distribution_board_components", + "tableTo": "circuit_sections", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_boards": { + "name": "distribution_boards", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "floor_id": { + "name": "floor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supply_type": { + "name": "supply_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "distribution_boards_project_id_projects_id_fk": { + "name": "distribution_boards_project_id_projects_id_fk", + "tableFrom": "distribution_boards", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "distribution_boards_floor_id_floors_id_fk": { + "name": "distribution_boards_floor_id_floors_id_fk", + "tableFrom": "distribution_boards", + "tableTo": "floors", + "columnsFrom": [ + "floor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_csv_configurations": { + "name": "external_csv_configurations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration_version": { + "name": "configuration_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration": { + "name": "configuration", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_csv_configurations_project_id_unique": { + "name": "external_csv_configurations_project_id_unique", + "columns": [ + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "external_csv_configurations_project_id_projects_id_fk": { + "name": "external_csv_configurations_project_id_projects_id_fk", + "tableFrom": "external_csv_configurations", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_import_batches": { + "name": "external_import_batches", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "import_kind": { + "name": "import_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imported_at_iso": { + "name": "imported_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_project_revision": { + "name": "applied_project_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration_version": { + "name": "configuration_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration_snapshot": { + "name": "configuration_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "original_bytes": { + "name": "original_bytes", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "document": { + "name": "document", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_import_batches_project_revision_idx": { + "name": "external_import_batches_project_revision_idx", + "columns": [ + "project_id", + "applied_project_revision" + ], + "isUnique": false + }, + "external_import_batches_source_imported_idx": { + "name": "external_import_batches_source_imported_idx", + "columns": [ + "source_id", + "imported_at_iso" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_import_batches_project_id_projects_id_fk": { + "name": "external_import_batches_project_id_projects_id_fk", + "tableFrom": "external_import_batches", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_import_batches_source_id_external_model_sources_id_fk": { + "name": "external_import_batches_source_id_external_model_sources_id_fk", + "tableFrom": "external_import_batches", + "tableTo": "external_model_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_model_objects": { + "name": "external_model_objects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ifc_guid": { + "name": "ifc_guid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen_import_batch_id": { + "name": "last_seen_import_batch_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_accepted_import_batch_id": { + "name": "last_accepted_import_batch_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accepted_source_values": { + "name": "accepted_source_values", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "planning_values": { + "name": "planning_values", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "overridden_fields": { + "name": "overridden_fields", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_room_mapping_id": { + "name": "external_room_mapping_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "distribution_board_id": { + "name": "distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_project_device_id": { + "name": "linked_project_device_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "circuit_device_row_id": { + "name": "circuit_device_row_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "presence_status": { + "name": "presence_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'present'" + } + }, + "indexes": { + "external_model_objects_source_ifc_guid_unique": { + "name": "external_model_objects_source_ifc_guid_unique", + "columns": [ + "source_id", + "ifc_guid" + ], + "isUnique": true + }, + "external_model_objects_project_presence_idx": { + "name": "external_model_objects_project_presence_idx", + "columns": [ + "project_id", + "presence_status" + ], + "isUnique": false + }, + "external_model_objects_distribution_board_idx": { + "name": "external_model_objects_distribution_board_idx", + "columns": [ + "distribution_board_id" + ], + "isUnique": false + }, + "external_model_objects_circuit_device_row_idx": { + "name": "external_model_objects_circuit_device_row_idx", + "columns": [ + "circuit_device_row_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_model_objects_project_id_projects_id_fk": { + "name": "external_model_objects_project_id_projects_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_model_objects_source_id_external_model_sources_id_fk": { + "name": "external_model_objects_source_id_external_model_sources_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_model_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_model_objects_last_seen_import_batch_id_external_import_batches_id_fk": { + "name": "external_model_objects_last_seen_import_batch_id_external_import_batches_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_import_batches", + "columnsFrom": [ + "last_seen_import_batch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "external_model_objects_last_accepted_import_batch_id_external_import_batches_id_fk": { + "name": "external_model_objects_last_accepted_import_batch_id_external_import_batches_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_import_batches", + "columnsFrom": [ + "last_accepted_import_batch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "external_model_objects_external_room_mapping_id_external_room_mappings_id_fk": { + "name": "external_model_objects_external_room_mapping_id_external_room_mappings_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_room_mappings", + "columnsFrom": [ + "external_room_mapping_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_model_objects_distribution_board_id_distribution_boards_id_fk": { + "name": "external_model_objects_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "distribution_boards", + "columnsFrom": [ + "distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_model_objects_linked_project_device_id_project_devices_id_fk": { + "name": "external_model_objects_linked_project_device_id_project_devices_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "project_devices", + "columnsFrom": [ + "linked_project_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_model_objects_circuit_device_row_id_circuit_device_rows_id_fk": { + "name": "external_model_objects_circuit_device_row_id_circuit_device_rows_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "circuit_device_rows", + "columnsFrom": [ + "circuit_device_row_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_model_sources": { + "name": "external_model_sources", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'revit_csv'" + } + }, + "indexes": { + "external_model_sources_project_type_unique": { + "name": "external_model_sources_project_type_unique", + "columns": [ + "project_id", + "source_type" + ], + "isUnique": true + } + }, + "foreignKeys": { + "external_model_sources_project_id_projects_id_fk": { + "name": "external_model_sources_project_id_projects_id_fk", + "tableFrom": "external_model_sources", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_room_mappings": { + "name": "external_room_mappings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "normalized_source_room_key": { + "name": "normalized_source_room_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_floor_name": { + "name": "source_floor_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_room_number": { + "name": "source_room_number", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_room_name": { + "name": "source_room_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_distribution_board_id": { + "name": "default_distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "external_room_mappings_source_key_unique": { + "name": "external_room_mappings_source_key_unique", + "columns": [ + "source_id", + "normalized_source_room_key" + ], + "isUnique": true + }, + "external_room_mappings_project_room_idx": { + "name": "external_room_mappings_project_room_idx", + "columns": [ + "project_id", + "room_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_room_mappings_project_id_projects_id_fk": { + "name": "external_room_mappings_project_id_projects_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_room_mappings_source_id_external_model_sources_id_fk": { + "name": "external_room_mappings_source_id_external_model_sources_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "external_model_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_room_mappings_room_id_rooms_id_fk": { + "name": "external_room_mappings_room_id_rooms_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_room_mappings_default_distribution_board_id_distribution_boards_id_fk": { + "name": "external_room_mappings_default_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "distribution_boards", + "columnsFrom": [ + "default_distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "floors": { + "name": "floors", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "floors_project_id_projects_id_fk": { + "name": "floors_project_id_projects_id_fk", + "tableFrom": "floors", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "global_devices": { + "name": "global_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "installed_power_per_unit_kw": { + "name": "installed_power_per_unit_kw", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "demand_factor": { + "name": "demand_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "voltage_v": { + "name": "voltage_v", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phase_count": { + "name": "phase_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "power_factor": { + "name": "power_factor", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_change_sets": { + "name": "project_change_sets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_revision_id": { + "name": "project_revision_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_type": { + "name": "command_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_schema_version": { + "name": "payload_schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forward_payload_json": { + "name": "forward_payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inverse_payload_json": { + "name": "inverse_payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_change_sets_revision_unique": { + "name": "project_change_sets_revision_unique", + "columns": [ + "project_revision_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_change_sets_project_revision_id_project_revisions_id_fk": { + "name": "project_change_sets_project_revision_id_project_revisions_id_fk", + "tableFrom": "project_change_sets", + "tableTo": "project_revisions", + "columnsFrom": [ + "project_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_devices": { + "name": "project_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase_type": { + "name": "phase_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'single_phase'" + }, + "connection_kind": { + "name": "connection_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_group": { + "name": "cost_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "power_per_unit": { + "name": "power_per_unit", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "cos_phi": { + "name": "cos_phi", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voltage_v": { + "name": "voltage_v", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "project_devices_project_id_projects_id_fk": { + "name": "project_devices_project_id_projects_id_fk", + "tableFrom": "project_devices", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_history_stack_entries": { + "name": "project_history_stack_entries", + "columns": { + "change_set_id": { + "name": "change_set_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stack": { + "name": "stack", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_history_stack_entries_position_unique": { + "name": "project_history_stack_entries_position_unique", + "columns": [ + "project_id", + "stack", + "position" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { + "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", + "tableFrom": "project_history_stack_entries", + "tableTo": "project_change_sets", + "columnsFrom": [ + "change_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_history_stack_entries_project_id_projects_id_fk": { + "name": "project_history_stack_entries_project_id_projects_id_fk", + "tableFrom": "project_history_stack_entries", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "project_history_stack_entries_stack_check": { + "name": "project_history_stack_entries_stack_check", + "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" + } + } + }, + "project_revisions": { + "name": "project_revisions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "project_revisions_project_number_unique": { + "name": "project_revisions_project_number_unique", + "columns": [ + "project_id", + "revision_number" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_revisions_project_id_projects_id_fk": { + "name": "project_revisions_project_id_projects_id_fk", + "tableFrom": "project_revisions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_snapshots": { + "name": "project_snapshots", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_revision": { + "name": "source_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'named'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_sha256": { + "name": "payload_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "project_snapshots_project_created_idx": { + "name": "project_snapshots_project_created_idx", + "columns": [ + "project_id", + "created_at_iso" + ], + "isUnique": false + }, + "project_snapshots_project_kind_revision_idx": { + "name": "project_snapshots_project_kind_revision_idx", + "columns": [ + "project_id", + "kind", + "source_revision" + ], + "isUnique": false + }, + "project_snapshots_project_name_unique": { + "name": "project_snapshots_project_name_unique", + "columns": [ + "project_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_snapshots_project_id_projects_id_fk": { + "name": "project_snapshots_project_id_projects_id_fk", + "tableFrom": "project_snapshots", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_project_number": { + "name": "internal_project_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_project_number": { + "name": "external_project_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "building_owner": { + "name": "building_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_public_building": { + "name": "is_public_building", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "single_phase_voltage_v": { + "name": "single_phase_voltage_v", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 230 + }, + "three_phase_voltage_v": { + "name": "three_phase_voltage_v", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 400 + }, + "enabled_distribution_board_supply_types": { + "name": "enabled_distribution_board_supply_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[\"AV\",\"SV\",\"EV\",\"USV\",\"MSR\",\"SiBe\"]'" + }, + "current_revision": { + "name": "current_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rooms": { + "name": "rooms", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "floor_id": { + "name": "floor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_number": { + "name": "room_number", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "room_name": { + "name": "room_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "rooms_project_id_projects_id_fk": { + "name": "rooms_project_id_projects_id_fk", + "tableFrom": "rooms", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rooms_floor_id_floors_id_fk": { + "name": "rooms_floor_id_floors_id_fk", + "tableFrom": "rooms", + "tableTo": "floors", + "columnsFrom": [ + "floor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index e3f76b7..5a65548 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1785684457208, "tag": "0004_illegal_the_stranger", "breakpoints": true + }, + { + "idx": 5, + "version": "6", + "when": 1785687503453, + "tag": "0005_stale_gorilla_man", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/repositories/circuit-device-row-project-command.repository.ts b/src/db/repositories/circuit-device-row-project-command.repository.ts index 1eba241..ecfa646 100644 --- a/src/db/repositories/circuit-device-row-project-command.repository.ts +++ b/src/db/repositories/circuit-device-row-project-command.repository.ts @@ -82,6 +82,14 @@ export class CircuitDeviceRowProjectCommandRepository change.value, ]) ) 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 ( input.source === "user" && patch.phaseType !== undefined && diff --git a/src/db/repositories/circuit-device-row-structure-project-command.repository.ts b/src/db/repositories/circuit-device-row-structure-project-command.repository.ts index 4d9532d..6919d20 100644 --- a/src/db/repositories/circuit-device-row-structure-project-command.repository.ts +++ b/src/db/repositories/circuit-device-row-structure-project-command.repository.ts @@ -20,6 +20,7 @@ import { circuitLists } from "../schema/circuit-lists.js"; import { circuits } from "../schema/circuits.js"; import { assertCircuitDeviceRowReferencesInProject, + toCircuitDeviceRowInsertValues, toCircuitDeviceRowSnapshot, } from "./circuit-device-row-structure.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."); } - database.insert(circuitDeviceRows).values(row).run(); + database + .insert(circuitDeviceRows) + .values(toCircuitDeviceRowInsertValues(row)) + .run(); const circuitUpdate = database .update(circuits) .set({ isReserve: 0 }) diff --git a/src/db/repositories/circuit-device-row-structure.persistence.ts b/src/db/repositories/circuit-device-row-structure.persistence.ts index fffaff4..6e567d4 100644 --- a/src/db/repositories/circuit-device-row-structure.persistence.ts +++ b/src/db/repositories/circuit-device-row-structure.persistence.ts @@ -61,6 +61,7 @@ export function toCircuitDeviceRowSnapshot( roomNumberSnapshot: row.roomNumberSnapshot, roomNameSnapshot: row.roomNameSnapshot, quantity: row.quantity, + manualQuantity: row.manualQuantity, powerPerUnit: row.powerPerUnit, simultaneityFactor: row.simultaneityFactor, cosPhi: row.cosPhi, @@ -68,3 +69,10 @@ export function toCircuitDeviceRowSnapshot( overriddenFields: row.overriddenFields, }; } + +export function toCircuitDeviceRowInsertValues(row: CircuitDeviceRowSnapshot) { + return { + ...row, + manualQuantity: row.manualQuantity ?? row.quantity, + }; +} diff --git a/src/db/repositories/circuit-device-row.persistence.ts b/src/db/repositories/circuit-device-row.persistence.ts index e1f9222..c075f61 100644 --- a/src/db/repositories/circuit-device-row.persistence.ts +++ b/src/db/repositories/circuit-device-row.persistence.ts @@ -13,6 +13,7 @@ export interface CircuitDeviceRowUpdateInput { roomNumberSnapshot?: string; roomNameSnapshot?: string; quantity: number; + manualQuantity?: number; powerPerUnit: number; simultaneityFactor: number; cosPhi?: number; @@ -34,6 +35,7 @@ export interface CircuitDeviceRowPatchInput { roomNumberSnapshot?: string | null; roomNameSnapshot?: string | null; quantity?: number; + manualQuantity?: number; powerPerUnit?: number; simultaneityFactor?: number; cosPhi?: number | null; @@ -61,6 +63,7 @@ export function toCircuitDeviceRowUpdateValues(input: CircuitDeviceRowUpdateInpu roomNumberSnapshot: input.roomNumberSnapshot ?? null, roomNameSnapshot: input.roomNameSnapshot ?? null, quantity: input.quantity, + manualQuantity: input.manualQuantity ?? input.quantity, powerPerUnit: input.powerPerUnit, simultaneityFactor: input.simultaneityFactor, cosPhi: input.cosPhi ?? null, @@ -90,6 +93,7 @@ export function toCircuitDeviceRowPatchValues(input: CircuitDeviceRowPatchInput) } if (has("roomNameSnapshot")) values.roomNameSnapshot = input.roomNameSnapshot ?? null; 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.simultaneityFactor !== undefined) { values.simultaneityFactor = input.simultaneityFactor; diff --git a/src/db/repositories/circuit-device-row.repository.ts b/src/db/repositories/circuit-device-row.repository.ts index 6bdb550..99c0860 100644 --- a/src/db/repositories/circuit-device-row.repository.ts +++ b/src/db/repositories/circuit-device-row.repository.ts @@ -37,6 +37,7 @@ export class CircuitDeviceRowRepository { roomNumberSnapshot: circuitDeviceRows.roomNumberSnapshot, roomNameSnapshot: circuitDeviceRows.roomNameSnapshot, quantity: circuitDeviceRows.quantity, + manualQuantity: circuitDeviceRows.manualQuantity, powerPerUnit: circuitDeviceRows.powerPerUnit, simultaneityFactor: circuitDeviceRows.simultaneityFactor, cosPhi: circuitDeviceRows.cosPhi, diff --git a/src/db/repositories/circuit-group-subtree-project-command.repository.ts b/src/db/repositories/circuit-group-subtree-project-command.repository.ts index 605146d..448cf99 100644 --- a/src/db/repositories/circuit-group-subtree-project-command.repository.ts +++ b/src/db/repositories/circuit-group-subtree-project-command.repository.ts @@ -24,6 +24,7 @@ import { circuitSections } from "../schema/circuit-sections.js"; import { circuits } from "../schema/circuits.js"; import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.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"; export class CircuitGroupSubtreeProjectCommandRepository @@ -212,7 +213,10 @@ export class CircuitGroupSubtreeProjectCommandRepository .run(); } if (deviceRows.length > 0) { - database.insert(circuitDeviceRows).values(deviceRows).run(); + database + .insert(circuitDeviceRows) + .values(deviceRows.map(toCircuitDeviceRowInsertValues)) + .run(); } } } diff --git a/src/db/repositories/circuit-structure-project-command.repository.ts b/src/db/repositories/circuit-structure-project-command.repository.ts index 8e76e99..454028e 100644 --- a/src/db/repositories/circuit-structure-project-command.repository.ts +++ b/src/db/repositories/circuit-structure-project-command.repository.ts @@ -22,6 +22,7 @@ import { circuitSections } from "../schema/circuit-sections.js"; import { circuits } from "../schema/circuits.js"; import { assertCircuitDeviceRowReferencesInProject, + toCircuitDeviceRowInsertValues, toCircuitDeviceRowSnapshot, } from "./circuit-device-row-structure.persistence.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; @@ -166,7 +167,10 @@ export class CircuitStructureProjectCommandRepository }) .run(); if (snapshot.deviceRows.length > 0) { - database.insert(circuitDeviceRows).values(snapshot.deviceRows).run(); + database + .insert(circuitDeviceRows) + .values(snapshot.deviceRows.map(toCircuitDeviceRowInsertValues)) + .run(); } if (snapshot.protectionDevice) { database diff --git a/src/db/repositories/distribution-board-subtree-project-command.repository.ts b/src/db/repositories/distribution-board-subtree-project-command.repository.ts index 0fab26d..63038eb 100644 --- a/src/db/repositories/distribution-board-subtree-project-command.repository.ts +++ b/src/db/repositories/distribution-board-subtree-project-command.repository.ts @@ -29,6 +29,7 @@ import { floors } from "../schema/floors.js"; import { projectDevices } from "../schema/project-devices.js"; import { projects } from "../schema/projects.js"; import { rooms } from "../schema/rooms.js"; +import { toCircuitDeviceRowInsertValues } from "./circuit-device-row-structure.persistence.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; export class DistributionBoardSubtreeProjectCommandRepository @@ -253,7 +254,10 @@ export class DistributionBoardSubtreeProjectCommandRepository .run(); } if (deviceRows.length > 0) { - database.insert(circuitDeviceRows).values(deviceRows).run(); + database + .insert(circuitDeviceRows) + .values(deviceRows.map(toCircuitDeviceRowInsertValues)) + .run(); } } } diff --git a/src/db/repositories/project-state-snapshot.persistence.ts b/src/db/repositories/project-state-snapshot.persistence.ts index 1fb96da..4dbca90 100644 --- a/src/db/repositories/project-state-snapshot.persistence.ts +++ b/src/db/repositories/project-state-snapshot.persistence.ts @@ -192,6 +192,7 @@ export function readProjectStateSnapshot( roomNumberSnapshot: circuitDeviceRows.roomNumberSnapshot, roomNameSnapshot: circuitDeviceRows.roomNameSnapshot, quantity: circuitDeviceRows.quantity, + manualQuantity: circuitDeviceRows.manualQuantity, powerPerUnit: circuitDeviceRows.powerPerUnit, simultaneityFactor: circuitDeviceRows.simultaneityFactor, cosPhi: circuitDeviceRows.cosPhi, diff --git a/src/db/schema/circuit-device-rows.ts b/src/db/schema/circuit-device-rows.ts index 805b590..a5da01e 100644 --- a/src/db/schema/circuit-device-rows.ts +++ b/src/db/schema/circuit-device-rows.ts @@ -25,6 +25,7 @@ export const circuitDeviceRows = sqliteTable("circuit_device_rows", { roomNumberSnapshot: text("room_number_snapshot"), roomNameSnapshot: text("room_name_snapshot"), quantity: integer("quantity").notNull(), + manualQuantity: integer("manual_quantity").notNull().default(0), powerPerUnit: real("power_per_unit").notNull(), simultaneityFactor: real("simultaneity_factor").notNull(), cosPhi: real("cos_phi"), diff --git a/src/domain/calculations/circuit-device-row-quantity.ts b/src/domain/calculations/circuit-device-row-quantity.ts new file mode 100644 index 0000000..3833e57 --- /dev/null +++ b/src/domain/calculations/circuit-device-row-quantity.ts @@ -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.`); + } +} diff --git a/src/domain/models/circuit-device-row-project-command.model.ts b/src/domain/models/circuit-device-row-project-command.model.ts index 5e79d04..825701f 100644 --- a/src/domain/models/circuit-device-row-project-command.model.ts +++ b/src/domain/models/circuit-device-row-project-command.model.ts @@ -18,6 +18,7 @@ export interface CircuitDeviceRowUpdateValues { roomNumberSnapshot: string | null; roomNameSnapshot: string | null; quantity: number; + manualQuantity: number; powerPerUnit: number; simultaneityFactor: number; cosPhi: number | null; @@ -51,7 +52,18 @@ export function createCircuitDeviceRowUpdateProjectCommand( rowId: string, patch: CircuitDeviceRowUpdatePatch ): 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, value, })) as CircuitDeviceRowUpdateFieldChange[]; @@ -125,6 +137,7 @@ function assertCircuitDeviceRowUpdateFieldValue( } if ( field === "quantity" || + field === "manualQuantity" || field === "powerPerUnit" || field === "simultaneityFactor" ) { @@ -164,6 +177,7 @@ function isCircuitDeviceRowUpdateField( "roomNumberSnapshot", "roomNameSnapshot", "quantity", + "manualQuantity", "powerPerUnit", "simultaneityFactor", "cosPhi", diff --git a/src/domain/models/circuit-device-row-structure-project-command.model.ts b/src/domain/models/circuit-device-row-structure-project-command.model.ts index a897b6b..d6e3cf6 100644 --- a/src/domain/models/circuit-device-row-structure-project-command.model.ts +++ b/src/domain/models/circuit-device-row-structure-project-command.model.ts @@ -22,6 +22,7 @@ export interface CircuitDeviceRowSnapshot { roomNumberSnapshot: string | null; roomNameSnapshot: string | null; quantity: number; + manualQuantity?: number; powerPerUnit: number; simultaneityFactor: number; cosPhi: number | null; @@ -57,10 +58,14 @@ export type CircuitDeviceRowStructureProjectCommand = export function createCircuitDeviceRowInsertProjectCommand( row: CircuitDeviceRowSnapshot ): CircuitDeviceRowInsertProjectCommand { + const normalizedRow = { + ...row, + manualQuantity: row.manualQuantity ?? row.quantity, + }; const command: CircuitDeviceRowInsertProjectCommand = { schemaVersion: circuitDeviceRowStructureCommandSchemaVersion, type: circuitDeviceRowInsertCommandType, - payload: { row }, + payload: { row: normalizedRow }, }; assertCircuitDeviceRowInsertProjectCommand(command); return command; @@ -117,7 +122,13 @@ export function assertCircuitDeviceRowInsertProjectCommand( ] as const) { 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.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); if (value < 0) { throw new Error(`${field} must not be negative.`); diff --git a/src/domain/models/circuit-device-row.model.ts b/src/domain/models/circuit-device-row.model.ts index 2b6c11c..f18c63c 100644 --- a/src/domain/models/circuit-device-row.model.ts +++ b/src/domain/models/circuit-device-row.model.ts @@ -14,6 +14,7 @@ export interface CircuitDeviceRow { roomNumberSnapshot?: string; roomNameSnapshot?: string; quantity: number; + manualQuantity: number; powerPerUnit: number; simultaneityFactor: number; cosPhi?: number; diff --git a/src/domain/models/circuit-tree.model.ts b/src/domain/models/circuit-tree.model.ts index 37d68ca..71fb743 100644 --- a/src/domain/models/circuit-tree.model.ts +++ b/src/domain/models/circuit-tree.model.ts @@ -13,6 +13,7 @@ export interface CircuitTreeDeviceRow { roomNumberSnapshot?: string; roomNameSnapshot?: string; quantity: number; + manualQuantity: number; powerPerUnit: number; simultaneityFactor: number; cosPhi?: number; diff --git a/src/domain/models/project-state-snapshot.model.ts b/src/domain/models/project-state-snapshot.model.ts index 77ad47f..060c795 100644 --- a/src/domain/models/project-state-snapshot.model.ts +++ b/src/domain/models/project-state-snapshot.model.ts @@ -27,8 +27,9 @@ import { type ExternalModelStateSnapshot, } from "../../external-model/domain/external-model-contracts.js"; -export const projectStateSnapshotSchemaVersion = 4 as const; -const previousProjectStateSnapshotSchemaVersion = 3 as const; +export const projectStateSnapshotSchemaVersion = 5 as const; +const previousProjectStateSnapshotSchemaVersion = 4 as const; +const externalModelProjectStateSnapshotSchemaVersion = 3 as const; const legacyProjectStateSnapshotSchemaVersion = 2 as const; const baselineProjectStateSnapshotSchemaVersion = 1 as const; @@ -103,8 +104,15 @@ const circuitSectionSchema = z } }); -const circuitDeviceRowSchema = z - .object({ +const circuitDeviceRowSchema = z.preprocess( + (value) => { + if (value === null || typeof value !== "object" || Array.isArray(value)) return value; + const row = value as Record; + return Object.prototype.hasOwnProperty.call(row, "manualQuantity") + ? row + : { ...row, manualQuantity: row.quantity }; + }, + z.object({ id: idSchema, circuitId: idSchema, linkedProjectDeviceId: idSchema.nullable(), @@ -120,13 +128,15 @@ const circuitDeviceRowSchema = z roomNumberSnapshot: nullableStringSchema, roomNameSnapshot: nullableStringSchema, quantity: finiteNumberSchema.nonnegative(), + manualQuantity: finiteNumberSchema.nonnegative(), powerPerUnit: finiteNumberSchema.nonnegative(), simultaneityFactor: finiteNumberSchema.nonnegative(), cosPhi: finiteNumberSchema.positive().nullable(), remark: nullableStringSchema, overriddenFields: nullableStringSchema, }) - .strict(); + .strict() +); const circuitSchema = z .object({ @@ -453,6 +463,17 @@ const previousProjectStateSnapshotSchema = z project: projectSchema, distributionBoards: z.array(distributionBoardSchema), 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, }) .strict(); @@ -502,6 +523,13 @@ function upgradePreviousProjectStateSnapshot(value: unknown): unknown { const schemaVersion = (value as { schemaVersion: number }).schemaVersion; if (schemaVersion === previousProjectStateSnapshotSchemaVersion) { const previous = previousProjectStateSnapshotSchema.parse(value); + return { + ...previous, + schemaVersion: projectStateSnapshotSchemaVersion, + }; + } + if (schemaVersion === externalModelProjectStateSnapshotSchemaVersion) { + const previous = externalModelProjectStateSnapshotSchema.parse(value); return { ...previous, schemaVersion: projectStateSnapshotSchemaVersion, @@ -765,6 +793,11 @@ function assertProjectStateSnapshotRelations( if (row.roomId !== null) { 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." + ); + } } } diff --git a/src/frontend/types.ts b/src/frontend/types.ts index feea899..6effc53 100644 --- a/src/frontend/types.ts +++ b/src/frontend/types.ts @@ -350,6 +350,7 @@ export interface CircuitTreeDeviceRowDto { roomNumberSnapshot?: string; roomNameSnapshot?: string; quantity: number; + manualQuantity: number; powerPerUnit: number; simultaneityFactor: number; cosPhi?: number; diff --git a/src/server/controllers/circuit-tree.controller.ts b/src/server/controllers/circuit-tree.controller.ts index b7f0e0b..b7c86bf 100644 --- a/src/server/controllers/circuit-tree.controller.ts +++ b/src/server/controllers/circuit-tree.controller.ts @@ -160,6 +160,7 @@ export async function getCircuitTree(req: Request, res: Response) { roomNumberSnapshot: row.roomNumberSnapshot ?? undefined, roomNameSnapshot: row.roomNameSnapshot ?? undefined, quantity: row.quantity, + manualQuantity: row.manualQuantity, powerPerUnit: row.powerPerUnit, simultaneityFactor: row.simultaneityFactor, cosPhi: row.cosPhi ?? undefined, diff --git a/tests/circuit-device-row-project-command.repository.test.ts b/tests/circuit-device-row-project-command.repository.test.ts index 1bcf1b3..5ab1e3a 100644 --- a/tests/circuit-device-row-project-command.repository.test.ts +++ b/tests/circuit-device-row-project-command.repository.test.ts @@ -128,6 +128,7 @@ function createTestDatabase(): DatabaseContext { name: "Leuchte", displayName: "Leuchte lokal", quantity: 1, + manualQuantity: 1, powerPerUnit: 0.1, simultaneityFactor: 1, }, @@ -138,6 +139,7 @@ function createTestDatabase(): DatabaseContext { name: "Fremdzeile", displayName: "Fremdzeile", quantity: 1, + manualQuantity: 1, powerPerUnit: 0.2, simultaneityFactor: 1, }, @@ -175,6 +177,7 @@ describe("circuit device-row project-command repository", () => { const row = getRow(context); assert.equal(row.quantity, 2); + assert.equal(row.manualQuantity, 2); assert.equal(row.roomId, "room-1"); assert.equal(row.remark, null); assert.equal(row.overriddenFields, "[\"quantity\"]"); @@ -185,6 +188,7 @@ describe("circuit device-row project-command repository", () => { { field: "quantity", value: 1 }, { field: "roomId", value: null }, { field: "remark", value: null }, + { field: "manualQuantity", value: 1 }, { field: "overriddenFields", value: null }, ], }); @@ -203,6 +207,7 @@ describe("circuit device-row project-command repository", () => { { field: "quantity", value: 2 }, { field: "roomId", value: "room-1" }, { field: "remark", value: null }, + { field: "manualQuantity", value: 2 }, { field: "overriddenFields", value: "[\"quantity\"]" }, ], }, @@ -234,6 +239,7 @@ describe("circuit device-row project-command repository", () => { const row = getRow(context); assert.equal(row.quantity, 1); + assert.equal(row.manualQuantity, 1); assert.equal(row.overriddenFields, null); assert.equal(undone.revision.revisionNumber, 2); assert.deepEqual( @@ -377,4 +383,22 @@ describe("circuit device-row project-command repository", () => { 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(); + } + }); }); diff --git a/tests/circuit-device-row-quantity.test.ts b/tests/circuit-device-row-quantity.test.ts new file mode 100644 index 0000000..c725714 --- /dev/null +++ b/tests/circuit-device-row-quantity.test.ts @@ -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(); + } + }); +}); diff --git a/tests/external-model-foundation.test.ts b/tests/external-model-foundation.test.ts index 46eed56..08319ec 100644 --- a/tests/external-model-foundation.test.ts +++ b/tests/external-model-foundation.test.ts @@ -21,6 +21,7 @@ import { projectDevices } from "../src/db/schema/project-devices.js"; import { projects } from "../src/db/schema/projects.js"; import { rooms } from "../src/db/schema/rooms.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 { parseExternalCsv } from "../src/external-model/csv/external-csv-transport.js"; import { @@ -211,7 +212,7 @@ describe("external model snapshot and transfer", () => { insertLinkedExternalFixture(context.db); const populated = readProjectStateSnapshot(context.db, "project-1"); 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"); context.db.delete(externalModelSources).run(); diff --git a/tests/project-command.model.test.ts b/tests/project-command.model.test.ts index 9284f47..c13ed74 100644 --- a/tests/project-command.model.test.ts +++ b/tests/project-command.model.test.ts @@ -179,6 +179,7 @@ describe("circuit device-row update project commands", () => { { field: "roomId", value: null }, { field: "quantity", value: 2 }, { field: "cosPhi", value: 0.9 }, + { field: "manualQuantity", value: 2 }, ]); }); @@ -187,6 +188,13 @@ describe("circuit device-row update project commands", () => { () => createCircuitDeviceRowUpdateProjectCommand("row-1", {}), /at least one change/ ); + assert.throws( + () => createCircuitDeviceRowUpdateProjectCommand("row-1", { + quantity: 1, + manualQuantity: 2, + }), + /must not exceed/ + ); assert.throws( () => createCircuitDeviceRowUpdateProjectCommand("row-1", { @@ -470,7 +478,7 @@ describe("circuit device-row structure project commands", () => { row.circuitId ); - assert.deepEqual(insert.payload.row, row); + assert.deepEqual(insert.payload.row, { ...row, manualQuantity: 1 }); assert.deepEqual(remove.payload, { rowId: "row-1", expectedCircuitId: "circuit-1", diff --git a/tests/project-state-snapshot.test.ts b/tests/project-state-snapshot.test.ts index a786814..878eeca 100644 --- a/tests/project-state-snapshot.test.ts +++ b/tests/project-state-snapshot.test.ts @@ -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", () => { const outdated = structuredClone(minimalSnapshot()); delete (outdated.project as Record).description; From 1a7dd7fe0350d1f125fc1ac6f0ce7ec8854ce8e7 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 2 Aug 2026 18:38:38 +0200 Subject: [PATCH 45/59] Add external object row assignment command --- AGENTS.md | 10 +- docs/circuit-list-editor-known-limitations.md | 6 +- docs/current-architecture.md | 14 +- .../spec/revit-csv-phase-14-audit-and-plan.md | 15 +- ...w-assignment-project-command.repository.ts | 204 ++++++++++++++++ ...ct-row-assignment-project-command.model.ts | 186 ++++++++++++++ ...ct-row-assignment-project-command.store.ts | 19 ++ .../services/project-command.service.ts | 18 +- .../composition/project-command-stores.ts | 6 +- ...ignment-project-command.repository.test.ts | 227 ++++++++++++++++++ 10 files changed, 693 insertions(+), 12 deletions(-) create mode 100644 src/db/repositories/external-object-row-assignment-project-command.repository.ts create mode 100644 src/domain/models/external-object-row-assignment-project-command.model.ts create mode 100644 src/domain/ports/external-object-row-assignment-project-command.store.ts create mode 100644 tests/external-object-row-assignment-project-command.repository.test.ts diff --git a/AGENTS.md b/AGENTS.md index 054b9c9..4b13c4e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -287,9 +287,13 @@ version introduced by additive migration `0004`; it is distinct from the CSV 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. +Normal manual row inserts and quantity edits update both values together. +`external-object.update-row-assignment` atomically assigns external objects to +existing rows, moves them between existing rows or detaches them. It preserves +the manual quantity, validates the materialized total against every target +link, rejects project/board/category/room/selection-marker mixing and requires +explicitly listed confirmations for other planning-value conflicts. Its exact +inverse supports persistent Undo/Redo, and detach never deletes the row. Confirmed initial state is written only through `external-import.apply-initial`. The command rechecks configuration version, original-byte SHA-256, parsed matrix, complete IFCGUID/source values, explicit diff --git a/docs/circuit-list-editor-known-limitations.md b/docs/circuit-list-editor-known-limitations.md index 52949b1..0c0ec21 100644 --- a/docs/circuit-list-editor-known-limitations.md +++ b/docs/circuit-list-editor-known-limitations.md @@ -9,8 +9,10 @@ - Named and automatic logical project snapshots can be listed and restored through the project page with persistent Undo/Redo. Automatic snapshots are created every 25 revisions and only their newest 12 entries are retained. -- The Revit/CSV foundation and confirmed initial import are implemented. Row - assignment, follow-up import, conflict review and return export are pending. +- The Revit/CSV foundation and confirmed initial import are implemented. + Existing-row assignment, move and detach have an atomic persistent command; + their editor UI, new-row/new-circuit paths, follow-up import, conflict review + and return export are pending. - Persistence currently targets local SQLite; PostgreSQL is an architectural option, not an implemented runtime. - The global device library supports basic CRUD and copy operations, but has no versioning, permissions or controlled synchronization model. diff --git a/docs/current-architecture.md b/docs/current-architecture.md index f55baa7..86b11f4 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -540,6 +540,17 @@ weiterhin materialisierten Gesamtwert. Bestehende Daten werden mit Zuordnungs-Commands lautet `quantity = manualQuantity + Summe der effectiveQuantity aller verknüpften ExternalModelObjects`; Link und beide Mengenwerte müssen innerhalb desselben Projekt-Commands wechseln. +`external-object.update-row-assignment` ist diese atomare Schreibgrenze für +bestehende Gerätezeilen. Sie weist mehrere externe Einzelobjekte zu, verschiebt +sie zwischen vorhandenen Rows oder löst ihre Links. Vor dem Schreiben werden +die vollständigen erwarteten Row- und Objektsnapshots, Projektzugehörigkeit, +Verteilung, Gruppenkategorie, Raum und Selektionskennzeichen geprüft. +Abweichende lokale Planungswerte benötigen eine explizite Konfliktbestätigung; +unterschiedliche Räume, Kategorien oder Selektionskennzeichen werden nicht +still zusammengeführt. Alle betroffenen materialisierten Mengen werden aus dem +unveränderten manuellen Anteil und sämtlichen Ziel-Objektlinks validiert. Die +Gegenrichtung enthält dieselben vollständigen Snapshots und funktioniert nach +Neustart; beim Lösen des letzten Objekts bleibt die Row bestehen. `GET` und `PUT /api/projects/:projectId/external-csv/configuration` lesen oder ändern die Konfiguration; der PUT plant Identität und nächsten Konfigurationsstand serverseitig und verwendet den typisierten Command. @@ -587,7 +598,8 @@ ihn für die betroffene Gruppe. ## Noch nicht unterstützt - Mehrbenutzerbetrieb und Konfliktauflösung -- Folgeimport, Editor-Zuordnung und strukturtreuer Revit-Rückexport +- Editor-Oberfläche für die vorhandene Zuordnungslogik, Folgeimport und + strukturtreuer Revit-Rückexport - vollständige elektrische Dimensionierung - Produktionsdeployment diff --git a/docs/spec/revit-csv-phase-14-audit-and-plan.md b/docs/spec/revit-csv-phase-14-audit-and-plan.md index 7fb4f88..f193d29 100644 --- a/docs/spec/revit-csv-phase-14-audit-and-plan.md +++ b/docs/spec/revit-csv-phase-14-audit-and-plan.md @@ -202,15 +202,14 @@ bleiben und dürfen spätere Quellstände nicht erneut auswerten. oder mehrerer Objekte ändern. - `external-model-object.update-project-device-link`: ProjectDevice-Links explizit setzen oder lösen, ohne das ProjectDevice zu verändern. -- `external-model-object.assign-to-row`: Objekte einer bestehenden Row - zuweisen und deren materialisierte Menge atomar aktualisieren. +- `external-object.update-row-assignment`: Ein oder mehrere Objekte einer + bestehenden Row zuweisen, zwischen bestehenden Rows verschieben oder lösen + und alle betroffenen materialisierten Mengen atomar aktualisieren. - `external-model-object.assign-to-new-row`: stabile neue Row einfügen, Objekte verknüpfen und gegebenenfalls den Reserve-Status aktualisieren. - `external-model-object.assign-to-new-circuit`: stabilen Circuit samt Schutz, Row und Objektlinks in einem Schritt einfügen. Das geplante BMK wird im Command gespeichert und bei Redo nicht neu berechnet. -- `external-model-object.unassign-from-row`: einzelne Objektlinks lösen und - die Menge exakt aktualisieren. - `external-model-object.delete-missing`: nur ausdrücklich bestätigte, weiterhin unveränderte fehlende Objekte löschen. @@ -319,6 +318,14 @@ Kombination aus Originalbytes und Matrix als Entscheidungen bestätigt. 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. + **Teilweise erledigt:** `external-object.update-row-assignment` deckt + Zuweisung zu vorhandenen Rows, Verschieben zwischen vorhandenen Rows und + Lösen ab. Der Command prüft Projekt, Verteilung, Gruppenkategorie, Raum, + Selektionskennzeichen und Planungswerte, verlangt für bestätigbare + Planungsabweichungen eine explizite Objektliste und aktualisiert Links und + Gesamtmengen in einer SQLite-Transaktion. Seine exakte Gegenrichtung ist + dauerhaft Undo/Redo-fähig. Beim Lösen bleibt die Row mit ihrem manuellen + Mengenanteil bestehen. Neue Row und neuer Circuit folgen separat. 3. Verteilungsbezogenen Drawer, Filter und Vorschau der Mengenwirkung ergänzen. 4. Einzel- und Mehrfach-Drag-and-drop samt Warnungen, Undo/Redo und Reload testen. diff --git a/src/db/repositories/external-object-row-assignment-project-command.repository.ts b/src/db/repositories/external-object-row-assignment-project-command.repository.ts new file mode 100644 index 0000000..dcf33b1 --- /dev/null +++ b/src/db/repositories/external-object-row-assignment-project-command.repository.ts @@ -0,0 +1,204 @@ +import { and, eq, inArray } from "drizzle-orm"; +import { + assertCircuitDeviceRowQuantity, +} from "../../domain/calculations/circuit-device-row-quantity.js"; +import { + assertExternalObjectRowAssignmentProjectCommand, + invertExternalObjectRowAssignmentProjectCommand, +} from "../../domain/models/external-object-row-assignment-project-command.model.js"; +import type { CircuitDeviceRowSnapshot } from "../../domain/models/circuit-device-row-structure-project-command.model.js"; +import type { ExternalObjectRowAssignmentProjectCommandStore } from "../../domain/ports/external-object-row-assignment-project-command.store.js"; +import type { ExternalModelObjectSnapshot } from "../../external-model/domain/external-model-contracts.js"; +import type { AppDatabase } from "../database-context.js"; +import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; +import { circuitLists } from "../schema/circuit-lists.js"; +import { circuitSections } from "../schema/circuit-sections.js"; +import { circuits } from "../schema/circuits.js"; +import { externalModelObjects } from "../schema/external-model-objects.js"; +import { externalRoomMappings } from "../schema/external-room-mappings.js"; +import { toCircuitDeviceRowSnapshot } from "./circuit-device-row-structure.persistence.js"; +import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; + +export class ExternalObjectRowAssignmentProjectCommandRepository + implements ExternalObjectRowAssignmentProjectCommandStore +{ + constructor(private readonly database: AppDatabase) {} + + execute(input: Parameters[0]) { + assertExternalObjectRowAssignmentProjectCommand(input.command); + return executeProjectCommandTransaction(this.database, input, (tx) => { + this.apply(tx, input.projectId, input.command); + return invertExternalObjectRowAssignmentProjectCommand(input.command); + }); + } + + private apply( + database: AppDatabase, + projectId: string, + command: Parameters[0]["command"] + ) { + const rowTransitions = new Map( + command.payload.rows.map((transition) => [transition.expected.id, transition]) + ); + const objectTransitions = new Map( + command.payload.objects.map((transition) => [transition.expected.id, transition]) + ); + const rowIds = [...rowTransitions.keys()]; + const objectIds = [...objectTransitions.keys()]; + const currentRows = database.select().from(circuitDeviceRows) + .where(inArray(circuitDeviceRows.id, rowIds)).all(); + const currentObjects = database.select().from(externalModelObjects) + .where(inArray(externalModelObjects.id, objectIds)).all(); + if (currentRows.length !== rowIds.length || currentObjects.length !== objectIds.length) { + throw new Error("External object assignment target no longer exists."); + } + for (const row of currentRows) { + const expected = rowTransitions.get(row.id)!.expected; + if (!same(toCircuitDeviceRowSnapshot(row), expected)) { + throw new Error("Circuit device row changed before external object assignment."); + } + } + for (const object of currentObjects) { + const expected = objectTransitions.get(object.id)!.expected; + if (!same(toExternalObjectSnapshot(object), expected)) { + throw new Error("External object changed before row assignment."); + } + if (object.projectId !== projectId) { + throw new Error("External object belongs to another project."); + } + } + + const rowContexts = database + .select({ + rowId: circuitDeviceRows.id, + projectId: circuitLists.projectId, + distributionBoardId: circuitLists.distributionBoardId, + category: circuitSections.category, + }) + .from(circuitDeviceRows) + .innerJoin(circuits, eq(circuits.id, circuitDeviceRows.circuitId)) + .innerJoin(circuitLists, eq(circuitLists.id, circuits.circuitListId)) + .innerJoin(circuitSections, eq(circuitSections.id, circuits.sectionId)) + .where(inArray(circuitDeviceRows.id, rowIds)) + .all(); + if ( + rowContexts.length !== rowIds.length || + rowContexts.some((context) => context.projectId !== projectId) + ) { + throw new Error("Circuit device row belongs to another project."); + } + const contextByRowId = new Map(rowContexts.map((context) => [context.rowId, context])); + + const allProjectObjects = database.select().from(externalModelObjects) + .where(eq(externalModelObjects.projectId, projectId)).all() + .map(toExternalObjectSnapshot); + const targetObjects = allProjectObjects.map((object) => + objectTransitions.get(object.id)?.target ?? object + ); + const mappings = database.select().from(externalRoomMappings) + .where(eq(externalRoomMappings.projectId, projectId)).all(); + const roomIdByMappingId = new Map(mappings.map((mapping) => [mapping.id, mapping.roomId])); + const confirmedConflicts = new Set(command.payload.confirmedConflictObjectIds); + + for (const transition of command.payload.objects) { + const object = transition.target; + if (object.circuitDeviceRowId === null) continue; + const row = rowTransitions.get(object.circuitDeviceRowId)?.target; + const context = contextByRowId.get(object.circuitDeviceRowId); + if (!row || !context) throw new Error("External object target row is incomplete."); + if ( + object.distributionBoardId === null || + object.distributionBoardId !== context.distributionBoardId + ) { + throw new Error("External object distribution does not match target row."); + } + if (object.planningValues.category !== context.category) { + throw new Error("External object category does not match target circuit group."); + } + const objectRoomId = object.externalRoomMappingId === null + ? null + : roomIdByMappingId.get(object.externalRoomMappingId) ?? null; + if (objectRoomId !== row.roomId) { + throw new Error("External object room does not match target row."); + } + if (!matchesRowPlanningValues(row, object) && !confirmedConflicts.has(object.id)) { + throw new Error("External object planning values require explicit conflict confirmation."); + } + } + + for (const transition of command.payload.rows) { + const linkedObjects = targetObjects.filter( + (object) => object.circuitDeviceRowId === transition.target.id + ); + const selectionMarkers = new Set( + linkedObjects.map((object) => object.acceptedSourceValues.selectionMarker.trim()) + ); + if (selectionMarkers.size > 1) { + throw new Error("External objects with different selection markers require separate rows."); + } + assertCircuitDeviceRowQuantity({ + quantity: transition.target.quantity, + manualQuantity: transition.target.manualQuantity ?? transition.target.quantity, + externalObjects: linkedObjects.map((object) => ({ + effectiveQuantity: object.planningValues.effectiveQuantity, + })), + }); + } + + for (const transition of command.payload.rows) { + const result = database.update(circuitDeviceRows) + .set({ quantity: transition.target.quantity }) + .where(and( + eq(circuitDeviceRows.id, transition.expected.id), + eq(circuitDeviceRows.quantity, transition.expected.quantity), + eq(circuitDeviceRows.manualQuantity, transition.expected.manualQuantity ?? transition.expected.quantity) + )).run(); + if (result.changes !== 1) throw new Error("Circuit device row changed during assignment."); + } + for (const transition of command.payload.objects) { + const result = database.update(externalModelObjects) + .set({ circuitDeviceRowId: transition.target.circuitDeviceRowId }) + .where(eq(externalModelObjects.id, transition.expected.id)).run(); + if (result.changes !== 1) throw new Error("External object changed during assignment."); + } + } +} + +function matchesRowPlanningValues( + row: CircuitDeviceRowSnapshot, + object: ExternalModelObjectSnapshot +) { + const planning = object.planningValues; + return ( + row.displayName === (planning.displayName ?? row.displayName) && + row.linkedProjectDeviceId === object.linkedProjectDeviceId && + row.category === planning.category && + row.connectionKind === planning.connectionKind && + (planning.powerPerUnitW === null || row.powerPerUnit === planning.powerPerUnitW / 1000) && + row.simultaneityFactor === planning.simultaneityFactor && + row.cosPhi === planning.cosPhi && + row.costGroup === planning.costGroup && + row.remark === planning.remark + ); +} + +function toExternalObjectSnapshot( + object: typeof externalModelObjects.$inferSelect +): ExternalModelObjectSnapshot { + return { ...object }; +} + +function same(left: unknown, right: unknown) { + return canonicalJson(left) === canonicalJson(right); +} + +function canonicalJson(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; + if (value !== null && typeof value === "object") { + const record = value as Record; + return `{${Object.keys(record).sort().map((key) => + `${JSON.stringify(key)}:${canonicalJson(record[key])}` + ).join(",")}}`; + } + return JSON.stringify(value); +} diff --git a/src/domain/models/external-object-row-assignment-project-command.model.ts b/src/domain/models/external-object-row-assignment-project-command.model.ts new file mode 100644 index 0000000..a45fe31 --- /dev/null +++ b/src/domain/models/external-object-row-assignment-project-command.model.ts @@ -0,0 +1,186 @@ +import type { ExternalModelObjectSnapshot } from "../../external-model/domain/external-model-contracts.js"; +import { + assertCircuitDeviceRowInsertProjectCommand, + circuitDeviceRowInsertCommandType, + circuitDeviceRowStructureCommandSchemaVersion, + type CircuitDeviceRowSnapshot, +} from "./circuit-device-row-structure-project-command.model.js"; +import type { SerializedProjectCommand } from "./project-command.model.js"; +import { parseExternalModelStateSnapshot } from "./project-state-snapshot.model.js"; + +export const externalObjectRowAssignmentCommandType = + "external-object.update-row-assignment" as const; +export const externalObjectRowAssignmentCommandSchemaVersion = 1 as const; + +export interface ExternalObjectRowAssignmentPayload { + rows: Array<{ expected: CircuitDeviceRowSnapshot; target: CircuitDeviceRowSnapshot }>; + objects: Array<{ expected: ExternalModelObjectSnapshot; target: ExternalModelObjectSnapshot }>; + confirmedConflictObjectIds: string[]; +} + +export interface ExternalObjectRowAssignmentProjectCommand + extends SerializedProjectCommand { + schemaVersion: typeof externalObjectRowAssignmentCommandSchemaVersion; + type: typeof externalObjectRowAssignmentCommandType; +} + +export function createExternalObjectRowAssignmentProjectCommand( + payload: ExternalObjectRowAssignmentPayload +): ExternalObjectRowAssignmentProjectCommand { + const normalizedPayload: ExternalObjectRowAssignmentPayload = { + ...payload, + rows: payload.rows.map(({ expected, target }) => ({ + expected: normalizeRow(expected), + target: normalizeRow(target), + })), + confirmedConflictObjectIds: [...payload.confirmedConflictObjectIds], + }; + const command: ExternalObjectRowAssignmentProjectCommand = { + schemaVersion: externalObjectRowAssignmentCommandSchemaVersion, + type: externalObjectRowAssignmentCommandType, + payload: normalizedPayload, + }; + assertExternalObjectRowAssignmentProjectCommand(command); + return command; +} + +export function invertExternalObjectRowAssignmentProjectCommand( + command: ExternalObjectRowAssignmentProjectCommand +) { + return createExternalObjectRowAssignmentProjectCommand({ + rows: command.payload.rows.map(({ expected, target }) => ({ expected: target, target: expected })), + objects: command.payload.objects.map(({ expected, target }) => ({ expected: target, target: expected })), + confirmedConflictObjectIds: [...command.payload.confirmedConflictObjectIds], + }); +} + +export function assertExternalObjectRowAssignmentProjectCommand( + command: SerializedProjectCommand +): asserts command is ExternalObjectRowAssignmentProjectCommand { + if ( + command.schemaVersion !== externalObjectRowAssignmentCommandSchemaVersion || + command.type !== externalObjectRowAssignmentCommandType || + !isRecord(command.payload) || + !Array.isArray(command.payload.rows) || + !Array.isArray(command.payload.objects) || + !Array.isArray(command.payload.confirmedConflictObjectIds) || + command.payload.rows.length === 0 || + command.payload.objects.length === 0 + ) { + throw new Error("Unsupported external object row-assignment command."); + } + const rowIds = new Set(); + for (const transition of command.payload.rows) { + if (!isRecord(transition)) throw new Error("Invalid row assignment transition."); + const expected = parseRow(transition.expected); + const target = parseRow(transition.target); + if (expected.manualQuantity === undefined || target.manualQuantity === undefined) { + throw new Error("Row assignment requires an explicit manual quantity."); + } + if (expected.id !== target.id || rowIds.has(expected.id)) { + throw new Error("Row assignment contains mismatched or duplicate rows."); + } + rowIds.add(expected.id); + if (expected.manualQuantity !== target.manualQuantity) { + throw new Error("Row assignment must preserve the manual quantity."); + } + assertOnlyFieldChanged( + expected as unknown as Record, + target as unknown as Record, + "quantity", + "row" + ); + } + const objectIds = new Set(); + const affectedRowIds = new Set(); + for (const transition of command.payload.objects) { + if (!isRecord(transition)) throw new Error("Invalid object assignment transition."); + const expected = parseObject(transition.expected); + const target = parseObject(transition.target); + if (expected.id !== target.id || objectIds.has(expected.id)) { + throw new Error("Row assignment contains mismatched or duplicate objects."); + } + objectIds.add(expected.id); + if (expected.circuitDeviceRowId === target.circuitDeviceRowId) { + throw new Error("External object row assignment must change its row link."); + } + assertOnlyFieldChanged( + expected as unknown as Record, + target as unknown as Record, + "circuitDeviceRowId", + "external object" + ); + if (expected.circuitDeviceRowId) affectedRowIds.add(expected.circuitDeviceRowId); + if (target.circuitDeviceRowId) affectedRowIds.add(target.circuitDeviceRowId); + } + const confirmedIds = new Set(); + for (const objectId of command.payload.confirmedConflictObjectIds) { + if ( + typeof objectId !== "string" || + !objectIds.has(objectId) || + confirmedIds.has(objectId) + ) { + throw new Error("Row assignment contains an invalid conflict confirmation."); + } + confirmedIds.add(objectId); + } + if ( + affectedRowIds.size !== rowIds.size || + [...affectedRowIds].some((rowId) => !rowIds.has(rowId)) + ) { + throw new Error("Row assignment must include every and only affected device row."); + } +} + +function normalizeRow(row: CircuitDeviceRowSnapshot): CircuitDeviceRowSnapshot { + return { ...row, manualQuantity: row.manualQuantity ?? row.quantity }; +} + +function parseRow(value: unknown): CircuitDeviceRowSnapshot { + const envelope = { + schemaVersion: circuitDeviceRowStructureCommandSchemaVersion, + type: circuitDeviceRowInsertCommandType, + payload: { row: value }, + }; + assertCircuitDeviceRowInsertProjectCommand(envelope); + return envelope.payload.row; +} + +function parseObject(value: unknown): ExternalModelObjectSnapshot { + const state = parseExternalModelStateSnapshot({ + source: null, + importBatches: [], + roomMappings: [], + objects: [value], + }); + return state.objects[0]!; +} + +function assertOnlyFieldChanged( + expected: Record, + target: Record, + allowedField: string, + label: string +) { + const expectedRest = { ...expected }; + const targetRest = { ...target }; + delete expectedRest[allowedField]; + delete targetRest[allowedField]; + if (canonicalJson(expectedRest) !== canonicalJson(targetRest)) { + throw new Error(`Row assignment may only change the ${label} assignment state.`); + } +} + +function canonicalJson(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; + if (isRecord(value)) { + return `{${Object.keys(value).sort().map((key) => + `${JSON.stringify(key)}:${canonicalJson(value[key])}` + ).join(",")}}`; + } + return JSON.stringify(value); +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} diff --git a/src/domain/ports/external-object-row-assignment-project-command.store.ts b/src/domain/ports/external-object-row-assignment-project-command.store.ts new file mode 100644 index 0000000..98f2fca --- /dev/null +++ b/src/domain/ports/external-object-row-assignment-project-command.store.ts @@ -0,0 +1,19 @@ +import type { ExternalObjectRowAssignmentProjectCommand } from "../models/external-object-row-assignment-project-command.model.js"; +import type { AppendedProjectRevision, ProjectRevisionSource } from "./project-revision.store.js"; + +export interface ExecuteExternalObjectRowAssignmentCommandInput { + projectId: string; + expectedRevision: number; + source: ProjectRevisionSource; + description?: string; + actorId?: string; + historyTargetChangeSetId?: string; + command: ExternalObjectRowAssignmentProjectCommand; +} + +export interface ExternalObjectRowAssignmentProjectCommandStore { + execute(input: ExecuteExternalObjectRowAssignmentCommandInput): { + revision: AppendedProjectRevision; + inverse: ExternalObjectRowAssignmentProjectCommand; + }; +} diff --git a/src/domain/services/project-command.service.ts b/src/domain/services/project-command.service.ts index ef6a85a..2a90470 100644 --- a/src/domain/services/project-command.service.ts +++ b/src/domain/services/project-command.service.ts @@ -171,6 +171,11 @@ import { externalInitialImportCommandType, } from "../models/external-initial-import-project-command.model.js"; import type { ExternalInitialImportProjectCommandStore } from "../ports/external-initial-import-project-command.store.js"; +import { + assertExternalObjectRowAssignmentProjectCommand, + externalObjectRowAssignmentCommandType, +} from "../models/external-object-row-assignment-project-command.model.js"; +import type { ExternalObjectRowAssignmentProjectCommandStore } from "../ports/external-object-row-assignment-project-command.store.js"; interface DispatchProjectCommandInput { projectId: string; @@ -207,7 +212,8 @@ export class ProjectCommandService implements ProjectCommandExecutor { private readonly circuitProtectionStore: CircuitProtectionProjectCommandStore, private readonly historyStore: ProjectHistoryStore, private readonly externalCsvConfigurationStore?: ExternalCsvConfigurationProjectCommandStore, - private readonly externalInitialImportStore?: ExternalInitialImportProjectCommandStore + private readonly externalInitialImportStore?: ExternalInitialImportProjectCommandStore, + private readonly externalObjectRowAssignmentStore?: ExternalObjectRowAssignmentProjectCommandStore ) {} executeUser( @@ -584,6 +590,16 @@ export class ProjectCommandService implements ProjectCommandExecutor { command: input.command, }).revision; } + case externalObjectRowAssignmentCommandType: { + assertExternalObjectRowAssignmentProjectCommand(input.command); + if (!this.externalObjectRowAssignmentStore) { + throw new Error("External object row-assignment store is not available."); + } + return this.externalObjectRowAssignmentStore.execute({ + ...input, + command: input.command, + }).revision; + } case projectStateRestoreCommandType: { assertProjectStateRestoreCommand(input.command); return this.projectStateRestoreStore.execute({ diff --git a/src/server/composition/project-command-stores.ts b/src/server/composition/project-command-stores.ts index 129ea1c..9b6d539 100644 --- a/src/server/composition/project-command-stores.ts +++ b/src/server/composition/project-command-stores.ts @@ -24,6 +24,7 @@ import { ProjectStateRestoreCommandRepository } from "../../db/repositories/proj import { ProjectCommandService } from "../../domain/services/project-command.service.js"; import { ExternalCsvConfigurationProjectCommandRepository } from "../../db/repositories/external-csv-configuration-project-command.repository.js"; import { ExternalInitialImportProjectCommandRepository } from "../../db/repositories/external-initial-import-project-command.repository.js"; +import { ExternalObjectRowAssignmentProjectCommandRepository } from "../../db/repositories/external-object-row-assignment-project-command.repository.js"; export const circuitProjectCommandStore = new CircuitProjectCommandRepository(db); export const circuitDeviceRowProjectCommandStore = @@ -71,6 +72,8 @@ export const externalCsvConfigurationProjectCommandStore = new ExternalCsvConfigurationProjectCommandRepository(db); export const externalInitialImportProjectCommandStore = new ExternalInitialImportProjectCommandRepository(db); +export const externalObjectRowAssignmentProjectCommandStore = + new ExternalObjectRowAssignmentProjectCommandRepository(db); export const projectCommandService = new ProjectCommandService( circuitProjectCommandStore, circuitDeviceRowProjectCommandStore, @@ -95,5 +98,6 @@ export const projectCommandService = new ProjectCommandService( circuitProtectionProjectCommandStore, projectHistoryStore, externalCsvConfigurationProjectCommandStore, - externalInitialImportProjectCommandStore + externalInitialImportProjectCommandStore, + externalObjectRowAssignmentProjectCommandStore ); diff --git a/tests/external-object-row-assignment-project-command.repository.test.ts b/tests/external-object-row-assignment-project-command.repository.test.ts new file mode 100644 index 0000000..5585204 --- /dev/null +++ b/tests/external-object-row-assignment-project-command.repository.test.ts @@ -0,0 +1,227 @@ +import path from "node:path"; +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { eq } from "drizzle-orm"; +import { migrate } from "drizzle-orm/better-sqlite3/migrator"; +import { createDatabaseContext, type AppDatabase } from "../src/db/database-context.js"; +import { ExternalObjectRowAssignmentProjectCommandRepository } from "../src/db/repositories/external-object-row-assignment-project-command.repository.js"; +import { toCircuitDeviceRowSnapshot } from "../src/db/repositories/circuit-device-row-structure.persistence.js"; +import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js"; +import { circuitLists } from "../src/db/schema/circuit-lists.js"; +import { circuitSections } from "../src/db/schema/circuit-sections.js"; +import { circuits } from "../src/db/schema/circuits.js"; +import { distributionBoards } from "../src/db/schema/distribution-boards.js"; +import { externalImportBatches } from "../src/db/schema/external-import-batches.js"; +import { externalModelObjects } from "../src/db/schema/external-model-objects.js"; +import { externalModelSources } from "../src/db/schema/external-model-sources.js"; +import { externalRoomMappings } from "../src/db/schema/external-room-mappings.js"; +import { floors } from "../src/db/schema/floors.js"; +import { projects } from "../src/db/schema/projects.js"; +import { rooms } from "../src/db/schema/rooms.js"; +import { createExternalObjectRowAssignmentProjectCommand } from "../src/domain/models/external-object-row-assignment-project-command.model.js"; +import type { ExternalModelObjectSnapshot } from "../src/external-model/domain/external-model-contracts.js"; +import { externalCsvTestConfiguration } from "./fixtures/revit-csv-fixtures.js"; + +function createFixture() { + const context = createDatabaseContext(":memory:"); + migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations") }); + const database = context.db; + database.insert(projects).values({ id: "project-1", name: "Projekt" }).run(); + database.insert(floors).values({ id: "floor-1", projectId: "project-1", name: "EG", sortOrder: 10 }).run(); + database.insert(rooms).values({ id: "room-1", projectId: "project-1", floorId: "floor-1", roomNumber: "101", roomName: "Büro" }).run(); + database.insert(distributionBoards).values({ id: "board-1", projectId: "project-1", name: "UV 1" }).run(); + database.insert(circuitLists).values({ id: "list-1", projectId: "project-1", distributionBoardId: "board-1", name: "Liste" }).run(); + database.insert(circuitSections).values({ id: "section-1", circuitListId: "list-1", key: "single-1", displayName: "1-phasig", prefix: "-2F1.", sortOrder: 10, category: "single_phase", groupNumber: 1 }).run(); + for (const index of [1, 2]) { + database.insert(circuits).values({ id: `circuit-${index}`, circuitListId: "list-1", sectionId: "section-1", equipmentIdentifier: `-2F1.${index}`, sortOrder: index * 10, voltage: 230 }).run(); + database.insert(circuitDeviceRows).values({ + id: `row-${index}`, + circuitId: `circuit-${index}`, + sortOrder: 10, + name: "socket", + displayName: "Steckdose", + phaseType: "single_phase", + connectionKind: "socket", + category: "single_phase", + roomId: "room-1", + roomNumberSnapshot: "101", + roomNameSnapshot: "Büro", + quantity: index === 1 ? 3 : 0, + manualQuantity: index === 1 ? 1 : 0, + powerPerUnit: 0.12, + simultaneityFactor: 1, + }).run(); + } + database.insert(externalModelSources).values({ id: "source-1", projectId: "project-1", name: "Revit", sourceType: "revit_csv" }).run(); + database.insert(externalImportBatches).values({ + id: "batch-1", + projectId: "project-1", + sourceId: "source-1", + importKind: "initial", + importedAtIso: "2026-08-02T16:00:00.000Z", + fileName: "revit.csv", + sha256: "a".repeat(64), + appliedProjectRevision: 0, + configurationVersion: 1, + configurationSnapshot: externalCsvTestConfiguration, + originalBytes: Buffer.from("test"), + document: { delimiter: ";", encoding: "utf-8", headers: [], rows: [] }, + }).run(); + database.insert(externalRoomMappings).values({ + id: "mapping-1", + projectId: "project-1", + sourceId: "source-1", + normalizedSourceRoomKey: "number:101", + sourceFloorName: "EG", + sourceRoomNumber: "101", + sourceRoomName: "Büro", + roomId: "room-1", + defaultDistributionBoardId: "board-1", + }).run(); + database.insert(externalModelObjects).values(objectSnapshot("row-1")).run(); + return context; +} + +function objectSnapshot(circuitDeviceRowId: string | null): ExternalModelObjectSnapshot { + return { + id: "object-1", + projectId: "project-1", + sourceId: "source-1", + ifcGuid: "ifc-1", + lastSeenImportBatchId: "batch-1", + lastAcceptedImportBatchId: "batch-1", + acceptedSourceValues: { + rowNumber: 2, + roomNumber: "101", + roomName: "Büro", + familyAndType: "Steckdose: Standard", + selectionMarker: "Steckdose", + circuitIdentifier: "-2F1.1", + power: "120", + quantity: "2", + additionalSourceValues: {}, + }, + planningValues: { + displayName: "Steckdose", + internalDeviceType: "socket", + category: "single_phase", + connectionKind: "socket", + effectiveQuantity: 2, + powerPerUnitW: 120, + simultaneityFactor: 1, + cosPhi: null, + costGroup: null, + remark: null, + }, + overriddenFields: [], + externalRoomMappingId: "mapping-1", + distributionBoardId: "board-1", + linkedProjectDeviceId: null, + circuitDeviceRowId, + presenceStatus: "present", + }; +} + +function row(database: AppDatabase, id: string) { + return toCircuitDeviceRowSnapshot( + database.select().from(circuitDeviceRows).where(eq(circuitDeviceRows.id, id)).get()! + ); +} + +function moveCommand(database: AppDatabase, confirmedConflictObjectIds: string[] = []) { + const row1 = row(database, "row-1"); + const row2 = row(database, "row-2"); + const object = objectSnapshot("row-1"); + return createExternalObjectRowAssignmentProjectCommand({ + rows: [ + { expected: row1, target: { ...row1, quantity: 1 } }, + { expected: row2, target: { ...row2, quantity: 2 } }, + ], + objects: [{ expected: object, target: { ...object, circuitDeviceRowId: "row-2" } }], + confirmedConflictObjectIds, + }); +} + +describe("external object row assignment project command", () => { + it("moves an object atomically and preserves exact quantities through undo and redo", () => { + const context = createFixture(); + try { + const repository = new ExternalObjectRowAssignmentProjectCommandRepository(context.db); + const moved = repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: moveCommand(context.db) }); + assert.equal(row(context.db, "row-1").quantity, 1); + assert.equal(row(context.db, "row-2").quantity, 2); + assert.equal(context.db.select().from(externalModelObjects).get()!.circuitDeviceRowId, "row-2"); + + const undone = repository.execute({ projectId: "project-1", expectedRevision: 1, source: "undo", historyTargetChangeSetId: moved.revision.changeSetId, command: moved.inverse }); + assert.equal(row(context.db, "row-1").quantity, 3); + assert.equal(row(context.db, "row-2").quantity, 0); + assert.equal(context.db.select().from(externalModelObjects).get()!.circuitDeviceRowId, "row-1"); + + repository.execute({ projectId: "project-1", expectedRevision: 2, source: "redo", historyTargetChangeSetId: moved.revision.changeSetId, command: undone.inverse }); + assert.equal(context.db.select().from(externalModelObjects).get()!.circuitDeviceRowId, "row-2"); + } finally { + context.close(); + } + }); + + it("detaches the last object but keeps the now-manual row", () => { + const context = createFixture(); + try { + const currentRow = row(context.db, "row-1"); + const object = objectSnapshot("row-1"); + const command = createExternalObjectRowAssignmentProjectCommand({ + rows: [{ expected: currentRow, target: { ...currentRow, quantity: 1 } }], + objects: [{ expected: object, target: { ...object, circuitDeviceRowId: null } }], + confirmedConflictObjectIds: [], + }); + new ExternalObjectRowAssignmentProjectCommandRepository(context.db).execute({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + command, + }); + assert.equal(row(context.db, "row-1").quantity, 1); + assert.equal(row(context.db, "row-1").manualQuantity, 1); + assert.equal(context.db.select().from(externalModelObjects).get()!.circuitDeviceRowId, null); + } finally { + context.close(); + } + }); + + it("requires explicit confirmation for differing planning values", () => { + const context = createFixture(); + try { + context.db.update(circuitDeviceRows).set({ displayName: "Abweichend" }).where(eq(circuitDeviceRows.id, "row-2")).run(); + const repository = new ExternalObjectRowAssignmentProjectCommandRepository(context.db); + assert.throws( + () => repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: moveCommand(context.db) }), + /explicit conflict confirmation/ + ); + repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: moveCommand(context.db, ["object-1"]) }); + assert.equal(context.db.select().from(externalModelObjects).get()!.circuitDeviceRowId, "row-2"); + } finally { + context.close(); + } + }); + + it("rolls back row quantities and links when history persistence fails", () => { + const context = createFixture(); + try { + context.sqlite.exec(` + CREATE TRIGGER fail_assignment_history + BEFORE INSERT ON project_change_sets + BEGIN SELECT RAISE(ABORT, 'late history failure'); END; + `); + const repository = new ExternalObjectRowAssignmentProjectCommandRepository(context.db); + assert.throws( + () => repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: moveCommand(context.db) }), + /late history failure/ + ); + assert.equal(row(context.db, "row-1").quantity, 3); + assert.equal(row(context.db, "row-2").quantity, 0); + assert.equal(context.db.select().from(externalModelObjects).get()!.circuitDeviceRowId, "row-1"); + } finally { + context.close(); + } + }); +}); From 347b3717c31b8360d02945b2af0f1fd75bbf57c9 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 2 Aug 2026 18:44:46 +0200 Subject: [PATCH 46/59] Add external object new row command --- AGENTS.md | 5 + docs/circuit-list-editor-known-limitations.md | 7 +- docs/current-architecture.md | 8 + .../spec/revit-csv-phase-14-audit-and-plan.md | 7 +- ...ject-new-row-project-command.repository.ts | 266 ++++++++++++++++++ ...al-object-new-row-project-command.model.ts | 163 +++++++++++ ...al-object-new-row-project-command.store.ts | 19 ++ .../services/project-command.service.ts | 20 +- .../composition/project-command-stores.ts | 6 +- ...ignment-project-command.repository.test.ts | 157 ++++++++++- 10 files changed, 649 insertions(+), 9 deletions(-) create mode 100644 src/db/repositories/external-object-new-row-project-command.repository.ts create mode 100644 src/domain/models/external-object-new-row-project-command.model.ts create mode 100644 src/domain/ports/external-object-new-row-project-command.store.ts diff --git a/AGENTS.md b/AGENTS.md index 4b13c4e..6332166 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -294,6 +294,11 @@ the manual quantity, validates the materialized total against every target link, rejects project/board/category/room/selection-marker mixing and requires explicitly listed confirmations for other planning-value conflicts. Its exact inverse supports persistent Undo/Redo, and detach never deletes the row. +`external-object.assign-to-new-row` atomically inserts unassigned external +objects as one external-only row (`manualQuantity = 0`) into an existing +circuit. Its history-only inverse removes the row only while its complete +snapshot and exact object-link set remain unchanged. Ordinary detach continues +to preserve the row. Confirmed initial state is written only through `external-import.apply-initial`. The command rechecks configuration version, original-byte SHA-256, parsed matrix, complete IFCGUID/source values, explicit diff --git a/docs/circuit-list-editor-known-limitations.md b/docs/circuit-list-editor-known-limitations.md index 0c0ec21..0a7b076 100644 --- a/docs/circuit-list-editor-known-limitations.md +++ b/docs/circuit-list-editor-known-limitations.md @@ -10,9 +10,10 @@ through the project page with persistent Undo/Redo. Automatic snapshots are created every 25 revisions and only their newest 12 entries are retained. - The Revit/CSV foundation and confirmed initial import are implemented. - Existing-row assignment, move and detach have an atomic persistent command; - their editor UI, new-row/new-circuit paths, follow-up import, conflict review - and return export are pending. + Existing-row assignment, move and detach and creation of an external-only row + in an existing circuit have atomic persistent commands. Their editor UI, the + new-circuit path, follow-up import, conflict review and return export are + pending. - Persistence currently targets local SQLite; PostgreSQL is an architectural option, not an implemented runtime. - The global device library supports basic CRUD and copy operations, but has no versioning, permissions or controlled synchronization model. diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 86b11f4..fb4f2ee 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -551,6 +551,14 @@ still zusammengeführt. Alle betroffenen materialisierten Mengen werden aus dem unveränderten manuellen Anteil und sämtlichen Ziel-Objektlinks validiert. Die Gegenrichtung enthält dieselben vollständigen Snapshots und funktioniert nach Neustart; beim Lösen des letzten Objekts bleibt die Row bestehen. +`external-object.assign-to-new-row` fügt unzugeordnete Objekte atomar als neue +Gerätezeile in einen vorhandenen Stromkreis ein. Der manuelle Anteil dieser Row +ist null; Gesamtmenge, Raum, Verteilung, Kategorie, Selektionskennzeichen und +Planungswerte werden vor dem Schreiben geprüft. Undo löst exakt diese Objekte +und löscht die erzeugte Row nur, wenn sie und ihre Linkmenge unverändert sind. +Der Löschbefehl ist ausschließlich für die Projekthistorie zugelassen; eine +normale spätere Trennung lässt die Row gemäß der allgemeinen Zuordnungsregel +bestehen. `GET` und `PUT /api/projects/:projectId/external-csv/configuration` lesen oder ändern die Konfiguration; der PUT plant Identität und nächsten Konfigurationsstand serverseitig und verwendet den typisierten Command. diff --git a/docs/spec/revit-csv-phase-14-audit-and-plan.md b/docs/spec/revit-csv-phase-14-audit-and-plan.md index f193d29..ffc1b45 100644 --- a/docs/spec/revit-csv-phase-14-audit-and-plan.md +++ b/docs/spec/revit-csv-phase-14-audit-and-plan.md @@ -325,7 +325,12 @@ Kombination aus Originalbytes und Matrix als Entscheidungen bestätigt. Planungsabweichungen eine explizite Objektliste und aktualisiert Links und Gesamtmengen in einer SQLite-Transaktion. Seine exakte Gegenrichtung ist dauerhaft Undo/Redo-fähig. Beim Lösen bleibt die Row mit ihrem manuellen - Mengenanteil bestehen. Neue Row und neuer Circuit folgen separat. + Mengenanteil bestehen. `external-object.assign-to-new-row` ergänzt die + Zuordnung ungebundener Objekte als neue Row in einem vorhandenen Circuit. + Diese Row erhält eine stabile ID und `manualQuantity = 0`; ihr historischer + Gegenbefehl entfernt sie nur, solange Row und Objektlinks vollständig + unverändert sind. Ein direktes Löschen über diesen internen Gegenbefehl ist + als Nutzeraktion gesperrt. Der neue Circuit folgt separat. 3. Verteilungsbezogenen Drawer, Filter und Vorschau der Mengenwirkung ergänzen. 4. Einzel- und Mehrfach-Drag-and-drop samt Warnungen, Undo/Redo und Reload testen. diff --git a/src/db/repositories/external-object-new-row-project-command.repository.ts b/src/db/repositories/external-object-new-row-project-command.repository.ts new file mode 100644 index 0000000..d2db69c --- /dev/null +++ b/src/db/repositories/external-object-new-row-project-command.repository.ts @@ -0,0 +1,266 @@ +import { and, eq, inArray, isNull } from "drizzle-orm"; +import { assertCircuitDeviceRowQuantity } from "../../domain/calculations/circuit-device-row-quantity.js"; +import { + assertExternalObjectNewRowProjectCommand, + externalObjectAssignToNewRowCommandType, + externalObjectDeleteCreatedRowCommandType, + invertExternalObjectNewRowProjectCommand, + type ExternalObjectNewRowProjectCommand, +} from "../../domain/models/external-object-new-row-project-command.model.js"; +import type { CircuitDeviceRowSnapshot } from "../../domain/models/circuit-device-row-structure-project-command.model.js"; +import type { ExternalObjectNewRowProjectCommandStore } from "../../domain/ports/external-object-new-row-project-command.store.js"; +import { isElectricalPhaseType } from "../../domain/services/project-voltage.service.js"; +import type { ExternalModelObjectSnapshot } from "../../external-model/domain/external-model-contracts.js"; +import type { AppDatabase } from "../database-context.js"; +import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; +import { circuitLists } from "../schema/circuit-lists.js"; +import { circuitSections } from "../schema/circuit-sections.js"; +import { circuits } from "../schema/circuits.js"; +import { externalModelObjects } from "../schema/external-model-objects.js"; +import { externalRoomMappings } from "../schema/external-room-mappings.js"; +import { + assertCircuitDeviceRowReferencesInProject, + toCircuitDeviceRowInsertValues, + toCircuitDeviceRowSnapshot, +} from "./circuit-device-row-structure.persistence.js"; +import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js"; + +export class ExternalObjectNewRowProjectCommandRepository + implements ExternalObjectNewRowProjectCommandStore +{ + constructor(private readonly database: AppDatabase) {} + + execute(input: Parameters[0]) { + assertExternalObjectNewRowProjectCommand(input.command); + if ( + input.source === "user" && + input.command.type === externalObjectDeleteCreatedRowCommandType + ) { + throw new Error("Created external rows may only be removed through project history."); + } + return executeProjectCommandTransaction(this.database, input, (tx) => { + if (input.command.type === externalObjectAssignToNewRowCommandType) { + this.insert(tx, input.projectId, input.command); + } else { + this.remove(tx, input.projectId, input.command); + } + return invertExternalObjectNewRowProjectCommand(input.command); + }); + } + + private insert( + database: AppDatabase, + projectId: string, + command: ExternalObjectNewRowProjectCommand + ) { + const row = command.payload.row; + const context = this.loadCircuitContext(database, projectId, row.circuitId); + if (!isElectricalPhaseType(row.phaseType)) { + throw new Error("External device-row phase type is invalid."); + } + if (row.category !== context.category) { + throw new Error("External device-row category does not match target circuit group."); + } + assertCircuitDeviceRowReferencesInProject(database, projectId, row); + if (database.select({ id: circuitDeviceRows.id }).from(circuitDeviceRows) + .where(eq(circuitDeviceRows.id, row.id)).get()) { + throw new Error("External device-row id already exists."); + } + const objects = this.loadExpectedObjects(database, projectId, command); + this.assertTargetCompatibility( + database, + row, + context.distributionBoardId, + context.category, + objects.map((object) => object.target), + new Set(command.payload.confirmedConflictObjectIds) + ); + assertCircuitDeviceRowQuantity({ + quantity: row.quantity, + manualQuantity: 0, + externalObjects: objects.map(({ target }) => ({ + effectiveQuantity: target.planningValues.effectiveQuantity, + })), + }); + + database.insert(circuitDeviceRows).values(toCircuitDeviceRowInsertValues(row)).run(); + this.updateCircuitState(database, projectId, row.circuitId); + this.applyObjectLinks(database, objects); + } + + private remove( + database: AppDatabase, + projectId: string, + command: ExternalObjectNewRowProjectCommand + ) { + const expectedRow = command.payload.row; + this.loadCircuitContext(database, projectId, expectedRow.circuitId); + const currentRow = database.select().from(circuitDeviceRows) + .where(eq(circuitDeviceRows.id, expectedRow.id)).get(); + if (!currentRow || !same(toCircuitDeviceRowSnapshot(currentRow), expectedRow)) { + throw new Error("Created external device row changed before history removal."); + } + const objects = this.loadExpectedObjects(database, projectId, command); + const linkedObjects = database.select({ id: externalModelObjects.id }) + .from(externalModelObjects) + .where(eq(externalModelObjects.circuitDeviceRowId, expectedRow.id)).all(); + if ( + linkedObjects.length !== objects.length || + linkedObjects.some(({ id }) => !objects.some((object) => object.expected.id === id)) + ) { + throw new Error("Created external device row has different object links."); + } + this.applyObjectLinks(database, objects); + const deleted = database.delete(circuitDeviceRows) + .where(eq(circuitDeviceRows.id, expectedRow.id)).run(); + if (deleted.changes !== 1) { + throw new Error("Created external device row changed during history removal."); + } + this.updateCircuitState(database, projectId, expectedRow.circuitId); + } + + private loadCircuitContext(database: AppDatabase, projectId: string, circuitId: string) { + const context = database.select({ + projectId: circuitLists.projectId, + distributionBoardId: circuitLists.distributionBoardId, + category: circuitSections.category, + }).from(circuits) + .innerJoin(circuitLists, eq(circuitLists.id, circuits.circuitListId)) + .innerJoin(circuitSections, eq(circuitSections.id, circuits.sectionId)) + .where(eq(circuits.id, circuitId)).get(); + if (!context || context.projectId !== projectId || context.category === null) { + throw new Error("Target circuit does not belong to the project."); + } + return { + projectId: context.projectId, + distributionBoardId: context.distributionBoardId, + category: context.category, + }; + } + + private loadExpectedObjects( + database: AppDatabase, + projectId: string, + command: ExternalObjectNewRowProjectCommand + ) { + const transitions = new Map( + command.payload.objects.map((transition) => [transition.expected.id, transition]) + ); + const current = database.select().from(externalModelObjects) + .where(inArray(externalModelObjects.id, [...transitions.keys()])).all(); + if (current.length !== transitions.size) { + throw new Error("One or more external objects no longer exist."); + } + for (const object of current) { + const expected = transitions.get(object.id)!.expected; + if (object.projectId !== projectId || !same(toExternalObjectSnapshot(object), expected)) { + throw new Error("External object changed before new-row assignment."); + } + } + return command.payload.objects; + } + + private assertTargetCompatibility( + database: AppDatabase, + row: CircuitDeviceRowSnapshot, + distributionBoardId: string, + category: string, + objects: ExternalModelObjectSnapshot[], + confirmedConflicts: Set + ) { + const roomMappings = database.select().from(externalRoomMappings) + .where(eq(externalRoomMappings.projectId, objects[0]!.projectId)).all(); + const roomIdByMappingId = new Map(roomMappings.map((mapping) => [mapping.id, mapping.roomId])); + const selectionMarkers = new Set( + objects.map((object) => object.acceptedSourceValues.selectionMarker.trim()) + ); + if (selectionMarkers.size > 1) { + throw new Error("External objects with different selection markers require separate rows."); + } + for (const object of objects) { + if (object.distributionBoardId !== distributionBoardId) { + throw new Error("External object distribution does not match target circuit."); + } + if (object.planningValues.category !== category) { + throw new Error("External object category does not match target circuit group."); + } + const objectRoomId = object.externalRoomMappingId === null + ? null + : roomIdByMappingId.get(object.externalRoomMappingId) ?? null; + if (objectRoomId !== row.roomId) { + throw new Error("External object room does not match the new device row."); + } + if (!matchesRowPlanningValues(row, object) && !confirmedConflicts.has(object.id)) { + throw new Error("External object planning values require explicit conflict confirmation."); + } + } + } + + private applyObjectLinks( + database: AppDatabase, + objects: ExternalObjectNewRowProjectCommand["payload"]["objects"] + ) { + for (const { expected, target } of objects) { + const result = database.update(externalModelObjects) + .set({ circuitDeviceRowId: target.circuitDeviceRowId }) + .where(and( + eq(externalModelObjects.id, expected.id), + expected.circuitDeviceRowId === null + ? isNull(externalModelObjects.circuitDeviceRowId) + : eq(externalModelObjects.circuitDeviceRowId, expected.circuitDeviceRowId) + )).run(); + if (result.changes !== 1) { + throw new Error("External object changed during new-row assignment."); + } + } + } + + private updateCircuitState(database: AppDatabase, projectId: string, circuitId: string) { + const remaining = database.select({ id: circuitDeviceRows.id }).from(circuitDeviceRows) + .where(eq(circuitDeviceRows.circuitId, circuitId)).limit(1).get(); + const result = database.update(circuits).set({ isReserve: remaining ? 0 : 1 }) + .where(eq(circuits.id, circuitId)).run(); + if (result.changes !== 1) throw new Error("Target circuit changed during row assignment."); + updateDerivedCircuitVoltage(database, projectId, circuitId); + } +} + +function matchesRowPlanningValues( + row: CircuitDeviceRowSnapshot, + object: ExternalModelObjectSnapshot +) { + const planning = object.planningValues; + return ( + row.displayName === (planning.displayName ?? row.displayName) && + row.linkedProjectDeviceId === object.linkedProjectDeviceId && + row.category === planning.category && + row.connectionKind === planning.connectionKind && + (planning.powerPerUnitW === null || row.powerPerUnit === planning.powerPerUnitW / 1000) && + row.simultaneityFactor === planning.simultaneityFactor && + row.cosPhi === planning.cosPhi && + row.costGroup === planning.costGroup && + row.remark === planning.remark + ); +} + +function toExternalObjectSnapshot( + object: typeof externalModelObjects.$inferSelect +): ExternalModelObjectSnapshot { + return { ...object }; +} + +function same(left: unknown, right: unknown) { + return canonicalJson(left) === canonicalJson(right); +} + +function canonicalJson(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; + if (value !== null && typeof value === "object") { + const record = value as Record; + return `{${Object.keys(record).sort().map((key) => + `${JSON.stringify(key)}:${canonicalJson(record[key])}` + ).join(",")}}`; + } + return JSON.stringify(value); +} diff --git a/src/domain/models/external-object-new-row-project-command.model.ts b/src/domain/models/external-object-new-row-project-command.model.ts new file mode 100644 index 0000000..d0416dd --- /dev/null +++ b/src/domain/models/external-object-new-row-project-command.model.ts @@ -0,0 +1,163 @@ +import type { ExternalModelObjectSnapshot } from "../../external-model/domain/external-model-contracts.js"; +import { + assertCircuitDeviceRowInsertProjectCommand, + circuitDeviceRowInsertCommandType, + circuitDeviceRowStructureCommandSchemaVersion, + type CircuitDeviceRowSnapshot, +} from "./circuit-device-row-structure-project-command.model.js"; +import type { SerializedProjectCommand } from "./project-command.model.js"; +import { parseExternalModelStateSnapshot } from "./project-state-snapshot.model.js"; + +export const externalObjectAssignToNewRowCommandType = + "external-object.assign-to-new-row" as const; +export const externalObjectDeleteCreatedRowCommandType = + "external-object.unassign-and-delete-created-row" as const; +export const externalObjectNewRowCommandSchemaVersion = 1 as const; + +export interface ExternalObjectNewRowPayload { + row: CircuitDeviceRowSnapshot; + objects: Array<{ + expected: ExternalModelObjectSnapshot; + target: ExternalModelObjectSnapshot; + }>; + confirmedConflictObjectIds: string[]; +} + +export interface ExternalObjectNewRowProjectCommand + extends SerializedProjectCommand { + schemaVersion: typeof externalObjectNewRowCommandSchemaVersion; + type: + | typeof externalObjectAssignToNewRowCommandType + | typeof externalObjectDeleteCreatedRowCommandType; +} + +export function createExternalObjectNewRowProjectCommand( + type: ExternalObjectNewRowProjectCommand["type"], + payload: ExternalObjectNewRowPayload +): ExternalObjectNewRowProjectCommand { + const command: ExternalObjectNewRowProjectCommand = { + schemaVersion: externalObjectNewRowCommandSchemaVersion, + type, + payload: { + row: { + ...payload.row, + manualQuantity: payload.row.manualQuantity ?? payload.row.quantity, + }, + objects: payload.objects, + confirmedConflictObjectIds: [...payload.confirmedConflictObjectIds], + }, + }; + assertExternalObjectNewRowProjectCommand(command); + return command; +} + +export function invertExternalObjectNewRowProjectCommand( + command: ExternalObjectNewRowProjectCommand +) { + return createExternalObjectNewRowProjectCommand( + command.type === externalObjectAssignToNewRowCommandType + ? externalObjectDeleteCreatedRowCommandType + : externalObjectAssignToNewRowCommandType, + { + row: command.payload.row, + objects: command.payload.objects.map(({ expected, target }) => ({ + expected: target, + target: expected, + })), + confirmedConflictObjectIds: command.payload.confirmedConflictObjectIds, + } + ); +} + +export function assertExternalObjectNewRowProjectCommand( + command: SerializedProjectCommand +): asserts command is ExternalObjectNewRowProjectCommand { + if ( + command.schemaVersion !== externalObjectNewRowCommandSchemaVersion || + (command.type !== externalObjectAssignToNewRowCommandType && + command.type !== externalObjectDeleteCreatedRowCommandType) || + !isRecord(command.payload) || + !Array.isArray(command.payload.objects) || + command.payload.objects.length === 0 || + !Array.isArray(command.payload.confirmedConflictObjectIds) + ) { + throw new Error("Unsupported external object new-row command."); + } + const row = parseRow(command.payload.row); + if (row.manualQuantity !== 0) { + throw new Error("An external-only device row must start with zero manual quantity."); + } + const objectIds = new Set(); + for (const transition of command.payload.objects) { + if (!isRecord(transition)) throw new Error("Invalid external object transition."); + const expected = parseObject(transition.expected); + const target = parseObject(transition.target); + if (expected.id !== target.id || objectIds.has(expected.id)) { + throw new Error("New-row assignment contains mismatched or duplicate objects."); + } + objectIds.add(expected.id); + const assigning = command.type === externalObjectAssignToNewRowCommandType; + if ( + (assigning && (expected.circuitDeviceRowId !== null || target.circuitDeviceRowId !== row.id)) || + (!assigning && (expected.circuitDeviceRowId !== row.id || target.circuitDeviceRowId !== null)) + ) { + throw new Error("External object links do not match the new-row action."); + } + assertOnlyAssignmentChanged(expected, target); + } + const confirmedIds = new Set(); + for (const objectId of command.payload.confirmedConflictObjectIds) { + if (typeof objectId !== "string" || !objectIds.has(objectId) || confirmedIds.has(objectId)) { + throw new Error("New-row assignment contains an invalid conflict confirmation."); + } + confirmedIds.add(objectId); + } +} + +function parseRow(value: unknown): CircuitDeviceRowSnapshot { + const envelope = { + schemaVersion: circuitDeviceRowStructureCommandSchemaVersion, + type: circuitDeviceRowInsertCommandType, + payload: { row: value }, + }; + assertCircuitDeviceRowInsertProjectCommand(envelope); + const row = envelope.payload.row; + if (row.manualQuantity === undefined) { + throw new Error("External object new-row command requires manual quantity."); + } + return row; +} + +function parseObject(value: unknown): ExternalModelObjectSnapshot { + return parseExternalModelStateSnapshot({ + source: null, + importBatches: [], + roomMappings: [], + objects: [value], + }).objects[0]!; +} + +function assertOnlyAssignmentChanged( + expected: ExternalModelObjectSnapshot, + target: ExternalModelObjectSnapshot +) { + const expectedRest = { ...expected, circuitDeviceRowId: null }; + const targetRest = { ...target, circuitDeviceRowId: null }; + if (canonicalJson(expectedRest) !== canonicalJson(targetRest)) { + throw new Error("New-row assignment may only change the external object row link."); + } +} + +function canonicalJson(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; + if (isRecord(value)) { + return `{${Object.keys(value).sort().map((key) => + `${JSON.stringify(key)}:${canonicalJson(value[key])}` + ).join(",")}}`; + } + return JSON.stringify(value); +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} diff --git a/src/domain/ports/external-object-new-row-project-command.store.ts b/src/domain/ports/external-object-new-row-project-command.store.ts new file mode 100644 index 0000000..cd13369 --- /dev/null +++ b/src/domain/ports/external-object-new-row-project-command.store.ts @@ -0,0 +1,19 @@ +import type { ExternalObjectNewRowProjectCommand } from "../models/external-object-new-row-project-command.model.js"; +import type { AppendedProjectRevision, ProjectRevisionSource } from "./project-revision.store.js"; + +export interface ExecuteExternalObjectNewRowCommandInput { + projectId: string; + expectedRevision: number; + source: ProjectRevisionSource; + description?: string; + actorId?: string; + historyTargetChangeSetId?: string; + command: ExternalObjectNewRowProjectCommand; +} + +export interface ExternalObjectNewRowProjectCommandStore { + execute(input: ExecuteExternalObjectNewRowCommandInput): { + revision: AppendedProjectRevision; + inverse: ExternalObjectNewRowProjectCommand; + }; +} diff --git a/src/domain/services/project-command.service.ts b/src/domain/services/project-command.service.ts index 2a90470..be66428 100644 --- a/src/domain/services/project-command.service.ts +++ b/src/domain/services/project-command.service.ts @@ -176,6 +176,12 @@ import { externalObjectRowAssignmentCommandType, } from "../models/external-object-row-assignment-project-command.model.js"; import type { ExternalObjectRowAssignmentProjectCommandStore } from "../ports/external-object-row-assignment-project-command.store.js"; +import { + assertExternalObjectNewRowProjectCommand, + externalObjectAssignToNewRowCommandType, + externalObjectDeleteCreatedRowCommandType, +} from "../models/external-object-new-row-project-command.model.js"; +import type { ExternalObjectNewRowProjectCommandStore } from "../ports/external-object-new-row-project-command.store.js"; interface DispatchProjectCommandInput { projectId: string; @@ -213,7 +219,8 @@ export class ProjectCommandService implements ProjectCommandExecutor { private readonly historyStore: ProjectHistoryStore, private readonly externalCsvConfigurationStore?: ExternalCsvConfigurationProjectCommandStore, private readonly externalInitialImportStore?: ExternalInitialImportProjectCommandStore, - private readonly externalObjectRowAssignmentStore?: ExternalObjectRowAssignmentProjectCommandStore + private readonly externalObjectRowAssignmentStore?: ExternalObjectRowAssignmentProjectCommandStore, + private readonly externalObjectNewRowStore?: ExternalObjectNewRowProjectCommandStore ) {} executeUser( @@ -600,6 +607,17 @@ export class ProjectCommandService implements ProjectCommandExecutor { command: input.command, }).revision; } + case externalObjectAssignToNewRowCommandType: + case externalObjectDeleteCreatedRowCommandType: { + assertExternalObjectNewRowProjectCommand(input.command); + if (!this.externalObjectNewRowStore) { + throw new Error("External object new-row store is not available."); + } + return this.externalObjectNewRowStore.execute({ + ...input, + command: input.command, + }).revision; + } case projectStateRestoreCommandType: { assertProjectStateRestoreCommand(input.command); return this.projectStateRestoreStore.execute({ diff --git a/src/server/composition/project-command-stores.ts b/src/server/composition/project-command-stores.ts index 9b6d539..9e63ed6 100644 --- a/src/server/composition/project-command-stores.ts +++ b/src/server/composition/project-command-stores.ts @@ -25,6 +25,7 @@ import { ProjectCommandService } from "../../domain/services/project-command.ser import { ExternalCsvConfigurationProjectCommandRepository } from "../../db/repositories/external-csv-configuration-project-command.repository.js"; import { ExternalInitialImportProjectCommandRepository } from "../../db/repositories/external-initial-import-project-command.repository.js"; import { ExternalObjectRowAssignmentProjectCommandRepository } from "../../db/repositories/external-object-row-assignment-project-command.repository.js"; +import { ExternalObjectNewRowProjectCommandRepository } from "../../db/repositories/external-object-new-row-project-command.repository.js"; export const circuitProjectCommandStore = new CircuitProjectCommandRepository(db); export const circuitDeviceRowProjectCommandStore = @@ -74,6 +75,8 @@ export const externalInitialImportProjectCommandStore = new ExternalInitialImportProjectCommandRepository(db); export const externalObjectRowAssignmentProjectCommandStore = new ExternalObjectRowAssignmentProjectCommandRepository(db); +export const externalObjectNewRowProjectCommandStore = + new ExternalObjectNewRowProjectCommandRepository(db); export const projectCommandService = new ProjectCommandService( circuitProjectCommandStore, circuitDeviceRowProjectCommandStore, @@ -99,5 +102,6 @@ export const projectCommandService = new ProjectCommandService( projectHistoryStore, externalCsvConfigurationProjectCommandStore, externalInitialImportProjectCommandStore, - externalObjectRowAssignmentProjectCommandStore + externalObjectRowAssignmentProjectCommandStore, + externalObjectNewRowProjectCommandStore ); diff --git a/tests/external-object-row-assignment-project-command.repository.test.ts b/tests/external-object-row-assignment-project-command.repository.test.ts index 5585204..4e127f1 100644 --- a/tests/external-object-row-assignment-project-command.repository.test.ts +++ b/tests/external-object-row-assignment-project-command.repository.test.ts @@ -5,6 +5,7 @@ import { eq } from "drizzle-orm"; import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import { createDatabaseContext, type AppDatabase } from "../src/db/database-context.js"; import { ExternalObjectRowAssignmentProjectCommandRepository } from "../src/db/repositories/external-object-row-assignment-project-command.repository.js"; +import { ExternalObjectNewRowProjectCommandRepository } from "../src/db/repositories/external-object-new-row-project-command.repository.js"; import { toCircuitDeviceRowSnapshot } from "../src/db/repositories/circuit-device-row-structure.persistence.js"; import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js"; import { circuitLists } from "../src/db/schema/circuit-lists.js"; @@ -19,10 +20,14 @@ import { floors } from "../src/db/schema/floors.js"; import { projects } from "../src/db/schema/projects.js"; import { rooms } from "../src/db/schema/rooms.js"; import { createExternalObjectRowAssignmentProjectCommand } from "../src/domain/models/external-object-row-assignment-project-command.model.js"; +import { + createExternalObjectNewRowProjectCommand, + externalObjectAssignToNewRowCommandType, +} from "../src/domain/models/external-object-new-row-project-command.model.js"; import type { ExternalModelObjectSnapshot } from "../src/external-model/domain/external-model-contracts.js"; import { externalCsvTestConfiguration } from "./fixtures/revit-csv-fixtures.js"; -function createFixture() { +function createFixture(initialObjectRowId: string | null = "row-1") { const context = createDatabaseContext(":memory:"); migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations") }); const database = context.db; @@ -46,7 +51,7 @@ function createFixture() { roomId: "room-1", roomNumberSnapshot: "101", roomNameSnapshot: "Büro", - quantity: index === 1 ? 3 : 0, + quantity: index === 1 ? (initialObjectRowId === "row-1" ? 3 : 1) : 0, manualQuantity: index === 1 ? 1 : 0, powerPerUnit: 0.12, simultaneityFactor: 1, @@ -78,7 +83,7 @@ function createFixture() { roomId: "room-1", defaultDistributionBoardId: "board-1", }).run(); - database.insert(externalModelObjects).values(objectSnapshot("row-1")).run(); + database.insert(externalModelObjects).values(objectSnapshot(initialObjectRowId)).run(); return context; } @@ -225,3 +230,149 @@ describe("external object row assignment project command", () => { } }); }); + +describe("external object new-row project command", () => { + it("creates an external-only row and supports exact persisted undo and redo", () => { + const context = createFixture(null); + try { + const base = row(context.db, "row-2"); + const newRow = { + ...base, + id: "row-new", + quantity: 2, + manualQuantity: 0, + sortOrder: 20, + }; + const object = objectSnapshot(null); + const command = createExternalObjectNewRowProjectCommand( + externalObjectAssignToNewRowCommandType, + { + row: newRow, + objects: [{ + expected: object, + target: { ...object, circuitDeviceRowId: "row-new" }, + }], + confirmedConflictObjectIds: [], + } + ); + const repository = new ExternalObjectNewRowProjectCommandRepository(context.db); + const inserted = repository.execute({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + command, + }); + assert.equal(row(context.db, "row-new").manualQuantity, 0); + assert.equal(row(context.db, "row-new").quantity, 2); + assert.equal(context.db.select().from(externalModelObjects).get()!.circuitDeviceRowId, "row-new"); + assert.throws( + () => repository.execute({ + projectId: "project-1", + expectedRevision: 1, + source: "user", + command: inserted.inverse, + }), + /only be removed through project history/ + ); + + const undone = repository.execute({ + projectId: "project-1", + expectedRevision: 1, + source: "undo", + historyTargetChangeSetId: inserted.revision.changeSetId, + command: inserted.inverse, + }); + assert.equal( + context.db.select().from(circuitDeviceRows).where(eq(circuitDeviceRows.id, "row-new")).get(), + undefined + ); + assert.equal(context.db.select().from(externalModelObjects).get()!.circuitDeviceRowId, null); + + repository.execute({ + projectId: "project-1", + expectedRevision: 2, + source: "redo", + historyTargetChangeSetId: inserted.revision.changeSetId, + command: undone.inverse, + }); + assert.equal(row(context.db, "row-new").quantity, 2); + } finally { + context.close(); + } + }); + + it("rolls back the row and object link when history persistence fails", () => { + const context = createFixture(null); + try { + context.sqlite.exec(` + CREATE TRIGGER fail_new_row_history + BEFORE INSERT ON project_change_sets + BEGIN SELECT RAISE(ABORT, 'late history failure'); END; + `); + const base = row(context.db, "row-2"); + const object = objectSnapshot(null); + const command = createExternalObjectNewRowProjectCommand( + externalObjectAssignToNewRowCommandType, + { + row: { ...base, id: "row-new", quantity: 2, manualQuantity: 0, sortOrder: 20 }, + objects: [{ expected: object, target: { ...object, circuitDeviceRowId: "row-new" } }], + confirmedConflictObjectIds: [], + } + ); + assert.throws( + () => new ExternalObjectNewRowProjectCommandRepository(context.db).execute({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + command, + }), + /late history failure/ + ); + assert.equal( + context.db.select().from(circuitDeviceRows).where(eq(circuitDeviceRows.id, "row-new")).get(), + undefined + ); + assert.equal(context.db.select().from(externalModelObjects).get()!.circuitDeviceRowId, null); + } finally { + context.close(); + } + }); + + it("keeps the created row when it changed after assignment", () => { + const context = createFixture(null); + try { + const base = row(context.db, "row-2"); + const object = objectSnapshot(null); + const repository = new ExternalObjectNewRowProjectCommandRepository(context.db); + const inserted = repository.execute({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + command: createExternalObjectNewRowProjectCommand( + externalObjectAssignToNewRowCommandType, + { + row: { ...base, id: "row-new", quantity: 2, manualQuantity: 0, sortOrder: 20 }, + objects: [{ expected: object, target: { ...object, circuitDeviceRowId: "row-new" } }], + confirmedConflictObjectIds: [], + } + ), + }); + context.db.update(circuitDeviceRows).set({ displayName: "Geändert" }) + .where(eq(circuitDeviceRows.id, "row-new")).run(); + assert.throws( + () => repository.execute({ + projectId: "project-1", + expectedRevision: 1, + source: "undo", + historyTargetChangeSetId: inserted.revision.changeSetId, + command: inserted.inverse, + }), + /changed before history removal/ + ); + assert.equal(row(context.db, "row-new").displayName, "Geändert"); + assert.equal(context.db.select().from(externalModelObjects).get()!.circuitDeviceRowId, "row-new"); + } finally { + context.close(); + } + }); +}); From a69b7b603f9383ce64511697b67396568599a6ae Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 2 Aug 2026 18:55:57 +0200 Subject: [PATCH 47/59] Add external object new circuit command --- AGENTS.md | 6 + docs/circuit-list-editor-known-limitations.md | 8 +- docs/current-architecture.md | 11 + .../spec/revit-csv-phase-14-audit-and-plan.md | 12 +- .../external-object-assignment.persistence.ts | 137 ++++++++++ ...-new-circuit-project-command.repository.ts | 239 ++++++++++++++++++ ...ject-new-row-project-command.repository.ts | 152 +++-------- ...w-assignment-project-command.repository.ts | 135 +++------- ...bject-new-circuit-project-command.model.ts | 168 ++++++++++++ ...bject-new-circuit-project-command.store.ts | 19 ++ .../services/project-command.service.ts | 20 +- .../composition/project-command-stores.ts | 6 +- ...ignment-project-command.repository.test.ts | 195 ++++++++++++++ 13 files changed, 880 insertions(+), 228 deletions(-) create mode 100644 src/db/repositories/external-object-assignment.persistence.ts create mode 100644 src/db/repositories/external-object-new-circuit-project-command.repository.ts create mode 100644 src/domain/models/external-object-new-circuit-project-command.model.ts create mode 100644 src/domain/ports/external-object-new-circuit-project-command.store.ts diff --git a/AGENTS.md b/AGENTS.md index 6332166..0b7077d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -299,6 +299,12 @@ objects as one external-only row (`manualQuantity = 0`) into an existing circuit. Its history-only inverse removes the row only while its complete snapshot and exact object-link set remain unchanged. Ordinary detach continues to preserve the row. +`external-object.assign-to-new-circuit` atomically inserts a preplanned circuit +with stable BMK/UUIDs, derived voltage, one external-only row, required default +protection and its external object links. Its history-only inverse requires the +complete unchanged circuit snapshot and exact link set. Redo never recalculates +the BMK. All three external assignment stores share +`external-object-assignment.persistence.ts` for compatibility and link safety. Confirmed initial state is written only through `external-import.apply-initial`. The command rechecks configuration version, original-byte SHA-256, parsed matrix, complete IFCGUID/source values, explicit diff --git a/docs/circuit-list-editor-known-limitations.md b/docs/circuit-list-editor-known-limitations.md index 0a7b076..4f7f660 100644 --- a/docs/circuit-list-editor-known-limitations.md +++ b/docs/circuit-list-editor-known-limitations.md @@ -10,10 +10,10 @@ through the project page with persistent Undo/Redo. Automatic snapshots are created every 25 revisions and only their newest 12 entries are retained. - The Revit/CSV foundation and confirmed initial import are implemented. - Existing-row assignment, move and detach and creation of an external-only row - in an existing circuit have atomic persistent commands. Their editor UI, the - new-circuit path, follow-up import, conflict review and return export are - pending. + Existing-row assignment, move and detach plus creation of an external-only + row in an existing or newly created circuit have atomic persistent commands. + Their API planning and editor UI, follow-up import, conflict review and return + export are pending. - Persistence currently targets local SQLite; PostgreSQL is an architectural option, not an implemented runtime. - The global device library supports basic CRUD and copy operations, but has no versioning, permissions or controlled synchronization model. diff --git a/docs/current-architecture.md b/docs/current-architecture.md index fb4f2ee..a0282de 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -559,6 +559,17 @@ und löscht die erzeugte Row nur, wenn sie und ihre Linkmenge unverändert sind. Der Löschbefehl ist ausschließlich für die Projekthistorie zugelassen; eine normale spätere Trennung lässt die Row gemäß der allgemeinen Zuordnungsregel bestehen. +`external-object.assign-to-new-circuit` erzeugt einen neuen Stromkreis mit +vorab festgeschriebenem BMK, abgeleiteter Projektspannung, genau einer +externen Row und verpflichtendem Standardschutz zusammen mit den Objektlinks. +Verteilungs-, Gruppen-, Raum-, Mengen- und Planungsregeln werden vor dem Insert +gegen den aktuellen Projektstand geprüft. Der historische Gegenbefehl ist kein +allgemeiner Löschpfad: Er entfernt den vollständigen Circuit-Teilbaum nur bei +unverändertem Snapshot und unveränderter Objektlinkmenge. Redo verwendet exakt +dieselben IDs, dasselbe BMK und denselben Schutzsnapshot. +Die drei Zuordnungsadapter teilen sich +`external-object-assignment.persistence.ts` für Snapshotvergleich, +Kompatibilitätsregeln und link-sichere Updates. `GET` und `PUT /api/projects/:projectId/external-csv/configuration` lesen oder ändern die Konfiguration; der PUT plant Identität und nächsten Konfigurationsstand serverseitig und verwendet den typisierten Command. diff --git a/docs/spec/revit-csv-phase-14-audit-and-plan.md b/docs/spec/revit-csv-phase-14-audit-and-plan.md index ffc1b45..b721c3a 100644 --- a/docs/spec/revit-csv-phase-14-audit-and-plan.md +++ b/docs/spec/revit-csv-phase-14-audit-and-plan.md @@ -205,9 +205,9 @@ bleiben und dürfen spätere Quellstände nicht erneut auswerten. - `external-object.update-row-assignment`: Ein oder mehrere Objekte einer bestehenden Row zuweisen, zwischen bestehenden Rows verschieben oder lösen und alle betroffenen materialisierten Mengen atomar aktualisieren. -- `external-model-object.assign-to-new-row`: stabile neue Row einfügen, +- `external-object.assign-to-new-row`: stabile neue Row einfügen, Objekte verknüpfen und gegebenenfalls den Reserve-Status aktualisieren. -- `external-model-object.assign-to-new-circuit`: stabilen Circuit samt Schutz, +- `external-object.assign-to-new-circuit`: stabilen Circuit samt Schutz, Row und Objektlinks in einem Schritt einfügen. Das geplante BMK wird im Command gespeichert und bei Redo nicht neu berechnet. - `external-model-object.delete-missing`: nur ausdrücklich bestätigte, @@ -330,7 +330,13 @@ Kombination aus Originalbytes und Matrix als Entscheidungen bestätigt. Diese Row erhält eine stabile ID und `manualQuantity = 0`; ihr historischer Gegenbefehl entfernt sie nur, solange Row und Objektlinks vollständig unverändert sind. Ein direktes Löschen über diesen internen Gegenbefehl ist - als Nutzeraktion gesperrt. Der neue Circuit folgt separat. + als Nutzeraktion gesperrt. `external-object.assign-to-new-circuit` schließt + den dritten Zielpfad: Ein serverseitig vorgeplanter Circuit mit festem BMK, + Standardschutz und genau einer externen Row wird gemeinsam mit den + Objektlinks eingefügt. Seine Historiengegenrichtung prüft den vollständigen + Circuit-Snapshot und die exakte Linkmenge, bevor sie den Teilbaum entfernt. + BMK und UUIDs bleiben bei Redo unverändert. Damit ist die persistente + Command-Grundlage dieses Schritts abgeschlossen; API-Planung und UI folgen. 3. Verteilungsbezogenen Drawer, Filter und Vorschau der Mengenwirkung ergänzen. 4. Einzel- und Mehrfach-Drag-and-drop samt Warnungen, Undo/Redo und Reload testen. diff --git a/src/db/repositories/external-object-assignment.persistence.ts b/src/db/repositories/external-object-assignment.persistence.ts new file mode 100644 index 0000000..ddc9891 --- /dev/null +++ b/src/db/repositories/external-object-assignment.persistence.ts @@ -0,0 +1,137 @@ +import { and, eq, inArray, isNull } from "drizzle-orm"; +import type { CircuitDeviceRowSnapshot } from "../../domain/models/circuit-device-row-structure-project-command.model.js"; +import type { ExternalModelObjectSnapshot } from "../../external-model/domain/external-model-contracts.js"; +import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js"; +import type { AppDatabase } from "../database-context.js"; +import { externalModelObjects } from "../schema/external-model-objects.js"; +import { externalRoomMappings } from "../schema/external-room-mappings.js"; + +export interface ExternalObjectLinkTransition { + expected: ExternalModelObjectSnapshot; + target: ExternalModelObjectSnapshot; +} + +export function loadExpectedExternalObjectTransitions( + database: AppDatabase, + projectId: string, + transitions: ExternalObjectLinkTransition[], + changedMessage: string +) { + const byId = new Map( + transitions.map((transition) => [transition.expected.id, transition]) + ); + const current = database.select().from(externalModelObjects) + .where(inArray(externalModelObjects.id, [...byId.keys()])).all(); + if (current.length !== byId.size) { + throw new Error("One or more external objects no longer exist."); + } + for (const object of current) { + if ( + object.projectId !== projectId || + !snapshotsEqual(toExternalObjectSnapshot(object), byId.get(object.id)!.expected) + ) { + throw new Error(changedMessage); + } + } + return transitions; +} + +export function assertExternalObjectsCompatibleWithRow(input: { + database: AppDatabase; + projectId: string; + row: CircuitDeviceRowSnapshot; + distributionBoardId: string; + category: CircuitGroupCategory; + assignedObjects: ExternalModelObjectSnapshot[]; + allLinkedObjects: ExternalModelObjectSnapshot[]; + confirmedConflictObjectIds: ReadonlySet; +}) { + const mappings = input.database.select().from(externalRoomMappings) + .where(eq(externalRoomMappings.projectId, input.projectId)).all(); + const roomIdByMappingId = new Map(mappings.map((mapping) => [mapping.id, mapping.roomId])); + const markers = new Set( + input.allLinkedObjects.map((object) => + object.acceptedSourceValues.selectionMarker.trim() + ) + ); + if (markers.size > 1) { + throw new Error("External objects with different selection markers require separate rows."); + } + for (const object of input.assignedObjects) { + if (object.distributionBoardId !== input.distributionBoardId) { + throw new Error("External object distribution does not match target circuit."); + } + if (object.planningValues.category !== input.category) { + throw new Error("External object category does not match target circuit group."); + } + const roomId = object.externalRoomMappingId === null + ? null + : roomIdByMappingId.get(object.externalRoomMappingId) ?? null; + if (roomId !== input.row.roomId) { + throw new Error("External object room does not match target device row."); + } + if ( + !matchesRowPlanningValues(input.row, object) && + !input.confirmedConflictObjectIds.has(object.id) + ) { + throw new Error("External object planning values require explicit conflict confirmation."); + } + } +} + +export function applyExternalObjectLinks( + database: AppDatabase, + transitions: ExternalObjectLinkTransition[], + changedMessage: string +) { + for (const { expected, target } of transitions) { + const updated = database.update(externalModelObjects) + .set({ circuitDeviceRowId: target.circuitDeviceRowId }) + .where(and( + eq(externalModelObjects.id, expected.id), + expected.circuitDeviceRowId === null + ? isNull(externalModelObjects.circuitDeviceRowId) + : eq(externalModelObjects.circuitDeviceRowId, expected.circuitDeviceRowId) + )).run(); + if (updated.changes !== 1) throw new Error(changedMessage); + } +} + +export function toExternalObjectSnapshot( + object: typeof externalModelObjects.$inferSelect +): ExternalModelObjectSnapshot { + return { ...object }; +} + +export function snapshotsEqual(left: unknown, right: unknown) { + return canonicalJson(left) === canonicalJson(right); +} + +function matchesRowPlanningValues( + row: CircuitDeviceRowSnapshot, + object: ExternalModelObjectSnapshot +) { + const planning = object.planningValues; + return ( + row.displayName === (planning.displayName ?? row.displayName) && + row.linkedProjectDeviceId === object.linkedProjectDeviceId && + row.category === planning.category && + row.connectionKind === planning.connectionKind && + (planning.powerPerUnitW === null || row.powerPerUnit === planning.powerPerUnitW / 1000) && + row.simultaneityFactor === planning.simultaneityFactor && + row.cosPhi === planning.cosPhi && + row.costGroup === planning.costGroup && + row.remark === planning.remark + ); +} + +function canonicalJson(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; + if (value !== null && typeof value === "object") { + const record = value as Record; + return `{${Object.keys(record).sort().map((key) => + `${JSON.stringify(key)}:${canonicalJson(record[key])}` + ).join(",")}}`; + } + return JSON.stringify(value); +} diff --git a/src/db/repositories/external-object-new-circuit-project-command.repository.ts b/src/db/repositories/external-object-new-circuit-project-command.repository.ts new file mode 100644 index 0000000..c25ac76 --- /dev/null +++ b/src/db/repositories/external-object-new-circuit-project-command.repository.ts @@ -0,0 +1,239 @@ +import { and, asc, eq } from "drizzle-orm"; +import { assertCircuitDeviceRowQuantity } from "../../domain/calculations/circuit-device-row-quantity.js"; +import { + assertExternalObjectNewCircuitProjectCommand, + externalObjectAssignToNewCircuitCommandType, + externalObjectDeleteCreatedCircuitCommandType, + invertExternalObjectNewCircuitProjectCommand, + type ExternalObjectNewCircuitProjectCommand, +} from "../../domain/models/external-object-new-circuit-project-command.model.js"; +import type { CircuitDeviceRowSnapshot } from "../../domain/models/circuit-device-row-structure-project-command.model.js"; +import type { CircuitSnapshot } from "../../domain/models/circuit-structure-project-command.model.js"; +import type { ExternalObjectNewCircuitProjectCommandStore } from "../../domain/ports/external-object-new-circuit-project-command.store.js"; +import { parseGroupedEquipmentIdentifier } from "../../domain/services/circuit-group-numbering.js"; +import { isElectricalPhaseType } from "../../domain/services/project-voltage.service.js"; +import type { AppDatabase } from "../database-context.js"; +import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; +import { circuitLists } from "../schema/circuit-lists.js"; +import { circuitProtectionDevices } from "../schema/circuit-protection-devices.js"; +import { circuitSections } from "../schema/circuit-sections.js"; +import { circuits } from "../schema/circuits.js"; +import { externalModelObjects } from "../schema/external-model-objects.js"; +import { + assertCircuitDeviceRowReferencesInProject, + toCircuitDeviceRowInsertValues, + toCircuitDeviceRowSnapshot, +} from "./circuit-device-row-structure.persistence.js"; +import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { resolveCircuitVoltage } from "./project-voltage.persistence.js"; +import { + applyExternalObjectLinks, + assertExternalObjectsCompatibleWithRow, + loadExpectedExternalObjectTransitions, + snapshotsEqual, +} from "./external-object-assignment.persistence.js"; + +export class ExternalObjectNewCircuitProjectCommandRepository + implements ExternalObjectNewCircuitProjectCommandStore +{ + constructor(private readonly database: AppDatabase) {} + + execute(input: Parameters[0]) { + assertExternalObjectNewCircuitProjectCommand(input.command); + if ( + input.source === "user" && + input.command.type === externalObjectDeleteCreatedCircuitCommandType + ) { + throw new Error("Created external circuits may only be removed through project history."); + } + return executeProjectCommandTransaction(this.database, input, (tx) => { + if (input.command.type === externalObjectAssignToNewCircuitCommandType) { + this.insert(tx, input.projectId, input.command); + } else { + this.remove(tx, input.projectId, input.command); + } + return invertExternalObjectNewCircuitProjectCommand(input.command); + }); + } + + private insert( + database: AppDatabase, + projectId: string, + command: ExternalObjectNewCircuitProjectCommand + ) { + const circuit = command.payload.circuit; + const row = circuit.deviceRows[0]!; + const context = this.loadSectionContext( + database, + projectId, + circuit.circuitListId, + circuit.sectionId + ); + const parsedIdentifier = parseGroupedEquipmentIdentifier(circuit.equipmentIdentifier); + if ( + !parsedIdentifier || + parsedIdentifier.kind !== "circuit" || + parsedIdentifier.category !== context.category || + parsedIdentifier.groupNumber !== context.groupNumber + ) { + throw new Error("External circuit equipment identifier does not match target group."); + } + if (row.category !== context.category || !isElectricalPhaseType(row.phaseType)) { + throw new Error("External device row does not match target circuit group."); + } + assertCircuitDeviceRowReferencesInProject(database, projectId, row); + const voltage = resolveCircuitVoltage(database, projectId, circuit.sectionId, [row.phaseType]); + if (circuit.voltage !== voltage) { + throw new Error("External circuit voltage must match the project phase voltage."); + } + if (database.select({ id: circuits.id }).from(circuits) + .where(eq(circuits.id, circuit.id)).get()) { + throw new Error("External circuit id already exists."); + } + if (database.select({ id: circuits.id }).from(circuits).where(and( + eq(circuits.circuitListId, circuit.circuitListId), + eq(circuits.equipmentIdentifier, circuit.equipmentIdentifier) + )).get()) { + throw new Error("Duplicate equipmentIdentifier in circuit list."); + } + if (database.select({ id: circuitDeviceRows.id }).from(circuitDeviceRows) + .where(eq(circuitDeviceRows.id, row.id)).get()) { + throw new Error("External device-row id already exists."); + } + + const objects = this.loadExpectedObjects(database, projectId, command); + assertExternalObjectsCompatibleWithRow({ + database, + projectId, + row, + distributionBoardId: context.distributionBoardId, + category: context.category, + assignedObjects: objects.map(({ target }) => target), + allLinkedObjects: objects.map(({ target }) => target), + confirmedConflictObjectIds: new Set(command.payload.confirmedConflictObjectIds), + }); + assertCircuitDeviceRowQuantity({ + quantity: row.quantity, + manualQuantity: 0, + externalObjects: objects.map(({ target }) => ({ + effectiveQuantity: target.planningValues.effectiveQuantity, + })), + }); + + database.insert(circuits).values(toCircuitValues(circuit)).run(); + database.insert(circuitDeviceRows).values(toCircuitDeviceRowInsertValues(row)).run(); + database.insert(circuitProtectionDevices).values(circuit.protectionDevice!).run(); + applyExternalObjectLinks( + database, + objects, + "External object changed during new-circuit assignment." + ); + } + + private remove( + database: AppDatabase, + projectId: string, + command: ExternalObjectNewCircuitProjectCommand + ) { + const expectedCircuit = command.payload.circuit; + this.loadSectionContext( + database, + projectId, + expectedCircuit.circuitListId, + expectedCircuit.sectionId + ); + const current = this.loadCircuitSnapshot(database, expectedCircuit.id); + if (!current || !snapshotsEqual(current, expectedCircuit)) { + throw new Error("Created external circuit changed before history removal."); + } + const objects = this.loadExpectedObjects(database, projectId, command); + const rowId = expectedCircuit.deviceRows[0]!.id; + const linkedObjects = database.select({ id: externalModelObjects.id }) + .from(externalModelObjects) + .where(eq(externalModelObjects.circuitDeviceRowId, rowId)).all(); + if ( + linkedObjects.length !== objects.length || + linkedObjects.some(({ id }) => !objects.some(({ expected }) => expected.id === id)) + ) { + throw new Error("Created external circuit has different object links."); + } + applyExternalObjectLinks( + database, + objects, + "External object changed during new-circuit assignment." + ); + const deleted = database.delete(circuits).where(and( + eq(circuits.id, expectedCircuit.id), + eq(circuits.circuitListId, expectedCircuit.circuitListId) + )).run(); + if (deleted.changes !== 1) { + throw new Error("Created external circuit changed during history removal."); + } + } + + private loadSectionContext( + database: AppDatabase, + projectId: string, + circuitListId: string, + sectionId: string + ) { + const context = database.select({ + projectId: circuitLists.projectId, + distributionBoardId: circuitLists.distributionBoardId, + category: circuitSections.category, + groupNumber: circuitSections.groupNumber, + }).from(circuitSections) + .innerJoin(circuitLists, eq(circuitLists.id, circuitSections.circuitListId)) + .where(and( + eq(circuitSections.id, sectionId), + eq(circuitSections.circuitListId, circuitListId) + )).get(); + if ( + !context || + context.projectId !== projectId || + context.category === null || + context.groupNumber === null + ) { + throw new Error("Target circuit group does not belong to the project."); + } + return { + distributionBoardId: context.distributionBoardId, + category: context.category, + groupNumber: context.groupNumber, + }; + } + + private loadExpectedObjects( + database: AppDatabase, + projectId: string, + command: ExternalObjectNewCircuitProjectCommand + ) { + return loadExpectedExternalObjectTransitions( + database, + projectId, + command.payload.objects, + "External object changed before new-circuit assignment." + ); + } + + private loadCircuitSnapshot(database: AppDatabase, circuitId: string): CircuitSnapshot | null { + const circuit = database.select().from(circuits).where(eq(circuits.id, circuitId)).get(); + if (!circuit) return null; + const rows = database.select().from(circuitDeviceRows) + .where(eq(circuitDeviceRows.circuitId, circuitId)) + .orderBy(asc(circuitDeviceRows.sortOrder), asc(circuitDeviceRows.id)).all(); + const protection = database.select().from(circuitProtectionDevices) + .where(eq(circuitProtectionDevices.circuitId, circuitId)).get(); + return { + ...circuit, + isReserve: Boolean(circuit.isReserve), + deviceRows: rows.map(toCircuitDeviceRowSnapshot), + protectionDevice: protection ?? null, + }; + } +} + +function toCircuitValues(circuit: CircuitSnapshot) { + const { deviceRows: _deviceRows, protectionDevice: _protectionDevice, ...values } = circuit; + return { ...values, isReserve: circuit.isReserve ? 1 : 0 }; +} diff --git a/src/db/repositories/external-object-new-row-project-command.repository.ts b/src/db/repositories/external-object-new-row-project-command.repository.ts index d2db69c..27146b8 100644 --- a/src/db/repositories/external-object-new-row-project-command.repository.ts +++ b/src/db/repositories/external-object-new-row-project-command.repository.ts @@ -1,4 +1,4 @@ -import { and, eq, inArray, isNull } from "drizzle-orm"; +import { eq } from "drizzle-orm"; import { assertCircuitDeviceRowQuantity } from "../../domain/calculations/circuit-device-row-quantity.js"; import { assertExternalObjectNewRowProjectCommand, @@ -7,17 +7,14 @@ import { invertExternalObjectNewRowProjectCommand, type ExternalObjectNewRowProjectCommand, } from "../../domain/models/external-object-new-row-project-command.model.js"; -import type { CircuitDeviceRowSnapshot } from "../../domain/models/circuit-device-row-structure-project-command.model.js"; import type { ExternalObjectNewRowProjectCommandStore } from "../../domain/ports/external-object-new-row-project-command.store.js"; import { isElectricalPhaseType } from "../../domain/services/project-voltage.service.js"; -import type { ExternalModelObjectSnapshot } from "../../external-model/domain/external-model-contracts.js"; import type { AppDatabase } from "../database-context.js"; import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; import { circuitLists } from "../schema/circuit-lists.js"; import { circuitSections } from "../schema/circuit-sections.js"; import { circuits } from "../schema/circuits.js"; import { externalModelObjects } from "../schema/external-model-objects.js"; -import { externalRoomMappings } from "../schema/external-room-mappings.js"; import { assertCircuitDeviceRowReferencesInProject, toCircuitDeviceRowInsertValues, @@ -25,6 +22,12 @@ import { } from "./circuit-device-row-structure.persistence.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js"; +import { + applyExternalObjectLinks, + assertExternalObjectsCompatibleWithRow, + loadExpectedExternalObjectTransitions, + snapshotsEqual, +} from "./external-object-assignment.persistence.js"; export class ExternalObjectNewRowProjectCommandRepository implements ExternalObjectNewRowProjectCommandStore @@ -68,14 +71,16 @@ export class ExternalObjectNewRowProjectCommandRepository throw new Error("External device-row id already exists."); } const objects = this.loadExpectedObjects(database, projectId, command); - this.assertTargetCompatibility( + assertExternalObjectsCompatibleWithRow({ database, + projectId, row, - context.distributionBoardId, - context.category, - objects.map((object) => object.target), - new Set(command.payload.confirmedConflictObjectIds) - ); + distributionBoardId: context.distributionBoardId, + category: context.category, + assignedObjects: objects.map(({ target }) => target), + allLinkedObjects: objects.map(({ target }) => target), + confirmedConflictObjectIds: new Set(command.payload.confirmedConflictObjectIds), + }); assertCircuitDeviceRowQuantity({ quantity: row.quantity, manualQuantity: 0, @@ -86,7 +91,11 @@ export class ExternalObjectNewRowProjectCommandRepository database.insert(circuitDeviceRows).values(toCircuitDeviceRowInsertValues(row)).run(); this.updateCircuitState(database, projectId, row.circuitId); - this.applyObjectLinks(database, objects); + applyExternalObjectLinks( + database, + objects, + "External object changed during new-row assignment." + ); } private remove( @@ -98,7 +107,7 @@ export class ExternalObjectNewRowProjectCommandRepository this.loadCircuitContext(database, projectId, expectedRow.circuitId); const currentRow = database.select().from(circuitDeviceRows) .where(eq(circuitDeviceRows.id, expectedRow.id)).get(); - if (!currentRow || !same(toCircuitDeviceRowSnapshot(currentRow), expectedRow)) { + if (!currentRow || !snapshotsEqual(toCircuitDeviceRowSnapshot(currentRow), expectedRow)) { throw new Error("Created external device row changed before history removal."); } const objects = this.loadExpectedObjects(database, projectId, command); @@ -111,7 +120,11 @@ export class ExternalObjectNewRowProjectCommandRepository ) { throw new Error("Created external device row has different object links."); } - this.applyObjectLinks(database, objects); + applyExternalObjectLinks( + database, + objects, + "External object changed during new-row assignment." + ); const deleted = database.delete(circuitDeviceRows) .where(eq(circuitDeviceRows.id, expectedRow.id)).run(); if (deleted.changes !== 1) { @@ -144,76 +157,12 @@ export class ExternalObjectNewRowProjectCommandRepository projectId: string, command: ExternalObjectNewRowProjectCommand ) { - const transitions = new Map( - command.payload.objects.map((transition) => [transition.expected.id, transition]) + return loadExpectedExternalObjectTransitions( + database, + projectId, + command.payload.objects, + "External object changed before new-row assignment." ); - const current = database.select().from(externalModelObjects) - .where(inArray(externalModelObjects.id, [...transitions.keys()])).all(); - if (current.length !== transitions.size) { - throw new Error("One or more external objects no longer exist."); - } - for (const object of current) { - const expected = transitions.get(object.id)!.expected; - if (object.projectId !== projectId || !same(toExternalObjectSnapshot(object), expected)) { - throw new Error("External object changed before new-row assignment."); - } - } - return command.payload.objects; - } - - private assertTargetCompatibility( - database: AppDatabase, - row: CircuitDeviceRowSnapshot, - distributionBoardId: string, - category: string, - objects: ExternalModelObjectSnapshot[], - confirmedConflicts: Set - ) { - const roomMappings = database.select().from(externalRoomMappings) - .where(eq(externalRoomMappings.projectId, objects[0]!.projectId)).all(); - const roomIdByMappingId = new Map(roomMappings.map((mapping) => [mapping.id, mapping.roomId])); - const selectionMarkers = new Set( - objects.map((object) => object.acceptedSourceValues.selectionMarker.trim()) - ); - if (selectionMarkers.size > 1) { - throw new Error("External objects with different selection markers require separate rows."); - } - for (const object of objects) { - if (object.distributionBoardId !== distributionBoardId) { - throw new Error("External object distribution does not match target circuit."); - } - if (object.planningValues.category !== category) { - throw new Error("External object category does not match target circuit group."); - } - const objectRoomId = object.externalRoomMappingId === null - ? null - : roomIdByMappingId.get(object.externalRoomMappingId) ?? null; - if (objectRoomId !== row.roomId) { - throw new Error("External object room does not match the new device row."); - } - if (!matchesRowPlanningValues(row, object) && !confirmedConflicts.has(object.id)) { - throw new Error("External object planning values require explicit conflict confirmation."); - } - } - } - - private applyObjectLinks( - database: AppDatabase, - objects: ExternalObjectNewRowProjectCommand["payload"]["objects"] - ) { - for (const { expected, target } of objects) { - const result = database.update(externalModelObjects) - .set({ circuitDeviceRowId: target.circuitDeviceRowId }) - .where(and( - eq(externalModelObjects.id, expected.id), - expected.circuitDeviceRowId === null - ? isNull(externalModelObjects.circuitDeviceRowId) - : eq(externalModelObjects.circuitDeviceRowId, expected.circuitDeviceRowId) - )).run(); - if (result.changes !== 1) { - throw new Error("External object changed during new-row assignment."); - } - } } private updateCircuitState(database: AppDatabase, projectId: string, circuitId: string) { @@ -225,42 +174,3 @@ export class ExternalObjectNewRowProjectCommandRepository updateDerivedCircuitVoltage(database, projectId, circuitId); } } - -function matchesRowPlanningValues( - row: CircuitDeviceRowSnapshot, - object: ExternalModelObjectSnapshot -) { - const planning = object.planningValues; - return ( - row.displayName === (planning.displayName ?? row.displayName) && - row.linkedProjectDeviceId === object.linkedProjectDeviceId && - row.category === planning.category && - row.connectionKind === planning.connectionKind && - (planning.powerPerUnitW === null || row.powerPerUnit === planning.powerPerUnitW / 1000) && - row.simultaneityFactor === planning.simultaneityFactor && - row.cosPhi === planning.cosPhi && - row.costGroup === planning.costGroup && - row.remark === planning.remark - ); -} - -function toExternalObjectSnapshot( - object: typeof externalModelObjects.$inferSelect -): ExternalModelObjectSnapshot { - return { ...object }; -} - -function same(left: unknown, right: unknown) { - return canonicalJson(left) === canonicalJson(right); -} - -function canonicalJson(value: unknown): string { - if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; - if (value !== null && typeof value === "object") { - const record = value as Record; - return `{${Object.keys(record).sort().map((key) => - `${JSON.stringify(key)}:${canonicalJson(record[key])}` - ).join(",")}}`; - } - return JSON.stringify(value); -} diff --git a/src/db/repositories/external-object-row-assignment-project-command.repository.ts b/src/db/repositories/external-object-row-assignment-project-command.repository.ts index dcf33b1..b6e8123 100644 --- a/src/db/repositories/external-object-row-assignment-project-command.repository.ts +++ b/src/db/repositories/external-object-row-assignment-project-command.repository.ts @@ -6,18 +6,22 @@ import { assertExternalObjectRowAssignmentProjectCommand, invertExternalObjectRowAssignmentProjectCommand, } from "../../domain/models/external-object-row-assignment-project-command.model.js"; -import type { CircuitDeviceRowSnapshot } from "../../domain/models/circuit-device-row-structure-project-command.model.js"; import type { ExternalObjectRowAssignmentProjectCommandStore } from "../../domain/ports/external-object-row-assignment-project-command.store.js"; -import type { ExternalModelObjectSnapshot } from "../../external-model/domain/external-model-contracts.js"; import type { AppDatabase } from "../database-context.js"; import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; import { circuitLists } from "../schema/circuit-lists.js"; import { circuitSections } from "../schema/circuit-sections.js"; import { circuits } from "../schema/circuits.js"; import { externalModelObjects } from "../schema/external-model-objects.js"; -import { externalRoomMappings } from "../schema/external-room-mappings.js"; import { toCircuitDeviceRowSnapshot } from "./circuit-device-row-structure.persistence.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { + applyExternalObjectLinks, + assertExternalObjectsCompatibleWithRow, + loadExpectedExternalObjectTransitions, + snapshotsEqual, + toExternalObjectSnapshot, +} from "./external-object-assignment.persistence.js"; export class ExternalObjectRowAssignmentProjectCommandRepository implements ExternalObjectRowAssignmentProjectCommandStore @@ -44,29 +48,23 @@ export class ExternalObjectRowAssignmentProjectCommandRepository command.payload.objects.map((transition) => [transition.expected.id, transition]) ); const rowIds = [...rowTransitions.keys()]; - const objectIds = [...objectTransitions.keys()]; const currentRows = database.select().from(circuitDeviceRows) .where(inArray(circuitDeviceRows.id, rowIds)).all(); - const currentObjects = database.select().from(externalModelObjects) - .where(inArray(externalModelObjects.id, objectIds)).all(); - if (currentRows.length !== rowIds.length || currentObjects.length !== objectIds.length) { + if (currentRows.length !== rowIds.length) { throw new Error("External object assignment target no longer exists."); } for (const row of currentRows) { const expected = rowTransitions.get(row.id)!.expected; - if (!same(toCircuitDeviceRowSnapshot(row), expected)) { + if (!snapshotsEqual(toCircuitDeviceRowSnapshot(row), expected)) { throw new Error("Circuit device row changed before external object assignment."); } } - for (const object of currentObjects) { - const expected = objectTransitions.get(object.id)!.expected; - if (!same(toExternalObjectSnapshot(object), expected)) { - throw new Error("External object changed before row assignment."); - } - if (object.projectId !== projectId) { - throw new Error("External object belongs to another project."); - } - } + loadExpectedExternalObjectTransitions( + database, + projectId, + command.payload.objects, + "External object changed before row assignment." + ); const rowContexts = database .select({ @@ -83,7 +81,9 @@ export class ExternalObjectRowAssignmentProjectCommandRepository .all(); if ( rowContexts.length !== rowIds.length || - rowContexts.some((context) => context.projectId !== projectId) + rowContexts.some((context) => + context.projectId !== projectId || context.category === null + ) ) { throw new Error("Circuit device row belongs to another project."); } @@ -95,47 +95,26 @@ export class ExternalObjectRowAssignmentProjectCommandRepository const targetObjects = allProjectObjects.map((object) => objectTransitions.get(object.id)?.target ?? object ); - const mappings = database.select().from(externalRoomMappings) - .where(eq(externalRoomMappings.projectId, projectId)).all(); - const roomIdByMappingId = new Map(mappings.map((mapping) => [mapping.id, mapping.roomId])); const confirmedConflicts = new Set(command.payload.confirmedConflictObjectIds); - for (const transition of command.payload.objects) { - const object = transition.target; - if (object.circuitDeviceRowId === null) continue; - const row = rowTransitions.get(object.circuitDeviceRowId)?.target; - const context = contextByRowId.get(object.circuitDeviceRowId); - if (!row || !context) throw new Error("External object target row is incomplete."); - if ( - object.distributionBoardId === null || - object.distributionBoardId !== context.distributionBoardId - ) { - throw new Error("External object distribution does not match target row."); - } - if (object.planningValues.category !== context.category) { - throw new Error("External object category does not match target circuit group."); - } - const objectRoomId = object.externalRoomMappingId === null - ? null - : roomIdByMappingId.get(object.externalRoomMappingId) ?? null; - if (objectRoomId !== row.roomId) { - throw new Error("External object room does not match target row."); - } - if (!matchesRowPlanningValues(row, object) && !confirmedConflicts.has(object.id)) { - throw new Error("External object planning values require explicit conflict confirmation."); - } - } - for (const transition of command.payload.rows) { const linkedObjects = targetObjects.filter( (object) => object.circuitDeviceRowId === transition.target.id ); - const selectionMarkers = new Set( - linkedObjects.map((object) => object.acceptedSourceValues.selectionMarker.trim()) - ); - if (selectionMarkers.size > 1) { - throw new Error("External objects with different selection markers require separate rows."); - } + const assignedObjects = command.payload.objects + .map(({ target }) => target) + .filter((object) => object.circuitDeviceRowId === transition.target.id); + const context = contextByRowId.get(transition.target.id)!; + assertExternalObjectsCompatibleWithRow({ + database, + projectId, + row: transition.target, + distributionBoardId: context.distributionBoardId, + category: context.category!, + assignedObjects, + allLinkedObjects: linkedObjects, + confirmedConflictObjectIds: confirmedConflicts, + }); assertCircuitDeviceRowQuantity({ quantity: transition.target.quantity, manualQuantity: transition.target.manualQuantity ?? transition.target.quantity, @@ -155,50 +134,10 @@ export class ExternalObjectRowAssignmentProjectCommandRepository )).run(); if (result.changes !== 1) throw new Error("Circuit device row changed during assignment."); } - for (const transition of command.payload.objects) { - const result = database.update(externalModelObjects) - .set({ circuitDeviceRowId: transition.target.circuitDeviceRowId }) - .where(eq(externalModelObjects.id, transition.expected.id)).run(); - if (result.changes !== 1) throw new Error("External object changed during assignment."); - } + applyExternalObjectLinks( + database, + command.payload.objects, + "External object changed during assignment." + ); } } - -function matchesRowPlanningValues( - row: CircuitDeviceRowSnapshot, - object: ExternalModelObjectSnapshot -) { - const planning = object.planningValues; - return ( - row.displayName === (planning.displayName ?? row.displayName) && - row.linkedProjectDeviceId === object.linkedProjectDeviceId && - row.category === planning.category && - row.connectionKind === planning.connectionKind && - (planning.powerPerUnitW === null || row.powerPerUnit === planning.powerPerUnitW / 1000) && - row.simultaneityFactor === planning.simultaneityFactor && - row.cosPhi === planning.cosPhi && - row.costGroup === planning.costGroup && - row.remark === planning.remark - ); -} - -function toExternalObjectSnapshot( - object: typeof externalModelObjects.$inferSelect -): ExternalModelObjectSnapshot { - return { ...object }; -} - -function same(left: unknown, right: unknown) { - return canonicalJson(left) === canonicalJson(right); -} - -function canonicalJson(value: unknown): string { - if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; - if (value !== null && typeof value === "object") { - const record = value as Record; - return `{${Object.keys(record).sort().map((key) => - `${JSON.stringify(key)}:${canonicalJson(record[key])}` - ).join(",")}}`; - } - return JSON.stringify(value); -} diff --git a/src/domain/models/external-object-new-circuit-project-command.model.ts b/src/domain/models/external-object-new-circuit-project-command.model.ts new file mode 100644 index 0000000..b34a449 --- /dev/null +++ b/src/domain/models/external-object-new-circuit-project-command.model.ts @@ -0,0 +1,168 @@ +import type { ExternalModelObjectSnapshot } from "../../external-model/domain/external-model-contracts.js"; +import { + assertCircuitInsertProjectCommand, + circuitInsertCommandType, + circuitStructureCommandSchemaVersion, + type CircuitSnapshot, +} from "./circuit-structure-project-command.model.js"; +import type { SerializedProjectCommand } from "./project-command.model.js"; +import { parseExternalModelStateSnapshot } from "./project-state-snapshot.model.js"; + +export const externalObjectAssignToNewCircuitCommandType = + "external-object.assign-to-new-circuit" as const; +export const externalObjectDeleteCreatedCircuitCommandType = + "external-object.unassign-and-delete-created-circuit" as const; +export const externalObjectNewCircuitCommandSchemaVersion = 1 as const; + +export interface ExternalObjectNewCircuitPayload { + circuit: CircuitSnapshot; + objects: Array<{ + expected: ExternalModelObjectSnapshot; + target: ExternalModelObjectSnapshot; + }>; + confirmedConflictObjectIds: string[]; +} + +export interface ExternalObjectNewCircuitProjectCommand + extends SerializedProjectCommand { + schemaVersion: typeof externalObjectNewCircuitCommandSchemaVersion; + type: + | typeof externalObjectAssignToNewCircuitCommandType + | typeof externalObjectDeleteCreatedCircuitCommandType; +} + +export function createExternalObjectNewCircuitProjectCommand( + type: ExternalObjectNewCircuitProjectCommand["type"], + payload: ExternalObjectNewCircuitPayload +): ExternalObjectNewCircuitProjectCommand { + const row = payload.circuit.deviceRows[0]; + const command: ExternalObjectNewCircuitProjectCommand = { + schemaVersion: externalObjectNewCircuitCommandSchemaVersion, + type, + payload: { + circuit: row + ? { + ...payload.circuit, + deviceRows: [{ + ...row, + manualQuantity: row.manualQuantity ?? row.quantity, + }], + } + : payload.circuit, + objects: payload.objects, + confirmedConflictObjectIds: [...payload.confirmedConflictObjectIds], + }, + }; + assertExternalObjectNewCircuitProjectCommand(command); + return command; +} + +export function invertExternalObjectNewCircuitProjectCommand( + command: ExternalObjectNewCircuitProjectCommand +) { + return createExternalObjectNewCircuitProjectCommand( + command.type === externalObjectAssignToNewCircuitCommandType + ? externalObjectDeleteCreatedCircuitCommandType + : externalObjectAssignToNewCircuitCommandType, + { + circuit: command.payload.circuit, + objects: command.payload.objects.map(({ expected, target }) => ({ + expected: target, + target: expected, + })), + confirmedConflictObjectIds: command.payload.confirmedConflictObjectIds, + } + ); +} + +export function assertExternalObjectNewCircuitProjectCommand( + command: SerializedProjectCommand +): asserts command is ExternalObjectNewCircuitProjectCommand { + if ( + command.schemaVersion !== externalObjectNewCircuitCommandSchemaVersion || + (command.type !== externalObjectAssignToNewCircuitCommandType && + command.type !== externalObjectDeleteCreatedCircuitCommandType) || + !isRecord(command.payload) || + !Array.isArray(command.payload.objects) || + command.payload.objects.length === 0 || + !Array.isArray(command.payload.confirmedConflictObjectIds) + ) { + throw new Error("Unsupported external object new-circuit command."); + } + assertCircuitInsertProjectCommand({ + schemaVersion: circuitStructureCommandSchemaVersion, + type: circuitInsertCommandType, + payload: { circuit: command.payload.circuit }, + }); + const circuit = command.payload.circuit as CircuitSnapshot; + if ( + circuit.deviceRows.length !== 1 || + circuit.deviceRows[0]!.manualQuantity !== 0 || + circuit.protectionDevice === undefined || + circuit.protectionDevice === null + ) { + throw new Error( + "An external circuit must contain one external-only row and protection device." + ); + } + const rowId = circuit.deviceRows[0]!.id; + const objectIds = new Set(); + for (const transition of command.payload.objects) { + if (!isRecord(transition)) throw new Error("Invalid external object transition."); + const expected = parseObject(transition.expected); + const target = parseObject(transition.target); + if (expected.id !== target.id || objectIds.has(expected.id)) { + throw new Error("New-circuit assignment contains mismatched or duplicate objects."); + } + objectIds.add(expected.id); + const assigning = command.type === externalObjectAssignToNewCircuitCommandType; + if ( + (assigning && (expected.circuitDeviceRowId !== null || target.circuitDeviceRowId !== rowId)) || + (!assigning && (expected.circuitDeviceRowId !== rowId || target.circuitDeviceRowId !== null)) + ) { + throw new Error("External object links do not match the new-circuit action."); + } + assertOnlyAssignmentChanged(expected, target); + } + const confirmedIds = new Set(); + for (const objectId of command.payload.confirmedConflictObjectIds) { + if (typeof objectId !== "string" || !objectIds.has(objectId) || confirmedIds.has(objectId)) { + throw new Error("New-circuit assignment contains an invalid conflict confirmation."); + } + confirmedIds.add(objectId); + } +} + +function parseObject(value: unknown): ExternalModelObjectSnapshot { + return parseExternalModelStateSnapshot({ + source: null, + importBatches: [], + roomMappings: [], + objects: [value], + }).objects[0]!; +} + +function assertOnlyAssignmentChanged( + expected: ExternalModelObjectSnapshot, + target: ExternalModelObjectSnapshot +) { + const expectedRest = { ...expected, circuitDeviceRowId: null }; + const targetRest = { ...target, circuitDeviceRowId: null }; + if (canonicalJson(expectedRest) !== canonicalJson(targetRest)) { + throw new Error("New-circuit assignment may only change the external object row link."); + } +} + +function canonicalJson(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; + if (isRecord(value)) { + return `{${Object.keys(value).sort().map((key) => + `${JSON.stringify(key)}:${canonicalJson(value[key])}` + ).join(",")}}`; + } + return JSON.stringify(value); +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} diff --git a/src/domain/ports/external-object-new-circuit-project-command.store.ts b/src/domain/ports/external-object-new-circuit-project-command.store.ts new file mode 100644 index 0000000..efd13ba --- /dev/null +++ b/src/domain/ports/external-object-new-circuit-project-command.store.ts @@ -0,0 +1,19 @@ +import type { ExternalObjectNewCircuitProjectCommand } from "../models/external-object-new-circuit-project-command.model.js"; +import type { AppendedProjectRevision, ProjectRevisionSource } from "./project-revision.store.js"; + +export interface ExecuteExternalObjectNewCircuitCommandInput { + projectId: string; + expectedRevision: number; + source: ProjectRevisionSource; + description?: string; + actorId?: string; + historyTargetChangeSetId?: string; + command: ExternalObjectNewCircuitProjectCommand; +} + +export interface ExternalObjectNewCircuitProjectCommandStore { + execute(input: ExecuteExternalObjectNewCircuitCommandInput): { + revision: AppendedProjectRevision; + inverse: ExternalObjectNewCircuitProjectCommand; + }; +} diff --git a/src/domain/services/project-command.service.ts b/src/domain/services/project-command.service.ts index be66428..bee31a3 100644 --- a/src/domain/services/project-command.service.ts +++ b/src/domain/services/project-command.service.ts @@ -182,6 +182,12 @@ import { externalObjectDeleteCreatedRowCommandType, } from "../models/external-object-new-row-project-command.model.js"; import type { ExternalObjectNewRowProjectCommandStore } from "../ports/external-object-new-row-project-command.store.js"; +import { + assertExternalObjectNewCircuitProjectCommand, + externalObjectAssignToNewCircuitCommandType, + externalObjectDeleteCreatedCircuitCommandType, +} from "../models/external-object-new-circuit-project-command.model.js"; +import type { ExternalObjectNewCircuitProjectCommandStore } from "../ports/external-object-new-circuit-project-command.store.js"; interface DispatchProjectCommandInput { projectId: string; @@ -220,7 +226,8 @@ export class ProjectCommandService implements ProjectCommandExecutor { private readonly externalCsvConfigurationStore?: ExternalCsvConfigurationProjectCommandStore, private readonly externalInitialImportStore?: ExternalInitialImportProjectCommandStore, private readonly externalObjectRowAssignmentStore?: ExternalObjectRowAssignmentProjectCommandStore, - private readonly externalObjectNewRowStore?: ExternalObjectNewRowProjectCommandStore + private readonly externalObjectNewRowStore?: ExternalObjectNewRowProjectCommandStore, + private readonly externalObjectNewCircuitStore?: ExternalObjectNewCircuitProjectCommandStore ) {} executeUser( @@ -618,6 +625,17 @@ export class ProjectCommandService implements ProjectCommandExecutor { command: input.command, }).revision; } + case externalObjectAssignToNewCircuitCommandType: + case externalObjectDeleteCreatedCircuitCommandType: { + assertExternalObjectNewCircuitProjectCommand(input.command); + if (!this.externalObjectNewCircuitStore) { + throw new Error("External object new-circuit store is not available."); + } + return this.externalObjectNewCircuitStore.execute({ + ...input, + command: input.command, + }).revision; + } case projectStateRestoreCommandType: { assertProjectStateRestoreCommand(input.command); return this.projectStateRestoreStore.execute({ diff --git a/src/server/composition/project-command-stores.ts b/src/server/composition/project-command-stores.ts index 9e63ed6..79a4a9f 100644 --- a/src/server/composition/project-command-stores.ts +++ b/src/server/composition/project-command-stores.ts @@ -26,6 +26,7 @@ import { ExternalCsvConfigurationProjectCommandRepository } from "../../db/repos import { ExternalInitialImportProjectCommandRepository } from "../../db/repositories/external-initial-import-project-command.repository.js"; import { ExternalObjectRowAssignmentProjectCommandRepository } from "../../db/repositories/external-object-row-assignment-project-command.repository.js"; import { ExternalObjectNewRowProjectCommandRepository } from "../../db/repositories/external-object-new-row-project-command.repository.js"; +import { ExternalObjectNewCircuitProjectCommandRepository } from "../../db/repositories/external-object-new-circuit-project-command.repository.js"; export const circuitProjectCommandStore = new CircuitProjectCommandRepository(db); export const circuitDeviceRowProjectCommandStore = @@ -77,6 +78,8 @@ export const externalObjectRowAssignmentProjectCommandStore = new ExternalObjectRowAssignmentProjectCommandRepository(db); export const externalObjectNewRowProjectCommandStore = new ExternalObjectNewRowProjectCommandRepository(db); +export const externalObjectNewCircuitProjectCommandStore = + new ExternalObjectNewCircuitProjectCommandRepository(db); export const projectCommandService = new ProjectCommandService( circuitProjectCommandStore, circuitDeviceRowProjectCommandStore, @@ -103,5 +106,6 @@ export const projectCommandService = new ProjectCommandService( externalCsvConfigurationProjectCommandStore, externalInitialImportProjectCommandStore, externalObjectRowAssignmentProjectCommandStore, - externalObjectNewRowProjectCommandStore + externalObjectNewRowProjectCommandStore, + externalObjectNewCircuitProjectCommandStore ); diff --git a/tests/external-object-row-assignment-project-command.repository.test.ts b/tests/external-object-row-assignment-project-command.repository.test.ts index 4e127f1..da8551e 100644 --- a/tests/external-object-row-assignment-project-command.repository.test.ts +++ b/tests/external-object-row-assignment-project-command.repository.test.ts @@ -6,11 +6,13 @@ import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import { createDatabaseContext, type AppDatabase } from "../src/db/database-context.js"; import { ExternalObjectRowAssignmentProjectCommandRepository } from "../src/db/repositories/external-object-row-assignment-project-command.repository.js"; import { ExternalObjectNewRowProjectCommandRepository } from "../src/db/repositories/external-object-new-row-project-command.repository.js"; +import { ExternalObjectNewCircuitProjectCommandRepository } from "../src/db/repositories/external-object-new-circuit-project-command.repository.js"; import { toCircuitDeviceRowSnapshot } from "../src/db/repositories/circuit-device-row-structure.persistence.js"; import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js"; import { circuitLists } from "../src/db/schema/circuit-lists.js"; import { circuitSections } from "../src/db/schema/circuit-sections.js"; import { circuits } from "../src/db/schema/circuits.js"; +import { circuitProtectionDevices } from "../src/db/schema/circuit-protection-devices.js"; import { distributionBoards } from "../src/db/schema/distribution-boards.js"; import { externalImportBatches } from "../src/db/schema/external-import-batches.js"; import { externalModelObjects } from "../src/db/schema/external-model-objects.js"; @@ -24,6 +26,10 @@ import { createExternalObjectNewRowProjectCommand, externalObjectAssignToNewRowCommandType, } from "../src/domain/models/external-object-new-row-project-command.model.js"; +import { + createExternalObjectNewCircuitProjectCommand, + externalObjectAssignToNewCircuitCommandType, +} from "../src/domain/models/external-object-new-circuit-project-command.model.js"; import type { ExternalModelObjectSnapshot } from "../src/external-model/domain/external-model-contracts.js"; import { externalCsvTestConfiguration } from "./fixtures/revit-csv-fixtures.js"; @@ -376,3 +382,192 @@ describe("external object new-row project command", () => { } }); }); + +function newExternalCircuit(database: AppDatabase) { + const sourceRow = row(database, "row-2"); + return { + id: "circuit-new", + circuitListId: "list-1", + sectionId: "section-1", + equipmentIdentifier: "-2F1.3", + displayName: "Steckdose", + sortOrder: 30, + cableType: null, + cableCrossSection: null, + cableLength: null, + rcdAssignment: null, + terminalDesignation: null, + voltage: 230, + controlRequirement: null, + status: null, + isReserve: false, + remark: null, + deviceRows: [{ + ...sourceRow, + id: "row-new", + circuitId: "circuit-new", + quantity: 2, + manualQuantity: 0, + }], + protectionDevice: { + circuitId: "circuit-new", + type: "FI_LS" as const, + ratedCurrentA: 16, + fuseUtilizationCategory: null, + tripCharacteristic: "B" as const, + rcdType: "A" as const, + ratedResidualCurrentMa: 30, + }, + }; +} + +describe("external object new-circuit project command", () => { + it("creates circuit, protection, row and links as one undoable revision", () => { + const context = createFixture(null); + try { + const object = objectSnapshot(null); + const circuit = newExternalCircuit(context.db); + const command = createExternalObjectNewCircuitProjectCommand( + externalObjectAssignToNewCircuitCommandType, + { + circuit, + objects: [{ + expected: object, + target: { ...object, circuitDeviceRowId: "row-new" }, + }], + confirmedConflictObjectIds: [], + } + ); + const repository = new ExternalObjectNewCircuitProjectCommandRepository(context.db); + const inserted = repository.execute({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + command, + }); + assert.equal( + context.db.select().from(circuits).where(eq(circuits.id, "circuit-new")).get()! + .equipmentIdentifier, + "-2F1.3" + ); + assert.equal(row(context.db, "row-new").manualQuantity, 0); + assert.equal( + context.db.select().from(circuitProtectionDevices) + .where(eq(circuitProtectionDevices.circuitId, "circuit-new")).get()!.type, + "FI_LS" + ); + assert.equal( + context.db.select().from(externalModelObjects).get()!.circuitDeviceRowId, + "row-new" + ); + assert.throws( + () => repository.execute({ + projectId: "project-1", + expectedRevision: 1, + source: "user", + command: inserted.inverse, + }), + /only be removed through project history/ + ); + + const undone = repository.execute({ + projectId: "project-1", + expectedRevision: 1, + source: "undo", + historyTargetChangeSetId: inserted.revision.changeSetId, + command: inserted.inverse, + }); + assert.equal( + context.db.select().from(circuits).where(eq(circuits.id, "circuit-new")).get(), + undefined + ); + assert.equal(context.db.select().from(externalModelObjects).get()!.circuitDeviceRowId, null); + + repository.execute({ + projectId: "project-1", + expectedRevision: 2, + source: "redo", + historyTargetChangeSetId: inserted.revision.changeSetId, + command: undone.inverse, + }); + assert.equal( + context.db.select().from(circuits).where(eq(circuits.id, "circuit-new")).get()! + .equipmentIdentifier, + "-2F1.3" + ); + } finally { + context.close(); + } + }); + + it("rejects a preplanned BMK that does not belong to the target group", () => { + const context = createFixture(null); + try { + const object = objectSnapshot(null); + const circuit = { + ...newExternalCircuit(context.db), + equipmentIdentifier: "-1F1.3", + }; + assert.throws( + () => new ExternalObjectNewCircuitProjectCommandRepository(context.db).execute({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + command: createExternalObjectNewCircuitProjectCommand( + externalObjectAssignToNewCircuitCommandType, + { + circuit, + objects: [{ + expected: object, + target: { ...object, circuitDeviceRowId: "row-new" }, + }], + confirmedConflictObjectIds: [], + } + ), + }), + /equipment identifier does not match target group/ + ); + assert.equal(context.db.select().from(externalModelObjects).get()!.circuitDeviceRowId, null); + } finally { + context.close(); + } + }); + + it("rolls back the complete circuit subtree when history persistence fails", () => { + const context = createFixture(null); + try { + context.sqlite.exec(` + CREATE TRIGGER fail_new_circuit_history + BEFORE INSERT ON project_change_sets + BEGIN SELECT RAISE(ABORT, 'late history failure'); END; + `); + const object = objectSnapshot(null); + assert.throws( + () => new ExternalObjectNewCircuitProjectCommandRepository(context.db).execute({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + command: createExternalObjectNewCircuitProjectCommand( + externalObjectAssignToNewCircuitCommandType, + { + circuit: newExternalCircuit(context.db), + objects: [{ + expected: object, + target: { ...object, circuitDeviceRowId: "row-new" }, + }], + confirmedConflictObjectIds: [], + } + ), + }), + /late history failure/ + ); + assert.equal( + context.db.select().from(circuits).where(eq(circuits.id, "circuit-new")).get(), + undefined + ); + assert.equal(context.db.select().from(externalModelObjects).get()!.circuitDeviceRowId, null); + } finally { + context.close(); + } + }); +}); From 74bea1a3c9550a53e7c92ea471e9fedbbf4adc7c Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 2 Aug 2026 18:59:01 +0200 Subject: [PATCH 48/59] Add Revit circuit list object feed --- AGENTS.md | 5 + docs/circuit-list-editor-api.md | 6 + docs/current-architecture.md | 7 ++ .../spec/revit-csv-phase-14-audit-and-plan.md | 5 + .../external-circuit-list-objects.ts | 88 +++++++++++++++ src/frontend/types.ts | 26 +++++ src/frontend/utils/api.ts | 10 ++ .../controllers/external-csv.controller.ts | 23 ++++ src/server/routes/project.routes.ts | 5 + tests/external-circuit-list-objects.test.ts | 105 ++++++++++++++++++ tests/revit-csv-frontend-api.test.ts | 7 ++ 11 files changed, 287 insertions(+) create mode 100644 src/external-model/application/external-circuit-list-objects.ts create mode 100644 tests/external-circuit-list-objects.test.ts diff --git a/AGENTS.md b/AGENTS.md index 0b7077d..f1a509d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -305,6 +305,11 @@ protection and its external object links. Its history-only inverse requires the complete unchanged circuit snapshot and exact link set. Redo never recalculates the BMK. All three external assignment stores share `external-object-assignment.persistence.ts` for compatibility and link safety. +The read-only +`GET /api/projects/:projectId/circuit-lists/:circuitListId/external-objects` +projection resolves the board through the owned CircuitList and returns only +that board's compact external objects, unassigned first. It exposes no original +CSV bytes or command payloads and returns an empty list before optional import. Confirmed initial state is written only through `external-import.apply-initial`. The command rechecks configuration version, original-byte SHA-256, parsed matrix, complete IFCGUID/source values, explicit diff --git a/docs/circuit-list-editor-api.md b/docs/circuit-list-editor-api.md index 70b5bb5..4ed696b 100644 --- a/docs/circuit-list-editor-api.md +++ b/docs/circuit-list-editor-api.md @@ -414,6 +414,12 @@ project device never triggers synchronization implicitly. - reparses the file, blocks unknown family/type values and executes `external-import.apply-initial` as one undoable project revision - creates no Circuit or CircuitDeviceRow +- `GET /projects/:projectId/circuit-lists/:circuitListId/external-objects` + - resolves the circuit list's distribution board server-side + - returns only external objects assigned to that board, with unassigned + objects first, their source room/marker values and current row-link status + - returns an empty list when the optional Revit import does not exist + - is read-only and exposes no stored command payloads or import bytes Parser errors expose stable codes such as `header-not-found`, `ambiguous-header`, `invalid-ifc-guid` and `duplicate-ifc-guid`. The confirmed diff --git a/docs/current-architecture.md b/docs/current-architecture.md index a0282de..32f6814 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -570,6 +570,13 @@ dieselben IDs, dasselbe BMK und denselben Schutzsnapshot. Die drei Zuordnungsadapter teilen sich `external-object-assignment.persistence.ts` für Snapshotvergleich, Kompatibilitätsregeln und link-sichere Updates. +`GET /api/projects/:projectId/circuit-lists/:circuitListId/external-objects` +liefert die verteilungsbezogene Read-Projektion für den späteren Editor-Drawer. +Die CircuitList bestimmt den Verteiler serverseitig; die Antwort enthält nur +dessen externe Objekte, sortiert unzugeordnet vor zugeordnet, sowie kompakte +Quell-, Raum-, Planungs- und Linkangaben. Originalbytes und vollständige +Command-Snapshots werden nicht an diese Oberfläche gegeben. Ohne optionalen +Revit-Import ist die Projektion leer. `GET` und `PUT /api/projects/:projectId/external-csv/configuration` lesen oder ändern die Konfiguration; der PUT plant Identität und nächsten Konfigurationsstand serverseitig und verwendet den typisierten Command. diff --git a/docs/spec/revit-csv-phase-14-audit-and-plan.md b/docs/spec/revit-csv-phase-14-audit-and-plan.md index b721c3a..be50eb1 100644 --- a/docs/spec/revit-csv-phase-14-audit-and-plan.md +++ b/docs/spec/revit-csv-phase-14-audit-and-plan.md @@ -338,6 +338,11 @@ Kombination aus Originalbytes und Matrix als Entscheidungen bestätigt. BMK und UUIDs bleiben bei Redo unverändert. Damit ist die persistente Command-Grundlage dieses Schritts abgeschlossen; API-Planung und UI folgen. 3. Verteilungsbezogenen Drawer, Filter und Vorschau der Mengenwirkung ergänzen. + **Begonnen:** Die reine Read-Projektion und der projekt-/listenbezogene + GET-Endpunkt liefern ausschließlich Objekte des zugehörigen Verteilers, + unzugeordnete Objekte zuerst, einschließlich Quellraum, Selektionsmarker, + Planungswerten und aktuellem Row-Link. API-Client und DTOs sind vorhanden; + Drawer, Filter und Mengenwirkung folgen. 4. Einzel- und Mehrfach-Drag-and-drop samt Warnungen, Undo/Redo und Reload testen. diff --git a/src/external-model/application/external-circuit-list-objects.ts b/src/external-model/application/external-circuit-list-objects.ts new file mode 100644 index 0000000..43f57c5 --- /dev/null +++ b/src/external-model/application/external-circuit-list-objects.ts @@ -0,0 +1,88 @@ +import type { + ExternalModelStateSnapshot, +} from "../domain/external-model-contracts.js"; + +export interface ExternalCircuitListObjectDto { + id: string; + ifcGuid: string; + selectionMarker: string; + familyAndType: string; + sourceCircuitIdentifier: string; + displayName: string | null; + category: string | null; + connectionKind: string | null; + effectiveQuantity: number; + powerPerUnitW: number | null; + sourceRoomNumber: string; + sourceRoomName: string; + roomId: string | null; + linkedProjectDeviceId: string | null; + circuitDeviceRowId: string | null; + presenceStatus: "present" | "missing"; +} + +export interface ExternalCircuitListObjectsDto { + sourceName: string | null; + objectCount: number; + unassignedObjectCount: number; + objects: ExternalCircuitListObjectDto[]; +} + +export function createExternalCircuitListObjects(input: { + state: ExternalModelStateSnapshot; + distributionBoardId: string; +}): ExternalCircuitListObjectsDto { + const mappingById = new Map( + input.state.roomMappings.map((mapping) => [mapping.id, mapping]) + ); + const objects = input.state.objects + .filter((object) => object.distributionBoardId === input.distributionBoardId) + .map((object) => { + const mapping = object.externalRoomMappingId === null + ? null + : mappingById.get(object.externalRoomMappingId) ?? null; + return { + id: object.id, + ifcGuid: object.ifcGuid, + selectionMarker: object.acceptedSourceValues.selectionMarker, + familyAndType: object.acceptedSourceValues.familyAndType, + sourceCircuitIdentifier: object.acceptedSourceValues.circuitIdentifier, + displayName: object.planningValues.displayName, + category: object.planningValues.category, + connectionKind: object.planningValues.connectionKind, + effectiveQuantity: object.planningValues.effectiveQuantity, + powerPerUnitW: object.planningValues.powerPerUnitW, + sourceRoomNumber: + mapping?.sourceRoomNumber ?? object.acceptedSourceValues.roomNumber, + sourceRoomName: + mapping?.sourceRoomName ?? object.acceptedSourceValues.roomName, + roomId: mapping?.roomId ?? null, + linkedProjectDeviceId: object.linkedProjectDeviceId, + circuitDeviceRowId: object.circuitDeviceRowId, + presenceStatus: object.presenceStatus, + } satisfies ExternalCircuitListObjectDto; + }) + .sort(compareExternalObjects); + return { + sourceName: input.state.source?.name ?? null, + objectCount: objects.length, + unassignedObjectCount: objects.filter( + (object) => object.circuitDeviceRowId === null + ).length, + objects, + }; +} + +function compareExternalObjects( + left: ExternalCircuitListObjectDto, + right: ExternalCircuitListObjectDto +) { + const assignmentOrder = Number(left.circuitDeviceRowId !== null) - + Number(right.circuitDeviceRowId !== null); + if (assignmentOrder !== 0) return assignmentOrder; + return left.sourceRoomNumber.localeCompare(right.sourceRoomNumber, "de", { + numeric: true, + }) || left.selectionMarker.localeCompare(right.selectionMarker, "de", { + numeric: true, + }) || left.ifcGuid.localeCompare(right.ifcGuid); +} diff --git a/src/frontend/types.ts b/src/frontend/types.ts index 6effc53..2bce5d9 100644 --- a/src/frontend/types.ts +++ b/src/frontend/types.ts @@ -428,6 +428,32 @@ export interface CircuitTreeResponseDto { footerComponents: CircuitTreeComponentDto[]; } +export interface ExternalCircuitListObjectDto { + id: string; + ifcGuid: string; + selectionMarker: string; + familyAndType: string; + sourceCircuitIdentifier: string; + displayName: string | null; + category: string | null; + connectionKind: string | null; + effectiveQuantity: number; + powerPerUnitW: number | null; + sourceRoomNumber: string; + sourceRoomName: string; + roomId: string | null; + linkedProjectDeviceId: string | null; + circuitDeviceRowId: string | null; + presenceStatus: "present" | "missing"; +} + +export interface ExternalCircuitListObjectsDto { + sourceName: string | null; + objectCount: number; + unassignedObjectCount: number; + objects: ExternalCircuitListObjectDto[]; +} + export interface CreateCircuitInputDto { sectionId: string; equipmentIdentifier: string; diff --git a/src/frontend/utils/api.ts b/src/frontend/utils/api.ts index fcc8e94..34af668 100644 --- a/src/frontend/utils/api.ts +++ b/src/frontend/utils/api.ts @@ -23,6 +23,7 @@ import type { ProjectRoomCommandResultDto, ProjectDeviceSyncCommandResultDto, ProjectDeviceSyncPreviewDto, + ExternalCircuitListObjectsDto, ProjectDto, RoomDto, CircuitTreeResponseDto, @@ -346,6 +347,15 @@ export function getExternalCsvConfiguration(projectId: string) { ); } +export function getExternalCircuitListObjects( + projectId: string, + circuitListId: string +) { + return request( + `/api/projects/${projectId}/circuit-lists/${circuitListId}/external-objects` + ); +} + export function updateExternalCsvConfiguration( projectId: string, expectedRevision: number, diff --git a/src/server/controllers/external-csv.controller.ts b/src/server/controllers/external-csv.controller.ts index 986d7de..4862d9b 100644 --- a/src/server/controllers/external-csv.controller.ts +++ b/src/server/controllers/external-csv.controller.ts @@ -5,6 +5,7 @@ import { ExternalCsvParseError } from "../../external-model/csv/external-csv-tra import { createExternalCsvPreview } from "../../external-model/application/external-csv-preview.js"; import { createExternalInitialImportPlan } from "../../external-model/application/external-initial-import-plan.js"; import { buildExternalInitialImportTarget } from "../../external-model/application/external-initial-import-target.js"; +import { createExternalCircuitListObjects } from "../../external-model/application/external-circuit-list-objects.js"; import { parseExternalCsv } from "../../external-model/csv/external-csv-transport.js"; import { createEmptyExternalModelState, @@ -24,6 +25,7 @@ import { floorRepository, projectDeviceRepository, roomRepository, + circuitListRepository, } from "../composition/application-repositories.js"; import { projectCommandService } from "../composition/project-command-stores.js"; import { respondWithProjectCommandError } from "./project-command.controller.js"; @@ -38,6 +40,27 @@ export function getExternalCsvConfiguration(req: Request, res: Response) { return res.json({ configuration: result.configuration }); } +export async function getExternalCircuitListObjects(req: Request, res: Response) { + const projectId = getProjectId(req, res); + if (!projectId) return; + const { circuitListId } = req.params; + if (typeof circuitListId !== "string" || !circuitListId.trim()) { + return res.status(400).json({ error: "Invalid circuitListId" }); + } + const circuitList = await circuitListRepository.findById(projectId, circuitListId); + if (!circuitList) { + return res.status(404).json({ error: "Circuit list not found" }); + } + const state = externalModelStateRepository.getByProject(projectId); + if (!state.projectExists) { + return res.status(404).json({ error: "Project not found" }); + } + return res.json(createExternalCircuitListObjects({ + state: state.state, + distributionBoardId: circuitList.distributionBoardId, + })); +} + export function updateExternalCsvConfiguration(req: Request, res: Response) { const projectId = getProjectId(req, res); if (!projectId) return; diff --git a/src/server/routes/project.routes.ts b/src/server/routes/project.routes.ts index 2ac20a5..604417e 100644 --- a/src/server/routes/project.routes.ts +++ b/src/server/routes/project.routes.ts @@ -41,6 +41,7 @@ import { previewExternalCsv, planExternalInitialImport, updateExternalCsvConfiguration, + getExternalCircuitListObjects, } from "../controllers/external-csv.controller.js"; export const projectRouter = Router(); @@ -58,6 +59,10 @@ projectRouter.post( "/:projectId/external-csv/initial-import/plan", planExternalInitialImport ); +projectRouter.get( + "/:projectId/circuit-lists/:circuitListId/external-objects", + getExternalCircuitListObjects +); projectRouter.post( "/:projectId/external-csv/initial-import/apply", applyExternalInitialImport diff --git a/tests/external-circuit-list-objects.test.ts b/tests/external-circuit-list-objects.test.ts new file mode 100644 index 0000000..4552d58 --- /dev/null +++ b/tests/external-circuit-list-objects.test.ts @@ -0,0 +1,105 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { createExternalCircuitListObjects } from "../src/external-model/application/external-circuit-list-objects.js"; +import type { + ExternalModelObjectSnapshot, + ExternalModelStateSnapshot, +} from "../src/external-model/domain/external-model-contracts.js"; + +function object(input: { + id: string; + distributionBoardId: string; + circuitDeviceRowId: string | null; + roomNumber: string; +}): ExternalModelObjectSnapshot { + return { + id: input.id, + projectId: "project-1", + sourceId: "source-1", + ifcGuid: `ifc-${input.id}`, + lastSeenImportBatchId: "batch-1", + lastAcceptedImportBatchId: "batch-1", + acceptedSourceValues: { + rowNumber: 2, + roomNumber: input.roomNumber, + roomName: "Büro", + familyAndType: "Steckdose: Standard", + selectionMarker: `S-${input.id}`, + circuitIdentifier: "-2F1.1", + power: "120", + quantity: "1", + additionalSourceValues: {}, + }, + planningValues: { + displayName: "Steckdose", + internalDeviceType: "socket", + category: "single_phase", + connectionKind: "socket", + effectiveQuantity: 1, + powerPerUnitW: 120, + simultaneityFactor: 1, + cosPhi: null, + costGroup: null, + remark: null, + }, + overriddenFields: [], + externalRoomMappingId: input.distributionBoardId === "board-1" ? "mapping-1" : null, + distributionBoardId: input.distributionBoardId, + linkedProjectDeviceId: null, + circuitDeviceRowId: input.circuitDeviceRowId, + presenceStatus: "present", + }; +} + +describe("external circuit-list object projection", () => { + it("returns only the board objects with unassigned objects first", () => { + const state: ExternalModelStateSnapshot = { + source: { + id: "source-1", + projectId: "project-1", + name: "Revit-Gesamtmodell", + sourceType: "revit_csv", + }, + importBatches: [], + roomMappings: [{ + id: "mapping-1", + projectId: "project-1", + sourceId: "source-1", + normalizedSourceRoomKey: "number:101", + sourceFloorName: "EG", + sourceRoomNumber: "101", + sourceRoomName: "Besprechung", + roomId: "room-1", + defaultDistributionBoardId: "board-1", + }], + objects: [ + object({ id: "assigned", distributionBoardId: "board-1", circuitDeviceRowId: "row-1", roomNumber: "102" }), + object({ id: "unassigned", distributionBoardId: "board-1", circuitDeviceRowId: null, roomNumber: "101" }), + object({ id: "other-board", distributionBoardId: "board-2", circuitDeviceRowId: null, roomNumber: "001" }), + ], + }; + + const result = createExternalCircuitListObjects({ + state, + distributionBoardId: "board-1", + }); + + assert.equal(result.sourceName, "Revit-Gesamtmodell"); + assert.equal(result.objectCount, 2); + assert.equal(result.unassignedObjectCount, 1); + assert.deepEqual(result.objects.map(({ id }) => id), ["unassigned", "assigned"]); + assert.equal(result.objects[0].sourceRoomName, "Besprechung"); + assert.equal(result.objects[0].roomId, "room-1"); + assert.equal(result.objects[0].sourceCircuitIdentifier, "-2F1.1"); + }); + + it("returns an empty projection before the optional Revit import", () => { + assert.deepEqual( + createExternalCircuitListObjects({ + state: { source: null, importBatches: [], roomMappings: [], objects: [] }, + distributionBoardId: "board-1", + }), + { sourceName: null, objectCount: 0, unassignedObjectCount: 0, objects: [] } + ); + }); +}); diff --git a/tests/revit-csv-frontend-api.test.ts b/tests/revit-csv-frontend-api.test.ts index d3695e5..3603aa7 100644 --- a/tests/revit-csv-frontend-api.test.ts +++ b/tests/revit-csv-frontend-api.test.ts @@ -12,6 +12,7 @@ import { RoomModal } from "../src/frontend/components/room-modal.js"; import { applyExternalInitialImport, getExternalCsvConfiguration, + getExternalCircuitListObjects, planExternalInitialImport, previewExternalCsv, updateExternalCsvConfiguration, @@ -39,6 +40,7 @@ describe("Revit CSV frontend API", () => { try { await getExternalCsvConfiguration("project-1"); + await getExternalCircuitListObjects("project-1", "list-1"); await updateExternalCsvConfiguration("project-1", 7, configuration); await previewExternalCsv("project-1", "revit.csv", "YWJj"); await planExternalInitialImport("project-1", "revit.csv", "YWJj"); @@ -62,6 +64,11 @@ describe("Revit CSV frontend API", () => { method: "GET", body: null, }, + { + url: "/api/projects/project-1/circuit-lists/list-1/external-objects", + method: "GET", + body: null, + }, { url: "/api/projects/project-1/external-csv/configuration", method: "PUT", From 9bd22dfcb7db35fcb509a2f32caadefc7eef7c03 Mon Sep 17 00:00:00 2001 From: Grovy311 Date: Mon, 3 Aug 2026 17:07:14 +0200 Subject: [PATCH 49/59] =?UTF-8?q?Add=20AMEV=20Gleichzeitigkeitsfaktor-Ausw?= =?UTF-8?q?=C3=A4hler=20und=20einklappbare=20Seitenleiste?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Gzf-Picker-Modal mit AMEV-Tabelle 3 (Planungshilfe für elektrische Leistungsbilanzen) im Projektgerät-Modal und im globalen Geräteformular - Docker-Deploy-Tooling (docker-start.sh, run-migrations.js) für den produktiven Container - Seitenleiste ist jetzt einklappbar (Icon-only, Zustand in localStorage) --- .dockerignore | 8 +- Dockerfile | 14 +-- scripts/docker-start.sh | 16 ++++ scripts/run-migrations.js | 16 ++++ src/app/globals.css | 65 ++++++++++++++ src/app/projects/page.tsx | 46 +++++++--- src/frontend/components/AppShell.tsx | 19 +++- src/frontend/components/Sidebar.tsx | 35 ++++++-- src/frontend/components/gzf-picker-modal.tsx | 86 +++++++++++++++++++ .../components/project-device-modal.tsx | 47 ++++++++-- src/shared/constants/amev-gzf.ts | 38 ++++++++ 11 files changed, 349 insertions(+), 41 deletions(-) create mode 100644 scripts/docker-start.sh create mode 100644 scripts/run-migrations.js create mode 100644 src/frontend/components/gzf-picker-modal.tsx create mode 100644 src/shared/constants/amev-gzf.ts diff --git a/.dockerignore b/.dockerignore index 69951f8..80054d0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,8 @@ .git -.agents -.codex .next -data dist node_modules -npm-debug.log* +data +*.db +.env +.env.* diff --git a/Dockerfile b/Dockerfile index 70e7761..41a08fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,14 @@ -FROM node:22-bookworm-slim - +FROM node:22 WORKDIR /app -ENV NEXT_TELEMETRY_DISABLED=1 - -COPY package.json package-lock.json ./ +COPY package*.json ./ RUN npm ci COPY . . +RUN npm run build:api && npm run build:web -EXPOSE 3000 3001 +RUN mkdir -p data && chmod +x scripts/docker-start.sh + +EXPOSE 3001 + +CMD ["sh", "scripts/docker-start.sh"] diff --git a/scripts/docker-start.sh b/scripts/docker-start.sh new file mode 100644 index 0000000..bf0d856 --- /dev/null +++ b/scripts/docker-start.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -e + +echo "Running migrations..." +node scripts/run-migrations.js + +echo "Starting API server on :3000..." +node dist/server/index.js & + +echo "Waiting for API..." +until node -e "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))" 2>/dev/null; do + sleep 1 +done + +echo "Starting Next.js on :3001..." +exec node_modules/.bin/next start -p 3001 diff --git a/scripts/run-migrations.js b/scripts/run-migrations.js new file mode 100644 index 0000000..ebbed8a --- /dev/null +++ b/scripts/run-migrations.js @@ -0,0 +1,16 @@ +const { migrate } = require('drizzle-orm/better-sqlite3/migrator'); +const { drizzle } = require('drizzle-orm/better-sqlite3'); +const Database = require('better-sqlite3'); +const fs = require('node:fs'); +const path = require('node:path'); + +const dataDir = path.resolve('data'); +if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true }); + +const sqlite = new Database(path.join(dataDir, 'leistungsbilanz.db')); +const db = drizzle(sqlite); + +migrate(db, { migrationsFolder: path.resolve('src/db/migrations') }); +sqlite.close(); + +console.log('Migrations applied'); diff --git a/src/app/globals.css b/src/app/globals.css index 27b2d63..9c07a40 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1219,3 +1219,68 @@ a.kpi:hover { max-height: calc(100vh - 5.5rem); } } + +/* ── Seitenleiste: eingeklappter Zustand ─────────────────────────────── */ + +.app-shell { + transition: grid-template-columns 0.18s ease; +} + +.app-shell-collapsed { + grid-template-columns: 64px 1fr; +} + +.sidebar { + transition: padding 0.18s ease; +} + +.sidebar-brand { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 8px; +} + +.sidebar-toggle { + background: rgba(255, 255, 255, 0.08); + border: 0; + border-radius: 6px; + color: #fff; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-size: 0.8rem; + line-height: 1; + width: 26px; + height: 26px; +} + +.sidebar-toggle:hover { + background: rgba(255, 255, 255, 0.18); +} + +.sidebar-collapsed .sidebar-brand { + padding: 16px 10px; + flex-direction: column; + align-items: center; +} + +.sidebar-collapsed .sidebar-brand-text { + display: none; +} + +.sidebar-collapsed .sidebar-nav { + padding: 12px 8px; +} + +.sidebar-collapsed .sidebar-link { + justify-content: center; + padding: 10px; +} + +.sidebar-collapsed .sidebar-section-label, +.sidebar-collapsed .sidebar-label { + display: none; +} diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx index 93a2d14..21409fd 100644 --- a/src/app/projects/page.tsx +++ b/src/app/projects/page.tsx @@ -14,6 +14,7 @@ import type { CreateGlobalDeviceInput, GlobalDeviceDto, } from "../../frontend/types"; +import { GzfPickerModal } from "../../frontend/components/gzf-picker-modal"; const PROJECTS_CHANGED_EVENT = "leistungsbilanz:projects-changed"; @@ -67,6 +68,7 @@ export default function ProjectsPage() { id: string; name: string; } | null>(null); + const [isGzfPickerOpen, setIsGzfPickerOpen] = useState(false); async function loadData() { setError(null); @@ -380,18 +382,28 @@ export default function ProjectsPage() { } />
-
- - setGlobalDeviceForm((current) => ({ ...current, demandFactor: event.target.value })) - } - /> +
+
+ + setGlobalDeviceForm((current) => ({ ...current, demandFactor: event.target.value })) + } + /> + +
update("simultaneityFactor", event.target.value)} + step="0.01" + type="number" + value={values.simultaneityFactor} + /> + +
+
+ {isGzfPickerOpen ? ( + setIsGzfPickerOpen(false)} + onSelect={(value) => { + update("simultaneityFactor", String(value)); + setIsGzfPickerOpen(false); + }} + /> + ) : null} + ); } diff --git a/src/shared/constants/amev-gzf.ts b/src/shared/constants/amev-gzf.ts new file mode 100644 index 0000000..0833091 --- /dev/null +++ b/src/shared/constants/amev-gzf.ts @@ -0,0 +1,38 @@ +export interface AmevGzfEntry { + group: string; + range: string; + mean: number; + overallRange?: string; +} + +// Quelle: AMEV „Planungshilfe für elektrische Leistungsbilanzen" (rev. 4), +// Tabelle 3 – Gleichzeitigkeitsfaktoren für Verbrauchergruppen. +export const AMEV_GZF_TABLE: AmevGzfEntry[] = [ + { group: "Absaugung, Digestorien", range: "0,7", mean: 0.7 }, + { group: "Aufzüge/Rolltreppen", range: "0,2 – 0,7", mean: 0.5, overallRange: "0,2" }, + { group: "Beleuchtungsanlagen", range: "0,7 – 0,8", mean: 0.75, overallRange: "0,4" }, + { group: "Beleuchtungsanlagen in innenliegenden Räumen", range: "0,7 – 0,9", mean: 0.8 }, + { group: "IuK-Anlagen", range: "1", mean: 1, overallRange: "1" }, + { group: "Experimentieranlagen", range: "0,2 – 0,4", mean: 0.3 }, + { group: "Heizung", range: "0,7 – 1", mean: 0.85 }, + { group: "Kälteanlagen", range: "0,8 – 1", mean: 0.9, overallRange: "0,4 – 0,5" }, + { group: "Küchen elektrisch ohne Energieoptimierung", range: "0,3 – 0,7", mean: 0.6 }, + { group: "Küchen elektrisch mit Energieoptimierung", range: "0,2 – 0,6", mean: 0.4 }, + { group: "Lastenaufzüge, Krananlagen", range: "0,2", mean: 0.2 }, + { + group: "Lüftungsanlagen, kontrollierte Lüftung", + range: "0,7", + mean: 0.7, + overallRange: "0,4 – 0,5", + }, + { + group: "Steckdosen 230 V allg. Verbraucher (100 W/Steckdose)", + range: "0,1 – 0,3", + mean: 0.2, + }, + { group: "Steckdosen 230 V für IuK (100 W/Steckdose)", range: "0,7 – 0,9", mean: 0.8 }, + { group: "Steckdosen 400 V (1000 W/Steckdose)", range: "0,1 – 0,5", mean: 0.35 }, + { group: "Schmutz-, Warmwasserpumpen", range: "0,2 – 0,4", mean: 0.3 }, + { group: "Umwälzpumpen", range: "0,6 – 1", mean: 0.8 }, + { group: "Werkstätten", range: "0,2 – 0,4", mean: 0.3 }, +]; From f26c0000078f00e40df90c392ab30c8ecdc95c2d Mon Sep 17 00:00:00 2001 From: root Date: Thu, 6 Aug 2026 18:58:56 +0200 Subject: [PATCH 50/59] Add local uris to nextjs config --- next.config.mjs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/next.config.mjs b/next.config.mjs index e81b670..e0610b9 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -2,6 +2,12 @@ const apiInternalUrl = (process.env.API_INTERNAL_URL || "http://localhost:3000").replace(/\/$/, ""); const nextConfig = { + allowedDevOrigins: [ + "192.168.3.13", + "docker01.int.jappel.io", + "lb.jappel.io" + ], + typescript: { tsconfigPath: "./tsconfig.next.json", }, @@ -20,3 +26,4 @@ const nextConfig = { }; export default nextConfig; + From ea3c02cd6bdce7650ff1978e94740044cad72286 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Thu, 6 Aug 2026 19:45:39 +0200 Subject: [PATCH 51/59] Add configurable structured logging Adds a leveled JSON logger (error/warn/info/verbose/debug, controlled via LOG_LEVEL) wired into the Express API (access log, error middleware, crash handlers, memory heartbeat) and the Next.js server (page-request proxy, instrumentation crash handlers, heartbeat). LOG_LEVEL is exposed through compose.yaml, and both services now rotate their Docker logs (json-file, 20m x 10 files) instead of growing unbounded. Intended to capture long-running diagnostic data for the intermittent 502s seen on the Docker host. Co-Authored-By: Claude Sonnet 5 --- compose.yaml | 12 ++++ docs/deployment.md | 29 ++++++++ src/instrumentation-node.ts | 25 +++++++ src/instrumentation.ts | 8 +++ src/proxy.ts | 17 +++++ src/server/index.ts | 48 ++++++++++++- src/server/middleware/error.middleware.ts | 11 ++- src/shared/logging/logger.ts | 71 +++++++++++++++++++ tests/logger.test.ts | 83 +++++++++++++++++++++++ tsconfig.json | 8 ++- tsconfig.next.json | 3 + tsconfig.scripts.json | 8 ++- 12 files changed, 317 insertions(+), 6 deletions(-) create mode 100644 src/instrumentation-node.ts create mode 100644 src/instrumentation.ts create mode 100644 src/proxy.ts create mode 100644 src/shared/logging/logger.ts create mode 100644 tests/logger.test.ts diff --git a/compose.yaml b/compose.yaml index 2062cb7..d8d787e 100644 --- a/compose.yaml +++ b/compose.yaml @@ -11,7 +11,13 @@ services: environment: PORT: "3000" CHOKIDAR_USEPOLLING: "true" + LOG_LEVEL: "${LOG_LEVEL:-info}" init: true + logging: + driver: json-file + options: + max-size: "20m" + max-file: "10" ports: - "3000:3000" volumes: @@ -45,7 +51,13 @@ services: API_INTERNAL_URL: http://api:3000 WATCHPACK_POLLING: "true" NEXT_TELEMETRY_DISABLED: "1" + LOG_LEVEL: "${LOG_LEVEL:-info}" init: true + logging: + driver: json-file + options: + max-size: "20m" + max-file: "10" depends_on: api: condition: service_healthy diff --git a/docs/deployment.md b/docs/deployment.md index 44c7dd5..5458a69 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -26,10 +26,39 @@ Verwendete Umgebungsvariablen: - `NEXT_TELEMETRY_DISABLED=1` - `CHOKIDAR_USEPOLLING=true` und `WATCHPACK_POLLING=true` für lokale Dateibeobachtung in Docker +- `LOG_LEVEL` – steuert für beide Dienste die Ausgabestufe des strukturierten + JSON-Loggers (`error`, `warn`, `info`, `verbose`, `debug`), Standard `info`. + Setzbar über eine `.env`-Datei neben `compose.yaml` oder + `LOG_LEVEL=verbose docker compose up`. Beim API-Start laufen zuerst `npm run db:migrate` und `npm run db:verify:circuit-schema`. +## Logging + +Beide Dienste schreiben strukturierte, einzeilige JSON-Log-Zeilen nach +stdout/stderr (`docker compose logs --follow`). Jede Zeile enthält +`timestamp`, `level`, `scope` und `message`. `compose.yaml` konfiguriert für +beide Dienste den `json-file`-Treiber mit Rotation (`max-size: 20m`, +`max-file: 10`, also bis zu 200 MB je Dienst); ohne diese Einstellung würde +Docker mit der Standardkonfiguration unbegrenzt in eine einzelne Datei unter +`/var/lib/docker/containers//` schreiben. Die Logs überleben +einen Container-Neustart (`docker compose restart`), aber nicht das Entfernen +des Containers (`docker compose down` gefolgt von `up` erzeugt neue +Container und damit neue, leere Logdateien); für ein echtes Langzeitarchiv +über Rebuilds hinweg müssten die Zeilen zusätzlich in eine Datei im +gemounteten `./data`-Verzeichnis oder an ein externes Log-System geschrieben +werden. Die Express-API protokolliert +jede abgeschlossene Anfrage (Methode, Pfad, Status, Dauer; `/health` wird +nicht mitgeloggt) sowie unbehandelte Exceptions/Promise-Rejections. Das +Next.js-Frontend protokolliert Seitenanfragen (Navigation) über +`src/proxy.ts` und unbehandelte Fehler über `src/instrumentation.ts`. +Beide Prozesse schreiben zusätzlich alle fünf Minuten einen `verbose`-Heartbeat +mit Laufzeit und Speicherverbrauch – nützlich, um Speicherlecks oder Hänger vor +einem 502 über einen längeren Zeitraum nachzuvollziehen. Für die Detailsuche +`LOG_LEVEL=debug` setzen; das protokolliert zusätzlich den Start jeder +API-Anfrage und macht damit hängende (nie abgeschlossene) Requests sichtbar. + ## Voraussetzungen für ein späteres Produktionssetup Vor einer produktiven Installation werden mindestens benötigt: diff --git a/src/instrumentation-node.ts b/src/instrumentation-node.ts new file mode 100644 index 0000000..0549375 --- /dev/null +++ b/src/instrumentation-node.ts @@ -0,0 +1,25 @@ +import { createLogger, toErrorMeta } from "./shared/logging/logger"; + +export function registerNodeInstrumentation() { + const logger = createLogger("web"); + const heartbeatIntervalMs = 5 * 60 * 1000; + + logger.info("web server starting", { pid: process.pid }); + + process.on("uncaughtException", (error) => { + logger.error("uncaught exception", toErrorMeta(error)); + }); + + process.on("unhandledRejection", (reason) => { + logger.error("unhandled rejection", toErrorMeta(reason)); + }); + + setInterval(() => { + const memory = process.memoryUsage(); + logger.verbose("heartbeat", { + uptimeSeconds: Math.round(process.uptime()), + rssMb: Math.round(memory.rss / 1024 / 1024), + heapUsedMb: Math.round(memory.heapUsed / 1024 / 1024), + }); + }, heartbeatIntervalMs).unref(); +} diff --git a/src/instrumentation.ts b/src/instrumentation.ts new file mode 100644 index 0000000..7812ea2 --- /dev/null +++ b/src/instrumentation.ts @@ -0,0 +1,8 @@ +export async function register() { + if (process.env.NEXT_RUNTIME !== "nodejs") { + return; + } + + const { registerNodeInstrumentation } = await import("./instrumentation-node"); + registerNodeInstrumentation(); +} diff --git a/src/proxy.ts b/src/proxy.ts new file mode 100644 index 0000000..9b301d8 --- /dev/null +++ b/src/proxy.ts @@ -0,0 +1,17 @@ +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { createLogger } from "./shared/logging/logger"; + +const logger = createLogger("web:navigation"); + +export function proxy(request: NextRequest) { + logger.info("page request", { + method: request.method, + path: request.nextUrl.pathname, + }); + return NextResponse.next(); +} + +export const config = { + matcher: ["/((?!_next/static|_next/image|favicon.ico|api).*)"], +}; diff --git a/src/server/index.ts b/src/server/index.ts index e951a07..851276a 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -4,12 +4,36 @@ import { globalDeviceRouter } from "./routes/global-device.routes.js"; import { projectDeviceRouter } from "./routes/project-device.routes.js"; import { projectRouter } from "./routes/project.routes.js"; import { errorMiddleware } from "./middleware/error.middleware.js"; +import { createLogger, toErrorMeta } from "../shared/logging/logger.js"; +const logger = createLogger("api"); const app = express(); const port = Number(process.env.PORT || 3000); +const heartbeatIntervalMs = 5 * 60 * 1000; app.use(express.json({ limit: "25mb" })); +app.use((req, res, next) => { + if (req.path === "/health") { + next(); + return; + } + const startedAt = Date.now(); + logger.debug("request started", { method: req.method, path: req.originalUrl }); + res.on("finish", () => { + const meta = { + method: req.method, + path: req.originalUrl, + status: res.statusCode, + durationMs: Date.now() - startedAt, + }; + if (res.statusCode >= 500) logger.error("request completed", meta); + else if (res.statusCode >= 400) logger.warn("request completed", meta); + else logger.info("request completed", meta); + }); + next(); +}); + app.get("/health", (_req, res) => { res.json({ ok: true }); }); @@ -21,6 +45,26 @@ app.use("/api/project-devices", projectDeviceRouter); app.use(errorMiddleware); -app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`); +process.on("uncaughtException", (error) => { + logger.error("uncaught exception", toErrorMeta(error)); +}); + +process.on("unhandledRejection", (reason) => { + logger.error("unhandled rejection", toErrorMeta(reason)); +}); + +process.on("SIGTERM", () => logger.info("received SIGTERM")); +process.on("SIGINT", () => logger.info("received SIGINT")); + +setInterval(() => { + const memory = process.memoryUsage(); + logger.verbose("heartbeat", { + uptimeSeconds: Math.round(process.uptime()), + rssMb: Math.round(memory.rss / 1024 / 1024), + heapUsedMb: Math.round(memory.heapUsed / 1024 / 1024), + }); +}, heartbeatIntervalMs).unref(); + +app.listen(port, () => { + logger.info("server started", { port }); }); diff --git a/src/server/middleware/error.middleware.ts b/src/server/middleware/error.middleware.ts index 33ff083..65b2df4 100644 --- a/src/server/middleware/error.middleware.ts +++ b/src/server/middleware/error.middleware.ts @@ -1,12 +1,19 @@ import type { NextFunction, Request, Response } from "express"; +import { createLogger, toErrorMeta } from "../../shared/logging/logger.js"; + +const logger = createLogger("api"); export function errorMiddleware( error: unknown, - _req: Request, + req: Request, res: Response, _next: NextFunction ) { - console.error(error); + logger.error("request handler threw", { + method: req.method, + path: req.originalUrl, + ...toErrorMeta(error), + }); res.status(500).json({ error: "Internal Server Error" }); } diff --git a/src/shared/logging/logger.ts b/src/shared/logging/logger.ts new file mode 100644 index 0000000..423a7ad --- /dev/null +++ b/src/shared/logging/logger.ts @@ -0,0 +1,71 @@ +export type LogLevel = "error" | "warn" | "info" | "verbose" | "debug"; + +const LEVEL_SEVERITY: Record = { + error: 0, + warn: 1, + info: 2, + verbose: 3, + debug: 4, +}; + +const DEFAULT_LEVEL: LogLevel = "info"; + +export function resolveLogLevel(raw: string | undefined): LogLevel { + const candidate = raw?.trim().toLowerCase(); + if (candidate && candidate in LEVEL_SEVERITY) { + return candidate as LogLevel; + } + return DEFAULT_LEVEL; +} + +export function toErrorMeta(error: unknown): Record { + if (error instanceof Error) { + return { + errorName: error.name, + errorMessage: error.message, + stack: error.stack, + }; + } + return { error: typeof error === "string" ? error : JSON.stringify(error) }; +} + +export interface Logger { + error(message: string, meta?: Record): void; + warn(message: string, meta?: Record): void; + info(message: string, meta?: Record): void; + verbose(message: string, meta?: Record): void; + debug(message: string, meta?: Record): void; +} + +export function createLogger( + scope: string, + options?: { level?: LogLevel } +): Logger { + const threshold = options?.level ?? resolveLogLevel(process.env.LOG_LEVEL); + + const write = (level: LogLevel, message: string, meta?: Record) => { + if (LEVEL_SEVERITY[level] > LEVEL_SEVERITY[threshold]) { + return; + } + const line = JSON.stringify({ + timestamp: new Date().toISOString(), + level, + scope, + message, + ...meta, + }); + if (level === "error" || level === "warn") { + console.error(line); + } else { + console.log(line); + } + }; + + return { + error: (message, meta) => write("error", message, meta), + warn: (message, meta) => write("warn", message, meta), + info: (message, meta) => write("info", message, meta), + verbose: (message, meta) => write("verbose", message, meta), + debug: (message, meta) => write("debug", message, meta), + }; +} diff --git a/tests/logger.test.ts b/tests/logger.test.ts new file mode 100644 index 0000000..35ee720 --- /dev/null +++ b/tests/logger.test.ts @@ -0,0 +1,83 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { createLogger, resolveLogLevel } from "../src/shared/logging/logger.js"; + +describe("resolveLogLevel", () => { + it("defaults to info for missing or unknown values", () => { + assert.equal(resolveLogLevel(undefined), "info"); + assert.equal(resolveLogLevel(""), "info"); + assert.equal(resolveLogLevel("nonsense"), "info"); + }); + + it("accepts known levels case-insensitively", () => { + assert.equal(resolveLogLevel("DEBUG"), "debug"); + assert.equal(resolveLogLevel(" verbose "), "verbose"); + assert.equal(resolveLogLevel("warn"), "warn"); + }); +}); + +describe("createLogger", () => { + it("suppresses levels below the configured threshold", () => { + const infoLines: string[] = []; + const originalLog = console.log; + console.log = (line: string) => { + infoLines.push(line); + }; + try { + const logger = createLogger("test", { level: "warn" }); + logger.info("should not appear"); + logger.debug("should not appear"); + logger.verbose("should not appear"); + } finally { + console.log = originalLog; + } + assert.equal(infoLines.length, 0); + }); + + it("emits enabled levels as structured JSON with scope and meta", () => { + const errorLines: string[] = []; + const originalError = console.error; + console.error = (line: string) => { + errorLines.push(line); + }; + try { + const logger = createLogger("test", { level: "warn" }); + logger.warn("something happened", { code: 42 }); + } finally { + console.error = originalError; + } + assert.equal(errorLines.length, 1); + const parsed = JSON.parse(errorLines[0]); + assert.equal(parsed.level, "warn"); + assert.equal(parsed.scope, "test"); + assert.equal(parsed.message, "something happened"); + assert.equal(parsed.code, 42); + assert.equal(typeof parsed.timestamp, "string"); + }); + + it("routes debug/verbose/info to console.log and error/warn to console.error", () => { + const logLines: string[] = []; + const errorLines: string[] = []; + const originalLog = console.log; + const originalError = console.error; + console.log = (line: string) => { + logLines.push(line); + }; + console.error = (line: string) => { + errorLines.push(line); + }; + try { + const logger = createLogger("test", { level: "debug" }); + logger.debug("d"); + logger.verbose("v"); + logger.info("i"); + logger.warn("w"); + logger.error("e"); + } finally { + console.log = originalLog; + console.error = originalError; + } + assert.equal(logLines.length, 3); + assert.equal(errorLines.length, 2); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 0f910df..9761c37 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,11 @@ "resolveJsonModule": true }, "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] + "exclude": [ + "node_modules", + "dist", + "src/proxy.ts", + "src/instrumentation.ts", + "src/instrumentation-node.ts" + ] } diff --git a/tsconfig.next.json b/tsconfig.next.json index b91ebfd..d54dc0c 100644 --- a/tsconfig.next.json +++ b/tsconfig.next.json @@ -18,6 +18,9 @@ "src/app/**/*.tsx", "src/frontend/**/*.ts", "src/frontend/**/*.tsx", + "src/proxy.ts", + "src/instrumentation.ts", + "src/instrumentation-node.ts", ".next/types/**/*.ts" ], "exclude": ["node_modules", "dist"] diff --git a/tsconfig.scripts.json b/tsconfig.scripts.json index ea4c56f..6facc56 100644 --- a/tsconfig.scripts.json +++ b/tsconfig.scripts.json @@ -5,5 +5,11 @@ "rootDir": "." }, "include": ["scripts/**/*.ts", "src/**/*.ts"], - "exclude": ["node_modules", "dist"] + "exclude": [ + "node_modules", + "dist", + "src/proxy.ts", + "src/instrumentation.ts", + "src/instrumentation-node.ts" + ] } From fa96be2d420eb33504eff843c3e759ca5ac9baa9 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Thu, 6 Aug 2026 19:58:31 +0200 Subject: [PATCH 52/59] Fix crash handling and log noise in logging feature Review of the logging work from the previous commit turned up a real correctness issue: the uncaughtException/unhandledRejection handlers logged the error but let the process keep running, which silently disabled Node's default crash-on-fatal-error behavior and could leave a zombie process serving broken requests instead of restarting. Both processes now log and then exit(1); restart: unless-stopped is added to both services so Docker actually brings them back up. Also: harden the logger against JSON.stringify throwing on non-serializable meta, log aborted (closed-before-finished) requests in the API access log, and stop the Next.js proxy from logging the Docker healthcheck's request to "/" every few seconds so real navigation events aren't drowned out. Co-Authored-By: Claude Sonnet 5 --- compose.yaml | 2 ++ docs/deployment.md | 9 +++++++++ src/instrumentation-node.ts | 6 ++++-- src/proxy.ts | 12 ++++++++---- src/server/index.ts | 15 +++++++++++++-- src/shared/logging/logger.ts | 20 +++++++++++++------- 6 files changed, 49 insertions(+), 15 deletions(-) diff --git a/compose.yaml b/compose.yaml index d8d787e..6a5ec7c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -13,6 +13,7 @@ services: CHOKIDAR_USEPOLLING: "true" LOG_LEVEL: "${LOG_LEVEL:-info}" init: true + restart: unless-stopped logging: driver: json-file options: @@ -53,6 +54,7 @@ services: NEXT_TELEMETRY_DISABLED: "1" LOG_LEVEL: "${LOG_LEVEL:-info}" init: true + restart: unless-stopped logging: driver: json-file options: diff --git a/docs/deployment.md b/docs/deployment.md index 5458a69..dfbb3f1 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -58,6 +58,15 @@ mit Laufzeit und Speicherverbrauch – nützlich, um Speicherlecks oder Hänger einem 502 über einen längeren Zeitraum nachzuvollziehen. Für die Detailsuche `LOG_LEVEL=debug` setzen; das protokolliert zusätzlich den Start jeder API-Anfrage und macht damit hängende (nie abgeschlossene) Requests sichtbar. +Ein `close`-Ereignis ohne vorheriges `finish` wird als `request aborted before +response finished` (`warn`) geloggt und zeigt damit vom Client oder einem +vorgeschalteten Proxy abgebrochene Verbindungen. + +Eine unbehandelte Exception oder Promise-Rejection wird geloggt und beendet +den jeweiligen Prozess anschließend bewusst (`process.exit(1)`), statt in +einem unbekannten Zustand weiterzulaufen. Beide Dienste laufen deshalb mit +`restart: unless-stopped`, damit Docker sie danach automatisch neu startet; +ohne diese Policy würde ein Crash den Dienst dauerhaft unerreichbar lassen. ## Voraussetzungen für ein späteres Produktionssetup diff --git a/src/instrumentation-node.ts b/src/instrumentation-node.ts index 0549375..760f6eb 100644 --- a/src/instrumentation-node.ts +++ b/src/instrumentation-node.ts @@ -7,11 +7,13 @@ export function registerNodeInstrumentation() { logger.info("web server starting", { pid: process.pid }); process.on("uncaughtException", (error) => { - logger.error("uncaught exception", toErrorMeta(error)); + logger.error("uncaught exception, exiting", toErrorMeta(error)); + process.exit(1); }); process.on("unhandledRejection", (reason) => { - logger.error("unhandled rejection", toErrorMeta(reason)); + logger.error("unhandled rejection, exiting", toErrorMeta(reason)); + process.exit(1); }); setInterval(() => { diff --git a/src/proxy.ts b/src/proxy.ts index 9b301d8..4607ec0 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -5,10 +5,14 @@ import { createLogger } from "./shared/logging/logger"; const logger = createLogger("web:navigation"); export function proxy(request: NextRequest) { - logger.info("page request", { - method: request.method, - path: request.nextUrl.pathname, - }); + // The Docker healthcheck hits "/" every few seconds with no User-Agent + // header; skip it so real navigation isn't drowned out in the logs. + if (request.headers.get("user-agent")) { + logger.info("page request", { + method: request.method, + path: request.nextUrl.pathname, + }); + } return NextResponse.next(); } diff --git a/src/server/index.ts b/src/server/index.ts index 851276a..fa3a74e 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -31,6 +31,15 @@ app.use((req, res, next) => { else if (res.statusCode >= 400) logger.warn("request completed", meta); else logger.info("request completed", meta); }); + res.on("close", () => { + if (!res.writableEnded) { + logger.warn("request aborted before response finished", { + method: req.method, + path: req.originalUrl, + durationMs: Date.now() - startedAt, + }); + } + }); next(); }); @@ -46,11 +55,13 @@ app.use("/api/project-devices", projectDeviceRouter); app.use(errorMiddleware); process.on("uncaughtException", (error) => { - logger.error("uncaught exception", toErrorMeta(error)); + logger.error("uncaught exception, exiting", toErrorMeta(error)); + process.exit(1); }); process.on("unhandledRejection", (reason) => { - logger.error("unhandled rejection", toErrorMeta(reason)); + logger.error("unhandled rejection, exiting", toErrorMeta(reason)); + process.exit(1); }); process.on("SIGTERM", () => logger.info("received SIGTERM")); diff --git a/src/shared/logging/logger.ts b/src/shared/logging/logger.ts index 423a7ad..38afb9d 100644 --- a/src/shared/logging/logger.ts +++ b/src/shared/logging/logger.ts @@ -47,13 +47,19 @@ export function createLogger( if (LEVEL_SEVERITY[level] > LEVEL_SEVERITY[threshold]) { return; } - const line = JSON.stringify({ - timestamp: new Date().toISOString(), - level, - scope, - message, - ...meta, - }); + const timestamp = new Date().toISOString(); + let line: string; + try { + line = JSON.stringify({ timestamp, level, scope, message, ...meta }); + } catch { + line = JSON.stringify({ + timestamp, + level, + scope, + message, + logError: "failed to serialize log metadata", + }); + } if (level === "error" || level === "warn") { console.error(line); } else { From b45dc5002d82be5a1b48ec6786f949df4cf433b6 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Thu, 6 Aug 2026 21:31:16 +0200 Subject: [PATCH 53/59] Fix code-review findings across domain, persistence, server and frontend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full-codebase review turned up five real correctness/security bugs and a dozen smaller inconsistencies; all are fixed here with matching test coverage: - BMK uniqueness silently allowed German-umlaut duplicates ("Ä1" vs "ä1") because the DB's normalized index only folds ASCII case. Added a shared Unicode-aware pre-check used by every circuit/component insert and rename path (one of which had no pre-check at all). - CircuitDeviceRow.simultaneityFactor had no upper bound at the row level (command model and snapshot/restore schema), unlike every sibling entity, letting a bad value silently corrupt power totals. - Grid cell editing silently misread German thousands-separator input ("1.500" parsed as 1.5); "." is now rejected outright with a clear message instead of guessing. - The editor's shared command runner (runCommand/applyHistory) had no re-entrancy guard, so a double click/drop could fire the same command twice and race a BMK collision or revision conflict. Added a synchronous ref guard plus isSaving on the buttons that lacked it. - GET .../next-identifier leaked circuit-numbering state for sections in other projects (no ownership check, 400 instead of 404). Moved under /projects/:projectId and scoped it. Also: added the missing circuits.section_id / circuit_device_rows. circuit_id indexes (migration 0006), gave FormModal a focus trap / Escape-to-close / focus restore and rebuilt ProjectSettingsModal on top of it instead of duplicated markup, removed dead code (3 orphaned domain model files, an unused persistence helper, a wrapper only used by its own test), pointed the project page at GET /projects/:id instead of listing+filtering client-side, closed the gap between the documented 18 MB CSV limit and the ~17.17 MiB actually enforced, added missing upper bounds on several free-text fields, filled in nine missing German labels in the revision timeline, replaced a key-order-fragile JSON.stringify equality check with a real field comparison, made an implicit sort-order assumption in three renumbering helpers explicit, cleared the sidebar's target selection when it no longer resolves after a tree reload, and fixed updateGlobalDevice to check-then-write instead of write-then-check. Co-Authored-By: Claude Sonnet 5 --- src/app/projects/[projectId]/page.tsx | 7 +- src/db/migrations/0006_damp_skrulls.sql | 2 + src/db/migrations/meta/0006_snapshot.json | 2419 +++++++++++++++++ src/db/migrations/meta/_journal.json | 7 + .../circuit-device-row.persistence.ts | 18 - .../circuit-project-command.repository.ts | 23 +- ...it-structure-project-command.repository.ts | 24 +- ...nt-structure-project-command.repository.ts | 17 + ...pment-identifier-uniqueness.persistence.ts | 41 + ...itial-import-project-command.repository.ts | 4 +- ...-new-circuit-project-command.repository.ts | 12 +- src/db/schema/circuit-device-rows.ts | 65 +- src/db/schema/circuits.ts | 7 +- ...ircuit-device-row-project-command.model.ts | 3 + ...ice-row-structure-project-command.model.ts | 3 + src/domain/models/circuit-device-row.model.ts | 24 - ...ircuit-protection-project-command.model.ts | 22 +- src/domain/models/circuit-section.model.ts | 9 - src/domain/models/circuit.model.ts | 19 - .../models/project-state-snapshot.model.ts | 2 +- .../components/circuit-tree-editor.tsx | 60 +- src/frontend/components/form-modal.tsx | 50 +- .../components/project-settings-modal.tsx | 562 ++-- src/frontend/utils/api.ts | 4 +- src/frontend/utils/circuit-grid-model.ts | 10 + src/frontend/utils/circuit-grid-projection.ts | 7 - src/frontend/utils/circuit-group-editing.ts | 36 +- .../utils/circuit-section-renumber-command.ts | 6 +- .../utils/circuit-structure-projection.ts | 2 +- src/frontend/utils/project-version-history.ts | 14 + src/server/controllers/circuit.controller.ts | 17 +- .../controllers/global-device.controller.ts | 7 +- src/server/index.ts | 2 - src/server/routes/circuit.routes.ts | 9 - src/server/routes/project.routes.ts | 5 + src/shared/validation/external-csv.schemas.ts | 9 +- .../validation/global-device.schemas.ts | 8 +- .../validation/project-device.schemas.ts | 10 +- .../validation/project-structure.schemas.ts | 12 +- tests/circuit-grid-model.test.ts | 15 +- tests/circuit-grid-projection.test.ts | 7 +- ...ructure-project-command.repository.test.ts | 39 + tests/circuit.controller.test.ts | 100 + ...ructure-project-command.repository.test.ts | 2 +- tests/external-csv-api-contract.test.ts | 3 +- ...-import-project-command.repository.test.ts | 24 + tests/project-command.model.test.ts | 15 + tests/project-version-history.test.ts | 26 + 48 files changed, 3263 insertions(+), 526 deletions(-) create mode 100644 src/db/migrations/0006_damp_skrulls.sql create mode 100644 src/db/migrations/meta/0006_snapshot.json create mode 100644 src/db/repositories/equipment-identifier-uniqueness.persistence.ts delete mode 100644 src/domain/models/circuit-device-row.model.ts delete mode 100644 src/domain/models/circuit-section.model.ts delete mode 100644 src/domain/models/circuit.model.ts delete mode 100644 src/server/routes/circuit.routes.ts create mode 100644 tests/circuit.controller.test.ts diff --git a/src/app/projects/[projectId]/page.tsx b/src/app/projects/[projectId]/page.tsx index c675340..fa342ea 100644 --- a/src/app/projects/[projectId]/page.tsx +++ b/src/app/projects/[projectId]/page.tsx @@ -17,13 +17,13 @@ import { deleteDistributionBoard, disconnectProjectDeviceRows, exportProjectTransfer, + getProject, getProjectDeviceSyncPreview, listCircuitLists, listDistributionBoards, listFloors, listGlobalDevices, listProjectDevices, - listProjects, listRooms, importProjectTransfer, synchronizeProjectDeviceRows, @@ -129,7 +129,7 @@ export default function ProjectDetailPage() { return; } Promise.all([ - listProjects(), + getProject(projectId), listDistributionBoards(projectId), listCircuitLists(projectId), listFloors(projectId), @@ -138,7 +138,7 @@ export default function ProjectDetailPage() { listGlobalDevices(), ]) .then(([ - projects, + currentProject, distributionBoards, loadedCircuitLists, loadedFloors, @@ -146,7 +146,6 @@ export default function ProjectDetailPage() { loadedProjectDevices, loadedGlobalDevices, ]) => { - const currentProject = projects.find((item) => item.id === projectId) ?? null; setProject(currentProject); setBoards(distributionBoards); setCircuitLists(loadedCircuitLists); diff --git a/src/db/migrations/0006_damp_skrulls.sql b/src/db/migrations/0006_damp_skrulls.sql new file mode 100644 index 0000000..3f6badf --- /dev/null +++ b/src/db/migrations/0006_damp_skrulls.sql @@ -0,0 +1,2 @@ +CREATE INDEX `circuit_device_rows_circuit_id_idx` ON `circuit_device_rows` (`circuit_id`);--> statement-breakpoint +CREATE INDEX `circuits_section_id_idx` ON `circuits` (`section_id`); \ No newline at end of file diff --git a/src/db/migrations/meta/0006_snapshot.json b/src/db/migrations/meta/0006_snapshot.json new file mode 100644 index 0000000..84f5229 --- /dev/null +++ b/src/db/migrations/meta/0006_snapshot.json @@ -0,0 +1,2419 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "b25e1a40-c400-4848-bc24-a04bda7a8209", + "prevId": "b79934fe-5d05-49da-b7c4-95c954e5fec2", + "tables": { + "circuit_device_rows": { + "name": "circuit_device_rows", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_id": { + "name": "circuit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "linked_project_device_id": { + "name": "linked_project_device_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase_type": { + "name": "phase_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_kind": { + "name": "connection_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_group": { + "name": "cost_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_number_snapshot": { + "name": "room_number_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_name_snapshot": { + "name": "room_name_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "manual_quantity": { + "name": "manual_quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "power_per_unit": { + "name": "power_per_unit", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cos_phi": { + "name": "cos_phi", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "overridden_fields": { + "name": "overridden_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "circuit_device_rows_circuit_id_idx": { + "name": "circuit_device_rows_circuit_id_idx", + "columns": [ + "circuit_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "circuit_device_rows_circuit_id_circuits_id_fk": { + "name": "circuit_device_rows_circuit_id_circuits_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "circuits", + "columnsFrom": [ + "circuit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { + "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "project_devices", + "columnsFrom": [ + "linked_project_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "circuit_device_rows_room_id_rooms_id_fk": { + "name": "circuit_device_rows_room_id_rooms_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_list_equipment_identifiers": { + "name": "circuit_list_equipment_identifiers", + "columns": { + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "circuit_list_equipment_identifiers_owner_unique": { + "name": "circuit_list_equipment_identifiers_owner_unique", + "columns": [ + "owner_type", + "owner_id" + ], + "isUnique": true + }, + "circuit_list_equipment_identifiers_list_identifier_unique": { + "name": "circuit_list_equipment_identifiers_list_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk": { + "name": "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuit_list_equipment_identifiers", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_lists": { + "name": "circuit_lists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "distribution_board_id": { + "name": "distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "circuit_lists_distribution_board_id_unique": { + "name": "circuit_lists_distribution_board_id_unique", + "columns": [ + "distribution_board_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_lists_project_id_projects_id_fk": { + "name": "circuit_lists_project_id_projects_id_fk", + "tableFrom": "circuit_lists", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuit_lists_distribution_board_id_distribution_boards_id_fk": { + "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "circuit_lists", + "tableTo": "distribution_boards", + "columnsFrom": [ + "distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_protection_devices": { + "name": "circuit_protection_devices", + "columns": { + "circuit_id": { + "name": "circuit_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rated_current_a": { + "name": "rated_current_a", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fuse_utilization_category": { + "name": "fuse_utilization_category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trip_characteristic": { + "name": "trip_characteristic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_type": { + "name": "rcd_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rated_residual_current_ma": { + "name": "rated_residual_current_ma", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "circuit_protection_devices_circuit_id_circuits_id_fk": { + "name": "circuit_protection_devices_circuit_id_circuits_id_fk", + "tableFrom": "circuit_protection_devices", + "tableTo": "circuits", + "columnsFrom": [ + "circuit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_sections": { + "name": "circuit_sections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_number": { + "name": "group_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "circuit_sections_list_key_unique": { + "name": "circuit_sections_list_key_unique", + "columns": [ + "circuit_list_id", + "key" + ], + "isUnique": true + }, + "circuit_sections_list_prefix_unique": { + "name": "circuit_sections_list_prefix_unique", + "columns": [ + "circuit_list_id", + "prefix" + ], + "isUnique": true + }, + "circuit_sections_list_category_group_unique": { + "name": "circuit_sections_list_category_group_unique", + "columns": [ + "circuit_list_id", + "category", + "group_number" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_sections_circuit_list_id_circuit_lists_id_fk": { + "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuit_sections", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuits": { + "name": "circuits", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cable_type": { + "name": "cable_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cable_cross_section": { + "name": "cable_cross_section", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cable_length": { + "name": "cable_length", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_assignment": { + "name": "rcd_assignment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_designation": { + "name": "terminal_designation", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voltage": { + "name": "voltage", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "control_requirement": { + "name": "control_requirement", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_reserve": { + "name": "is_reserve", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "circuits_section_id_idx": { + "name": "circuits_section_id_idx", + "columns": [ + "section_id" + ], + "isUnique": false + }, + "circuits_list_equipment_identifier_unique": { + "name": "circuits_list_equipment_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuits_circuit_list_id_circuit_lists_id_fk": { + "name": "circuits_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuits", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuits_section_id_circuit_sections_id_fk": { + "name": "circuits_section_id_circuit_sections_id_fk", + "tableFrom": "circuits", + "tableTo": "circuit_sections", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_board_component_protection_devices": { + "name": "distribution_board_component_protection_devices", + "columns": { + "component_id": { + "name": "component_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rated_current_a": { + "name": "rated_current_a", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fuse_utilization_category": { + "name": "fuse_utilization_category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trip_characteristic": { + "name": "trip_characteristic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_type": { + "name": "rcd_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rated_residual_current_ma": { + "name": "rated_residual_current_ma", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk": { + "name": "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk", + "tableFrom": "distribution_board_component_protection_devices", + "tableTo": "distribution_board_components", + "columnsFrom": [ + "component_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_board_components": { + "name": "distribution_board_components", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "placement": { + "name": "placement", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "distribution_board_components_list_identifier_unique": { + "name": "distribution_board_components_list_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + }, + "distribution_board_components_section_role_unique": { + "name": "distribution_board_components_section_role_unique", + "columns": [ + "section_id", + "role" + ], + "isUnique": true + } + }, + "foreignKeys": { + "distribution_board_components_circuit_list_id_circuit_lists_id_fk": { + "name": "distribution_board_components_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "distribution_board_components", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "distribution_board_components_section_id_circuit_sections_id_fk": { + "name": "distribution_board_components_section_id_circuit_sections_id_fk", + "tableFrom": "distribution_board_components", + "tableTo": "circuit_sections", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_boards": { + "name": "distribution_boards", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "floor_id": { + "name": "floor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supply_type": { + "name": "supply_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "distribution_boards_project_id_projects_id_fk": { + "name": "distribution_boards_project_id_projects_id_fk", + "tableFrom": "distribution_boards", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "distribution_boards_floor_id_floors_id_fk": { + "name": "distribution_boards_floor_id_floors_id_fk", + "tableFrom": "distribution_boards", + "tableTo": "floors", + "columnsFrom": [ + "floor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_csv_configurations": { + "name": "external_csv_configurations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration_version": { + "name": "configuration_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration": { + "name": "configuration", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_csv_configurations_project_id_unique": { + "name": "external_csv_configurations_project_id_unique", + "columns": [ + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "external_csv_configurations_project_id_projects_id_fk": { + "name": "external_csv_configurations_project_id_projects_id_fk", + "tableFrom": "external_csv_configurations", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_import_batches": { + "name": "external_import_batches", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "import_kind": { + "name": "import_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imported_at_iso": { + "name": "imported_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_project_revision": { + "name": "applied_project_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration_version": { + "name": "configuration_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration_snapshot": { + "name": "configuration_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "original_bytes": { + "name": "original_bytes", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "document": { + "name": "document", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_import_batches_project_revision_idx": { + "name": "external_import_batches_project_revision_idx", + "columns": [ + "project_id", + "applied_project_revision" + ], + "isUnique": false + }, + "external_import_batches_source_imported_idx": { + "name": "external_import_batches_source_imported_idx", + "columns": [ + "source_id", + "imported_at_iso" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_import_batches_project_id_projects_id_fk": { + "name": "external_import_batches_project_id_projects_id_fk", + "tableFrom": "external_import_batches", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_import_batches_source_id_external_model_sources_id_fk": { + "name": "external_import_batches_source_id_external_model_sources_id_fk", + "tableFrom": "external_import_batches", + "tableTo": "external_model_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_model_objects": { + "name": "external_model_objects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ifc_guid": { + "name": "ifc_guid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen_import_batch_id": { + "name": "last_seen_import_batch_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_accepted_import_batch_id": { + "name": "last_accepted_import_batch_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accepted_source_values": { + "name": "accepted_source_values", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "planning_values": { + "name": "planning_values", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "overridden_fields": { + "name": "overridden_fields", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_room_mapping_id": { + "name": "external_room_mapping_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "distribution_board_id": { + "name": "distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_project_device_id": { + "name": "linked_project_device_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "circuit_device_row_id": { + "name": "circuit_device_row_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "presence_status": { + "name": "presence_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'present'" + } + }, + "indexes": { + "external_model_objects_source_ifc_guid_unique": { + "name": "external_model_objects_source_ifc_guid_unique", + "columns": [ + "source_id", + "ifc_guid" + ], + "isUnique": true + }, + "external_model_objects_project_presence_idx": { + "name": "external_model_objects_project_presence_idx", + "columns": [ + "project_id", + "presence_status" + ], + "isUnique": false + }, + "external_model_objects_distribution_board_idx": { + "name": "external_model_objects_distribution_board_idx", + "columns": [ + "distribution_board_id" + ], + "isUnique": false + }, + "external_model_objects_circuit_device_row_idx": { + "name": "external_model_objects_circuit_device_row_idx", + "columns": [ + "circuit_device_row_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_model_objects_project_id_projects_id_fk": { + "name": "external_model_objects_project_id_projects_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_model_objects_source_id_external_model_sources_id_fk": { + "name": "external_model_objects_source_id_external_model_sources_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_model_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_model_objects_last_seen_import_batch_id_external_import_batches_id_fk": { + "name": "external_model_objects_last_seen_import_batch_id_external_import_batches_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_import_batches", + "columnsFrom": [ + "last_seen_import_batch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "external_model_objects_last_accepted_import_batch_id_external_import_batches_id_fk": { + "name": "external_model_objects_last_accepted_import_batch_id_external_import_batches_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_import_batches", + "columnsFrom": [ + "last_accepted_import_batch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "external_model_objects_external_room_mapping_id_external_room_mappings_id_fk": { + "name": "external_model_objects_external_room_mapping_id_external_room_mappings_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_room_mappings", + "columnsFrom": [ + "external_room_mapping_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_model_objects_distribution_board_id_distribution_boards_id_fk": { + "name": "external_model_objects_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "distribution_boards", + "columnsFrom": [ + "distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_model_objects_linked_project_device_id_project_devices_id_fk": { + "name": "external_model_objects_linked_project_device_id_project_devices_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "project_devices", + "columnsFrom": [ + "linked_project_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_model_objects_circuit_device_row_id_circuit_device_rows_id_fk": { + "name": "external_model_objects_circuit_device_row_id_circuit_device_rows_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "circuit_device_rows", + "columnsFrom": [ + "circuit_device_row_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_model_sources": { + "name": "external_model_sources", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'revit_csv'" + } + }, + "indexes": { + "external_model_sources_project_type_unique": { + "name": "external_model_sources_project_type_unique", + "columns": [ + "project_id", + "source_type" + ], + "isUnique": true + } + }, + "foreignKeys": { + "external_model_sources_project_id_projects_id_fk": { + "name": "external_model_sources_project_id_projects_id_fk", + "tableFrom": "external_model_sources", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_room_mappings": { + "name": "external_room_mappings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "normalized_source_room_key": { + "name": "normalized_source_room_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_floor_name": { + "name": "source_floor_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_room_number": { + "name": "source_room_number", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_room_name": { + "name": "source_room_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_distribution_board_id": { + "name": "default_distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "external_room_mappings_source_key_unique": { + "name": "external_room_mappings_source_key_unique", + "columns": [ + "source_id", + "normalized_source_room_key" + ], + "isUnique": true + }, + "external_room_mappings_project_room_idx": { + "name": "external_room_mappings_project_room_idx", + "columns": [ + "project_id", + "room_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_room_mappings_project_id_projects_id_fk": { + "name": "external_room_mappings_project_id_projects_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_room_mappings_source_id_external_model_sources_id_fk": { + "name": "external_room_mappings_source_id_external_model_sources_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "external_model_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_room_mappings_room_id_rooms_id_fk": { + "name": "external_room_mappings_room_id_rooms_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_room_mappings_default_distribution_board_id_distribution_boards_id_fk": { + "name": "external_room_mappings_default_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "distribution_boards", + "columnsFrom": [ + "default_distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "floors": { + "name": "floors", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "floors_project_id_projects_id_fk": { + "name": "floors_project_id_projects_id_fk", + "tableFrom": "floors", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "global_devices": { + "name": "global_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "installed_power_per_unit_kw": { + "name": "installed_power_per_unit_kw", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "demand_factor": { + "name": "demand_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "voltage_v": { + "name": "voltage_v", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phase_count": { + "name": "phase_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "power_factor": { + "name": "power_factor", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_change_sets": { + "name": "project_change_sets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_revision_id": { + "name": "project_revision_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_type": { + "name": "command_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_schema_version": { + "name": "payload_schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forward_payload_json": { + "name": "forward_payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inverse_payload_json": { + "name": "inverse_payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_change_sets_revision_unique": { + "name": "project_change_sets_revision_unique", + "columns": [ + "project_revision_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_change_sets_project_revision_id_project_revisions_id_fk": { + "name": "project_change_sets_project_revision_id_project_revisions_id_fk", + "tableFrom": "project_change_sets", + "tableTo": "project_revisions", + "columnsFrom": [ + "project_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_devices": { + "name": "project_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase_type": { + "name": "phase_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'single_phase'" + }, + "connection_kind": { + "name": "connection_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_group": { + "name": "cost_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "power_per_unit": { + "name": "power_per_unit", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "cos_phi": { + "name": "cos_phi", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voltage_v": { + "name": "voltage_v", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "project_devices_project_id_projects_id_fk": { + "name": "project_devices_project_id_projects_id_fk", + "tableFrom": "project_devices", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_history_stack_entries": { + "name": "project_history_stack_entries", + "columns": { + "change_set_id": { + "name": "change_set_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stack": { + "name": "stack", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_history_stack_entries_position_unique": { + "name": "project_history_stack_entries_position_unique", + "columns": [ + "project_id", + "stack", + "position" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { + "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", + "tableFrom": "project_history_stack_entries", + "tableTo": "project_change_sets", + "columnsFrom": [ + "change_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_history_stack_entries_project_id_projects_id_fk": { + "name": "project_history_stack_entries_project_id_projects_id_fk", + "tableFrom": "project_history_stack_entries", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "project_history_stack_entries_stack_check": { + "name": "project_history_stack_entries_stack_check", + "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" + } + } + }, + "project_revisions": { + "name": "project_revisions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "project_revisions_project_number_unique": { + "name": "project_revisions_project_number_unique", + "columns": [ + "project_id", + "revision_number" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_revisions_project_id_projects_id_fk": { + "name": "project_revisions_project_id_projects_id_fk", + "tableFrom": "project_revisions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_snapshots": { + "name": "project_snapshots", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_revision": { + "name": "source_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'named'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_sha256": { + "name": "payload_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "project_snapshots_project_created_idx": { + "name": "project_snapshots_project_created_idx", + "columns": [ + "project_id", + "created_at_iso" + ], + "isUnique": false + }, + "project_snapshots_project_kind_revision_idx": { + "name": "project_snapshots_project_kind_revision_idx", + "columns": [ + "project_id", + "kind", + "source_revision" + ], + "isUnique": false + }, + "project_snapshots_project_name_unique": { + "name": "project_snapshots_project_name_unique", + "columns": [ + "project_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_snapshots_project_id_projects_id_fk": { + "name": "project_snapshots_project_id_projects_id_fk", + "tableFrom": "project_snapshots", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_project_number": { + "name": "internal_project_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_project_number": { + "name": "external_project_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "building_owner": { + "name": "building_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_public_building": { + "name": "is_public_building", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "single_phase_voltage_v": { + "name": "single_phase_voltage_v", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 230 + }, + "three_phase_voltage_v": { + "name": "three_phase_voltage_v", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 400 + }, + "enabled_distribution_board_supply_types": { + "name": "enabled_distribution_board_supply_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[\"AV\",\"SV\",\"EV\",\"USV\",\"MSR\",\"SiBe\"]'" + }, + "current_revision": { + "name": "current_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rooms": { + "name": "rooms", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "floor_id": { + "name": "floor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_number": { + "name": "room_number", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "room_name": { + "name": "room_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "rooms_project_id_projects_id_fk": { + "name": "rooms_project_id_projects_id_fk", + "tableFrom": "rooms", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rooms_floor_id_floors_id_fk": { + "name": "rooms_floor_id_floors_id_fk", + "tableFrom": "rooms", + "tableTo": "floors", + "columnsFrom": [ + "floor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 5a65548..eedfc7a 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1785687503453, "tag": "0005_stale_gorilla_man", "breakpoints": true + }, + { + "idx": 6, + "version": "6", + "when": 1786043080323, + "tag": "0006_damp_skrulls", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/repositories/circuit-device-row.persistence.ts b/src/db/repositories/circuit-device-row.persistence.ts index c075f61..4a32623 100644 --- a/src/db/repositories/circuit-device-row.persistence.ts +++ b/src/db/repositories/circuit-device-row.persistence.ts @@ -43,12 +43,6 @@ export interface CircuitDeviceRowPatchInput { overriddenFields?: string | null; } -export interface CircuitDeviceRowCreateInput extends CircuitDeviceRowUpdateInput { - circuitId: string; - linkedProjectDeviceId?: string; - sortOrder: number; -} - export function toCircuitDeviceRowUpdateValues(input: CircuitDeviceRowUpdateInput) { return { linkedProjectDeviceId: input.linkedProjectDeviceId ?? null, @@ -105,15 +99,3 @@ export function toCircuitDeviceRowPatchValues(input: CircuitDeviceRowPatchInput) return values; } - -export function toCircuitDeviceRowCreateValues( - id: string, - input: CircuitDeviceRowCreateInput -) { - return { - id, - circuitId: input.circuitId, - sortOrder: input.sortOrder, - ...toCircuitDeviceRowUpdateValues(input), - }; -} diff --git a/src/db/repositories/circuit-project-command.repository.ts b/src/db/repositories/circuit-project-command.repository.ts index 6f04ffb..bbd6543 100644 --- a/src/db/repositories/circuit-project-command.repository.ts +++ b/src/db/repositories/circuit-project-command.repository.ts @@ -1,4 +1,4 @@ -import { and, eq, ne } from "drizzle-orm"; +import { and, eq } from "drizzle-orm"; import { assertCircuitUpdateProjectCommand, createCircuitUpdateProjectCommand, @@ -21,6 +21,7 @@ import { import { executeProjectCommandTransactionWithAppliedForward } from "./project-command-transaction.persistence.js"; import { resolveCircuitVoltage } from "./project-voltage.persistence.js"; import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; +import { assertEquipmentIdentifierAvailable } from "./equipment-identifier-uniqueness.persistence.js"; type CircuitRow = typeof circuits.$inferSelect; @@ -166,20 +167,12 @@ export class CircuitProjectCommandRepository ) { return; } - const duplicate = database - .select({ id: circuits.id }) - .from(circuits) - .where( - and( - eq(circuits.circuitListId, circuit.circuitListId), - eq(circuits.equipmentIdentifier, equipmentIdentifier), - ne(circuits.id, circuit.id) - ) - ) - .get(); - if (duplicate) { - throw new Error("Duplicate equipmentIdentifier in circuit list."); - } + assertEquipmentIdentifierAvailable( + database, + circuit.circuitListId, + equipmentIdentifier, + circuit.id + ); } } diff --git a/src/db/repositories/circuit-structure-project-command.repository.ts b/src/db/repositories/circuit-structure-project-command.repository.ts index 454028e..9b8941d 100644 --- a/src/db/repositories/circuit-structure-project-command.repository.ts +++ b/src/db/repositories/circuit-structure-project-command.repository.ts @@ -27,6 +27,7 @@ import { } from "./circuit-device-row-structure.persistence.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; import { resolveCircuitVoltage } from "./project-voltage.persistence.js"; +import { assertEquipmentIdentifierAvailable } from "./equipment-identifier-uniqueness.persistence.js"; export class CircuitStructureProjectCommandRepository implements CircuitStructureProjectCommandStore @@ -103,24 +104,11 @@ export class CircuitStructureProjectCommandRepository if (existingCircuit) { throw new Error("Circuit id already exists."); } - const duplicateIdentifier = database - .select({ id: circuits.id }) - .from(circuits) - .where( - and( - eq(circuits.circuitListId, snapshot.circuitListId), - eq( - circuits.equipmentIdentifier, - snapshot.equipmentIdentifier - ) - ) - ) - .get(); - if (duplicateIdentifier) { - throw new Error( - "Duplicate equipmentIdentifier in circuit list." - ); - } + assertEquipmentIdentifierAvailable( + database, + snapshot.circuitListId, + snapshot.equipmentIdentifier + ); if (snapshot.deviceRows.length > 0) { const rowIds = snapshot.deviceRows.map((row) => row.id); diff --git a/src/db/repositories/distribution-board-component-structure-project-command.repository.ts b/src/db/repositories/distribution-board-component-structure-project-command.repository.ts index 2dc3bba..3ea4291 100644 --- a/src/db/repositories/distribution-board-component-structure-project-command.repository.ts +++ b/src/db/repositories/distribution-board-component-structure-project-command.repository.ts @@ -22,6 +22,7 @@ import { circuitSections } from "../schema/circuit-sections.js"; import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js"; import { distributionBoardComponents } from "../schema/distribution-board-components.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { assertEquipmentIdentifierAvailable } from "./equipment-identifier-uniqueness.persistence.js"; export class DistributionBoardComponentStructureProjectCommandRepository implements DistributionBoardComponentStructureProjectCommandStore @@ -94,6 +95,11 @@ export class DistributionBoardComponentStructureProjectCommandRepository if (existing) { throw new Error("Distribution-board component id already exists."); } + assertEquipmentIdentifierAvailable( + database, + snapshot.component.circuitListId, + snapshot.component.equipmentIdentifier + ); database .insert(distributionBoardComponents) .values(snapshot.component) @@ -187,6 +193,17 @@ export class DistributionBoardComponentStructureProjectCommandRepository "Distribution-board component changed before update." ); } + if ( + target.component.equipmentIdentifier !== + expected.component.equipmentIdentifier + ) { + assertEquipmentIdentifierAvailable( + database, + expected.component.circuitListId, + target.component.equipmentIdentifier, + expected.component.id + ); + } database .update(distributionBoardComponents) .set(target.component) diff --git a/src/db/repositories/equipment-identifier-uniqueness.persistence.ts b/src/db/repositories/equipment-identifier-uniqueness.persistence.ts new file mode 100644 index 0000000..43b63ad --- /dev/null +++ b/src/db/repositories/equipment-identifier-uniqueness.persistence.ts @@ -0,0 +1,41 @@ +import { eq } from "drizzle-orm"; +import type { AppDatabase } from "../database-context.js"; +import { circuitListEquipmentIdentifiers } from "../schema/circuit-list-equipment-identifiers.js"; + +export function normalizeEquipmentIdentifier(value: string): string { + return value.trim().toLowerCase(); +} + +/** + * The DB-level normalized unique index uses SQLite's built-in lower(), + * which only folds ASCII a-z and leaves German characters (Ä/Ö/Ü/ß/…) + * untouched, so it alone would let e.g. "Ä1" and "ä1" coexist. This check + * normalizes with JS's Unicode-aware toLowerCase() against every + * identifier already registered for the circuit list (circuits and + * distribution-board components share one BMK namespace via + * circuit_list_equipment_identifiers), catching what the DB index cannot. + */ +export function assertEquipmentIdentifierAvailable( + database: AppDatabase, + circuitListId: string, + equipmentIdentifier: string, + excludeOwnerId?: string +): void { + const candidate = normalizeEquipmentIdentifier(equipmentIdentifier); + const existing = database + .select({ + ownerId: circuitListEquipmentIdentifiers.ownerId, + equipmentIdentifier: circuitListEquipmentIdentifiers.equipmentIdentifier, + }) + .from(circuitListEquipmentIdentifiers) + .where(eq(circuitListEquipmentIdentifiers.circuitListId, circuitListId)) + .all(); + const duplicate = existing.some( + (row) => + row.ownerId !== excludeOwnerId && + normalizeEquipmentIdentifier(row.equipmentIdentifier) === candidate + ); + if (duplicate) { + throw new Error("Duplicate equipmentIdentifier in circuit list."); + } +} diff --git a/src/db/repositories/external-initial-import-project-command.repository.ts b/src/db/repositories/external-initial-import-project-command.repository.ts index 9d2895a..1f2281e 100644 --- a/src/db/repositories/external-initial-import-project-command.repository.ts +++ b/src/db/repositories/external-initial-import-project-command.repository.ts @@ -212,7 +212,9 @@ function replaceExternalState( if (target.roomMappings.length) { database.insert(externalRoomMappings).values(target.roomMappings).run(); } - database.insert(externalModelObjects).values(target.objects).run(); + if (target.objects.length) { + database.insert(externalModelObjects).values(target.objects).run(); + } } function decodeCanonicalBase64(value: string) { diff --git a/src/db/repositories/external-object-new-circuit-project-command.repository.ts b/src/db/repositories/external-object-new-circuit-project-command.repository.ts index c25ac76..bea64ff 100644 --- a/src/db/repositories/external-object-new-circuit-project-command.repository.ts +++ b/src/db/repositories/external-object-new-circuit-project-command.repository.ts @@ -32,6 +32,7 @@ import { loadExpectedExternalObjectTransitions, snapshotsEqual, } from "./external-object-assignment.persistence.js"; +import { assertEquipmentIdentifierAvailable } from "./equipment-identifier-uniqueness.persistence.js"; export class ExternalObjectNewCircuitProjectCommandRepository implements ExternalObjectNewCircuitProjectCommandStore @@ -90,12 +91,11 @@ export class ExternalObjectNewCircuitProjectCommandRepository .where(eq(circuits.id, circuit.id)).get()) { throw new Error("External circuit id already exists."); } - if (database.select({ id: circuits.id }).from(circuits).where(and( - eq(circuits.circuitListId, circuit.circuitListId), - eq(circuits.equipmentIdentifier, circuit.equipmentIdentifier) - )).get()) { - throw new Error("Duplicate equipmentIdentifier in circuit list."); - } + assertEquipmentIdentifierAvailable( + database, + circuit.circuitListId, + circuit.equipmentIdentifier + ); if (database.select({ id: circuitDeviceRows.id }).from(circuitDeviceRows) .where(eq(circuitDeviceRows.id, row.id)).get()) { throw new Error("External device-row id already exists."); diff --git a/src/db/schema/circuit-device-rows.ts b/src/db/schema/circuit-device-rows.ts index a5da01e..f718c5d 100644 --- a/src/db/schema/circuit-device-rows.ts +++ b/src/db/schema/circuit-device-rows.ts @@ -1,35 +1,38 @@ -import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { index, integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core"; import { circuits } from "./circuits.js"; import { projectDevices } from "./project-devices.js"; import { rooms } from "./rooms.js"; -export const circuitDeviceRows = sqliteTable("circuit_device_rows", { - id: text("id").primaryKey(), - circuitId: text("circuit_id") - .notNull() - .references(() => circuits.id, { onDelete: "cascade" }), - linkedProjectDeviceId: text("linked_project_device_id").references(() => projectDevices.id, { - onDelete: "set null", - }), - sortOrder: integer("sort_order").notNull().default(0), - name: text("name").notNull(), - displayName: text("display_name").notNull(), - phaseType: text("phase_type"), - connectionKind: text("connection_kind"), - costGroup: text("cost_group"), - category: text("category"), - level: text("level"), - roomId: text("room_id").references(() => rooms.id, { - onDelete: "set null", - }), - roomNumberSnapshot: text("room_number_snapshot"), - roomNameSnapshot: text("room_name_snapshot"), - quantity: integer("quantity").notNull(), - manualQuantity: integer("manual_quantity").notNull().default(0), - powerPerUnit: real("power_per_unit").notNull(), - simultaneityFactor: real("simultaneity_factor").notNull(), - cosPhi: real("cos_phi"), - remark: text("remark"), - overriddenFields: text("overridden_fields"), -}); - +export const circuitDeviceRows = sqliteTable( + "circuit_device_rows", + { + id: text("id").primaryKey(), + circuitId: text("circuit_id") + .notNull() + .references(() => circuits.id, { onDelete: "cascade" }), + linkedProjectDeviceId: text("linked_project_device_id").references(() => projectDevices.id, { + onDelete: "set null", + }), + sortOrder: integer("sort_order").notNull().default(0), + name: text("name").notNull(), + displayName: text("display_name").notNull(), + phaseType: text("phase_type"), + connectionKind: text("connection_kind"), + costGroup: text("cost_group"), + category: text("category"), + level: text("level"), + roomId: text("room_id").references(() => rooms.id, { + onDelete: "set null", + }), + roomNumberSnapshot: text("room_number_snapshot"), + roomNameSnapshot: text("room_name_snapshot"), + quantity: integer("quantity").notNull(), + manualQuantity: integer("manual_quantity").notNull().default(0), + powerPerUnit: real("power_per_unit").notNull(), + simultaneityFactor: real("simultaneity_factor").notNull(), + cosPhi: real("cos_phi"), + remark: text("remark"), + overriddenFields: text("overridden_fields"), + }, + (table) => [index("circuit_device_rows_circuit_id_idx").on(table.circuitId)] +); diff --git a/src/db/schema/circuits.ts b/src/db/schema/circuits.ts index c0c733c..6e3ed3e 100644 --- a/src/db/schema/circuits.ts +++ b/src/db/schema/circuits.ts @@ -1,4 +1,4 @@ -import { integer, real, sqliteTable, text, unique } from "drizzle-orm/sqlite-core"; +import { index, integer, real, sqliteTable, text, unique } from "drizzle-orm/sqlite-core"; import { circuitLists } from "./circuit-lists.js"; import { circuitSections } from "./circuit-sections.js"; @@ -26,6 +26,9 @@ export const circuits = sqliteTable( isReserve: integer("is_reserve").notNull().default(0), remark: text("remark"), }, - (table) => [unique("circuits_list_equipment_identifier_unique").on(table.circuitListId, table.equipmentIdentifier)] + (table) => [ + unique("circuits_list_equipment_identifier_unique").on(table.circuitListId, table.equipmentIdentifier), + index("circuits_section_id_idx").on(table.sectionId), + ] ); diff --git a/src/domain/models/circuit-device-row-project-command.model.ts b/src/domain/models/circuit-device-row-project-command.model.ts index 825701f..8e949a1 100644 --- a/src/domain/models/circuit-device-row-project-command.model.ts +++ b/src/domain/models/circuit-device-row-project-command.model.ts @@ -144,6 +144,9 @@ function assertCircuitDeviceRowUpdateFieldValue( if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { throw new Error(`${field} must be a non-negative finite number.`); } + if (field === "simultaneityFactor" && value > 1) { + throw new Error("simultaneityFactor must not exceed 1."); + } return; } if (field === "cosPhi") { diff --git a/src/domain/models/circuit-device-row-structure-project-command.model.ts b/src/domain/models/circuit-device-row-structure-project-command.model.ts index d6e3cf6..2464a27 100644 --- a/src/domain/models/circuit-device-row-structure-project-command.model.ts +++ b/src/domain/models/circuit-device-row-structure-project-command.model.ts @@ -134,6 +134,9 @@ export function assertCircuitDeviceRowInsertProjectCommand( row.simultaneityFactor, "row.simultaneityFactor" ); + if (row.simultaneityFactor > 1) { + throw new Error("row.simultaneityFactor must not exceed 1."); + } if (row.cosPhi !== null) { assertFiniteNumber(row.cosPhi, "row.cosPhi"); if (row.cosPhi <= 0) { diff --git a/src/domain/models/circuit-device-row.model.ts b/src/domain/models/circuit-device-row.model.ts deleted file mode 100644 index f18c63c..0000000 --- a/src/domain/models/circuit-device-row.model.ts +++ /dev/null @@ -1,24 +0,0 @@ -export interface CircuitDeviceRow { - id: string; - circuitId: string; - linkedProjectDeviceId?: string; - sortOrder: number; - name: string; - displayName: string; - phaseType?: string; - connectionKind?: string; - costGroup?: string; - category?: string; - level?: string; - roomId?: string; - roomNumberSnapshot?: string; - roomNameSnapshot?: string; - quantity: number; - manualQuantity: number; - powerPerUnit: number; - simultaneityFactor: number; - cosPhi?: number; - remark?: string; - overriddenFields?: string; -} - diff --git a/src/domain/models/circuit-protection-project-command.model.ts b/src/domain/models/circuit-protection-project-command.model.ts index 2e8e6f4..0c8a19e 100644 --- a/src/domain/models/circuit-protection-project-command.model.ts +++ b/src/domain/models/circuit-protection-project-command.model.ts @@ -71,15 +71,33 @@ export function assertCircuitProtectionUpdateProjectCommand( if (target !== null) { assertCircuitProtectionSnapshot(target, circuitId); } - if (JSON.stringify(expected) === JSON.stringify(target)) { + if (circuitProtectionSnapshotsEqual(expected, target)) { throw new Error("Circuit protection update must change state."); } } +function circuitProtectionSnapshotsEqual( + left: CircuitProtectionSnapshot | null, + right: CircuitProtectionSnapshot | null +): boolean { + if (left === null || right === null) { + return left === right; + } + return ( + left.circuitId === right.circuitId && + left.type === right.type && + left.ratedCurrentA === right.ratedCurrentA && + left.fuseUtilizationCategory === right.fuseUtilizationCategory && + left.tripCharacteristic === right.tripCharacteristic && + left.rcdType === right.rcdType && + left.ratedResidualCurrentMa === right.ratedResidualCurrentMa + ); +} + export function assertCircuitProtectionSnapshot( value: unknown, circuitId: string -) { +): asserts value is CircuitProtectionSnapshot { if ( !isPlainObject(value) || Object.keys(value).length !== 7 || diff --git a/src/domain/models/circuit-section.model.ts b/src/domain/models/circuit-section.model.ts deleted file mode 100644 index bba2c4e..0000000 --- a/src/domain/models/circuit-section.model.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface CircuitSection { - id: string; - circuitListId: string; - key: string; - displayName: string; - prefix: string; - sortOrder: number; -} - diff --git a/src/domain/models/circuit.model.ts b/src/domain/models/circuit.model.ts deleted file mode 100644 index 3efe12c..0000000 --- a/src/domain/models/circuit.model.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface Circuit { - id: string; - circuitListId: string; - sectionId: string; - equipmentIdentifier: string; - displayName?: string; - sortOrder: number; - cableType?: string; - cableCrossSection?: string; - cableLength?: number; - rcdAssignment?: string; - terminalDesignation?: string; - voltage?: number; - controlRequirement?: string; - status?: string; - isReserve: boolean; - remark?: string; -} - diff --git a/src/domain/models/project-state-snapshot.model.ts b/src/domain/models/project-state-snapshot.model.ts index 060c795..7172cb7 100644 --- a/src/domain/models/project-state-snapshot.model.ts +++ b/src/domain/models/project-state-snapshot.model.ts @@ -130,7 +130,7 @@ const circuitDeviceRowSchema = z.preprocess( quantity: finiteNumberSchema.nonnegative(), manualQuantity: finiteNumberSchema.nonnegative(), powerPerUnit: finiteNumberSchema.nonnegative(), - simultaneityFactor: finiteNumberSchema.nonnegative(), + simultaneityFactor: finiteNumberSchema.min(0).max(1), cosPhi: finiteNumberSchema.positive().nullable(), remark: nullableStringSchema, overriddenFields: nullableStringSchema, diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx index b63cd8e..03aa38d 100644 --- a/src/frontend/components/circuit-tree-editor.tsx +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -268,6 +268,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const [editingCell, setEditingCell] = useState(null); const [activeSectionId, setActiveSectionId] = useState(null); const [isSaving, setIsSaving] = useState(false); + // Synchronous re-entry guard: isSaving is React state and only reflects + // reality after the next render, so a second click/drop fired within the + // same tick could otherwise race past it and double-submit a command. + const commandInFlightRef = useRef(false); const [componentEditorIntent, setComponentEditorIntent] = useState(null); const [circuitGroupEditorIntent, setCircuitGroupEditorIntent] = @@ -723,6 +727,27 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str ); }, [data]); + // Clears the sidebar's target selection once it no longer resolves to a + // real section/circuit (e.g. deleted, moved or renumbered elsewhere) + // instead of silently holding a stale id after a tree reload. + useEffect(() => { + if (!data) { + return; + } + if ( + targetSectionId && + !data.sections.some((section) => section.id === targetSectionId) + ) { + setTargetSectionId(null); + } + if ( + targetCircuitId && + !circuitOptions.some((option) => option.id === targetCircuitId) + ) { + setTargetCircuitId(null); + } + }, [data, circuitOptions, targetSectionId, targetCircuitId]); + const allCircuits = useMemo( () => data?.sections.flatMap((section) => section.circuits) ?? [], [data] @@ -1046,6 +1071,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str // Runs a normal command. The server records it in project-wide history. async function runCommand(command: HistoryCommand) { + if (commandInFlightRef.current) { + return; + } + commandInFlightRef.current = true; try { setError(null); setIsSaving(true); @@ -1056,6 +1085,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str await loadTree({ showLoading: false }); setError(message); } finally { + commandInFlightRef.current = false; setIsSaving(false); } } @@ -1063,6 +1093,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str // Applies the next eligible project-wide history operation. Selection is only // a best-effort local hint; command eligibility and data changes stay server-owned. async function applyHistory(mode: "undo" | "redo") { + if (commandInFlightRef.current) { + return; + } + commandInFlightRef.current = true; try { setError(null); setHistoryBusy(true); @@ -1088,6 +1122,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str await loadTree({ showLoading: false }); setError(message); } finally { + commandInFlightRef.current = false; setIsSaving(false); setHistoryBusy(false); } @@ -1744,7 +1779,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str if (!section) { throw new Error("Bereich wurde nicht gefunden."); } - const next = await getNextCircuitIdentifier(sectionId); + const next = await getNextCircuitIdentifier(projectId, sectionId); const sortOrder = section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10; const isDeviceField = deviceFieldKeys.has(key); @@ -1933,7 +1968,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str await runCommand({ label: "Stromkreis hinzufügen", redo: async () => { - const next = await getNextCircuitIdentifier(sectionId); + const next = await getNextCircuitIdentifier(projectId, sectionId); const sortOrder = getInsertionSortOrder(section.circuits, afterCircuitId); const circuit = createCircuitSnapshot({ sectionId, @@ -2127,7 +2162,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str if (!section) { throw new Error("Der Zielbereich ist ungültig."); } - const next = await getNextCircuitIdentifier(sectionId); + const next = await getNextCircuitIdentifier(projectId, sectionId); const sortOrder = section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10; const circuit = createCircuitSnapshot( @@ -2538,7 +2573,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str await runCommand({ label: newCircuitLabel, redo: async () => { - const next = await getNextCircuitIdentifier(intent.sectionId); + const next = await getNextCircuitIdentifier(projectId, intent.sectionId); const sortOrder = intent.targetCircuitId && intent.placement ? getAdjacentInsertionSortOrder( @@ -3699,6 +3734,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str type="button" tabIndex={-1} disabled={ + isSaving || !buildCircuitGroupReorderAssignments( data.sections, section.id, @@ -3716,6 +3752,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str type="button" tabIndex={-1} disabled={ + isSaving || !buildCircuitGroupReorderAssignments( data.sections, section.id, @@ -3733,6 +3770,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str type="button" tabIndex={-1} disabled={ + isSaving || hasActiveSortOrFilter || !section.category || !canRenumberCircuitGroups( @@ -3758,6 +3796,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str - ) : null} diff --git a/src/frontend/components/form-modal.tsx b/src/frontend/components/form-modal.tsx index 05e18eb..d893690 100644 --- a/src/frontend/components/form-modal.tsx +++ b/src/frontend/components/form-modal.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { type FormEvent, type ReactNode } from "react"; +import React, { type FormEvent, type ReactNode, useEffect, useRef } from "react"; interface FormModalProps { children: ReactNode; @@ -14,6 +14,9 @@ interface FormModalProps { title: string; } +const FOCUSABLE_SELECTOR = + 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'; + export function FormModal({ children, description, @@ -25,11 +28,56 @@ export function FormModal({ submitLabel, title, }: FormModalProps) { + const dialogRef = useRef(null); + + // Focuses the dialog on open and returns focus to the element that + // triggered it on close, so keyboard users never lose their place in the + // grid behind the backdrop. + useEffect(() => { + const previouslyFocused = document.activeElement as HTMLElement | null; + const firstFocusable = + dialogRef.current?.querySelector(FOCUSABLE_SELECTOR); + firstFocusable?.focus(); + return () => { + previouslyFocused?.focus(); + }; + }, []); + + function handleKeyDown(event: React.KeyboardEvent) { + if (event.key === "Escape") { + if (!isSaving) { + event.stopPropagation(); + onClose(); + } + return; + } + if (event.key !== "Tab" || !dialogRef.current) { + return; + } + const focusable = Array.from( + dialogRef.current.querySelectorAll(FOCUSABLE_SELECTOR) + ); + if (focusable.length === 0) { + return; + } + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (event.shiftKey && document.activeElement === first) { + event.preventDefault(); + last.focus(); + } else if (!event.shiftKey && document.activeElement === last) { + event.preventDefault(); + first.focus(); + } + } + return ( <>
diff --git a/src/frontend/components/project-settings-modal.tsx b/src/frontend/components/project-settings-modal.tsx index 77295df..4af751c 100644 --- a/src/frontend/components/project-settings-modal.tsx +++ b/src/frontend/components/project-settings-modal.tsx @@ -7,6 +7,7 @@ import { distributionBoardSupplyTypes, type DistributionBoardSupplyType, } from "../../shared/constants/distribution-board"; +import { FormModal } from "./form-modal"; export interface ProjectSettingsInput { name: string; @@ -131,319 +132,278 @@ export function ProjectSettingsModal({ } return ( - <> -
-
- -
-
-

- Projekteinstellungen -

-

- Stammdaten und elektrische Standardwerte des Projekts -

-
-
-
-
-
- - setName(event.target.value)} - required - value={name} - /> -
-
- - - setInternalProjectNumber(event.target.value) - } - value={internalProjectNumber} - /> -
-
- - - setExternalProjectNumber(event.target.value) - } - value={externalProjectNumber} - /> -
-
-
- - Verwendete Netzarten - -

- Nur ausgewählte Netzarten stehen bei Verteilungen zur - Auswahl. Bereits verwendete Netzarten können nicht - deaktiviert werden. -

-
- {distributionBoardSupplyTypes.map((supplyType) => ( -
- -
- ))} -
- {enabledDistributionBoardSupplyTypes.length === 0 ? ( -
- Mindestens eine Netzart muss aktiviert sein. -
- ) : null} -
-
-
- - setBuildingOwner(event.target.value)} - value={buildingOwner} - /> -
-
- -