From b893c725343c9f5e2bca8e64fdf88c4fa528c0a4 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 27 Sep 2026 22:21:58 +0200 Subject: [PATCH] Add logical distribution board feed links --- src/db/migrations/0012_good_gateway.sql | 4 + src/db/migrations/meta/0012_snapshot.json | 2986 +++++++++++++++++ src/db/migrations/meta/_journal.json | 7 + ...rd-structure-project-command.repository.ts | 95 +- ...oard-subtree-project-command.repository.ts | 63 + ...roject-state-restore-command.repository.ts | 44 +- .../project-transfer.repository.ts | 29 +- src/db/schema/circuit-lists.ts | 4 +- src/db/schema/circuits.ts | 4 +- src/db/schema/distribution-boards.ts | 58 +- ...istribution-board-project-command.model.ts | 42 +- ...stribution-board-subtree-snapshot.model.ts | 17 + .../models/project-state-snapshot.model.ts | 112 +- src/domain/models/project-transfer.model.ts | 8 + src/frontend/types.ts | 2 + src/frontend/utils/api.ts | 8 +- .../distribution-board.controller.ts | 14 + .../validation/project-structure.schemas.ts | 2 + ...ructure-project-command.repository.test.ts | 153 + tests/distribution-board-subtree.test.ts | 7 + tests/print-view-migration.test.ts | 2 + tests/project-snapshot.repository.test.ts | 9 +- tests/project-state-snapshot.test.ts | 107 +- 23 files changed, 3732 insertions(+), 45 deletions(-) create mode 100644 src/db/migrations/0012_good_gateway.sql create mode 100644 src/db/migrations/meta/0012_snapshot.json diff --git a/src/db/migrations/0012_good_gateway.sql b/src/db/migrations/0012_good_gateway.sql new file mode 100644 index 0000000..57ad9dc --- /dev/null +++ b/src/db/migrations/0012_good_gateway.sql @@ -0,0 +1,4 @@ +ALTER TABLE `distribution_boards` ADD `feeding_distribution_board_id` text REFERENCES distribution_boards(id);--> statement-breakpoint +ALTER TABLE `distribution_boards` ADD `feeding_circuit_id` text REFERENCES circuits(id);--> statement-breakpoint +CREATE INDEX `distribution_boards_feeding_board_idx` ON `distribution_boards` (`feeding_distribution_board_id`);--> statement-breakpoint +CREATE INDEX `distribution_boards_feeding_circuit_idx` ON `distribution_boards` (`feeding_circuit_id`); \ No newline at end of file diff --git a/src/db/migrations/meta/0012_snapshot.json b/src/db/migrations/meta/0012_snapshot.json new file mode 100644 index 0000000..832cf1a --- /dev/null +++ b/src/db/migrations/meta/0012_snapshot.json @@ -0,0 +1,2986 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "8029e6ab-56de-4345-8d38-0ca7462640de", + "prevId": "63f27b3a-7378-462d-8119-b67d67903dbb", + "tables": { + "buildings": { + "name": "buildings", + "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": { + "buildings_project_id_projects_id_fk": { + "name": "buildings_project_id_projects_id_fk", + "tableFrom": "buildings", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "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 + }, + "board_number": { + "name": "board_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fed_from": { + "name": "fed_from", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "feeding_distribution_board_id": { + "name": "feeding_distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "feeding_circuit_id": { + "name": "feeding_circuit_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "distribution_boards_feeding_board_idx": { + "name": "distribution_boards_feeding_board_idx", + "columns": [ + "feeding_distribution_board_id" + ], + "isUnique": false + }, + "distribution_boards_feeding_circuit_idx": { + "name": "distribution_boards_feeding_circuit_idx", + "columns": [ + "feeding_circuit_id" + ], + "isUnique": false + } + }, + "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" + }, + "distribution_boards_feeding_distribution_board_id_distribution_boards_id_fk": { + "name": "distribution_boards_feeding_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "distribution_boards", + "tableTo": "distribution_boards", + "columnsFrom": [ + "feeding_distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "distribution_boards_feeding_circuit_id_circuits_id_fk": { + "name": "distribution_boards_feeding_circuit_id_circuits_id_fk", + "tableFrom": "distribution_boards", + "tableTo": "circuits", + "columnsFrom": [ + "feeding_circuit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "document_branding_assets": { + "name": "document_branding_assets", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "document_branding_settings": { + "name": "document_branding_settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "estimate_balances": { + "name": "estimate_balances", + "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 + }, + "work_stage": { + "name": "work_stage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "date_iso": { + "name": "date_iso", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reserve_av_percent": { + "name": "reserve_av_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reserve_sv_percent": { + "name": "reserve_sv_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "estimate_balances_project_id_idx": { + "name": "estimate_balances_project_id_idx", + "columns": [ + "project_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "estimate_balances_project_id_projects_id_fk": { + "name": "estimate_balances_project_id_projects_id_fk", + "tableFrom": "estimate_balances", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "estimate_groups": { + "name": "estimate_groups", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "balance_id": { + "name": "balance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cost_group": { + "name": "cost_group", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "editor": { + "name": "editor", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "estimate_groups_balance_id_idx": { + "name": "estimate_groups_balance_id_idx", + "columns": [ + "balance_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "estimate_groups_balance_id_estimate_balances_id_fk": { + "name": "estimate_groups_balance_id_estimate_balances_id_fk", + "tableFrom": "estimate_groups", + "tableTo": "estimate_balances", + "columnsFrom": [ + "balance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "estimate_rows": { + "name": "estimate_rows", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "supply": { + "name": "supply", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "quantity": { + "name": "quantity", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'piece'" + }, + "connection": { + "name": "connection", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "power_per_unit_w": { + "name": "power_per_unit_w", + "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": true, + "autoincrement": false, + "default": 1 + }, + "safety_supply": { + "name": "safety_supply", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "estimate_rows_group_id_idx": { + "name": "estimate_rows_group_id_idx", + "columns": [ + "group_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "estimate_rows_group_id_estimate_groups_id_fk": { + "name": "estimate_rows_group_id_estimate_groups_id_fk", + "tableFrom": "estimate_rows", + "tableTo": "estimate_groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "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 + }, + "building_id": { + "name": "building_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "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" + }, + "floors_building_id_buildings_id_fk": { + "name": "floors_building_id_buildings_id_fk", + "tableFrom": "floors", + "tableTo": "buildings", + "columnsFrom": [ + "building_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "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\"]'" + }, + "site_address": { + "name": "site_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "work_stage": { + "name": "work_stage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_lead": { + "name": "project_lead", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_editor": { + "name": "project_editor", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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 + }, + "area_m2": { + "name": "area_m2", + "type": "real", + "primaryKey": false, + "notNull": false, + "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 90c3bd0..548734d 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -85,6 +85,13 @@ "when": 1790531351492, "tag": "0011_glossy_norman_osborn", "breakpoints": true + }, + { + "idx": 12, + "version": "6", + "when": 1790540142918, + "tag": "0012_good_gateway", + "breakpoints": true } ] } \ No newline at end of file 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 db2cede..a4fba2b 100644 --- a/src/db/repositories/distribution-board-structure-project-command.repository.ts +++ b/src/db/repositories/distribution-board-structure-project-command.repository.ts @@ -325,6 +325,16 @@ export class DistributionBoardStructureProjectCommandRepository patch.supplyType ); } + this.assertFeedingReference(database, input.projectId, current.id, { + feedingDistributionBoardId: + patch.feedingDistributionBoardId === undefined + ? current.feedingDistributionBoardId + : patch.feedingDistributionBoardId, + feedingCircuitId: + patch.feedingCircuitId === undefined + ? current.feedingCircuitId + : patch.feedingCircuitId, + }); const inversePatch = Object.fromEntries( input.command.payload.changes.map((change) => [ change.field, @@ -333,7 +343,8 @@ export class DistributionBoardStructureProjectCommandRepository ) as DistributionBoardUpdatePatch; const inverse = createDistributionBoardUpdateProjectCommand( current.id, - inversePatch + inversePatch, + input.command.schemaVersion ); const updated = database .update(distributionBoards) @@ -352,6 +363,77 @@ export class DistributionBoardStructureProjectCommandRepository } return inverse; } + + private assertFeedingReference( + database: AppDatabase, + projectId: string, + distributionBoardId: string, + reference: { + feedingDistributionBoardId: string | null; + feedingCircuitId: string | null; + } + ) { + const { feedingDistributionBoardId, feedingCircuitId } = reference; + if (feedingDistributionBoardId === null) { + if (feedingCircuitId !== null) { + throw new Error( + "Ein speisender Stromkreis erfordert eine speisende Verteilung." + ); + } + return; + } + if (feedingDistributionBoardId === distributionBoardId) { + throw new Error("Eine Verteilung kann sich nicht selbst speisen."); + } + const projectBoards = database + .select({ + id: distributionBoards.id, + feedingDistributionBoardId: + distributionBoards.feedingDistributionBoardId, + }) + .from(distributionBoards) + .where(eq(distributionBoards.projectId, projectId)) + .all(); + const boardById = new Map(projectBoards.map((board) => [board.id, board])); + if (!boardById.has(feedingDistributionBoardId)) { + throw new Error( + "Die speisende Verteilung gehört nicht zum Projekt." + ); + } + const visited = new Set([distributionBoardId]); + let currentId: string | null = feedingDistributionBoardId; + while (currentId !== null) { + if (visited.has(currentId)) { + throw new Error("Die Einspeisungsverknüpfung würde einen Zyklus erzeugen."); + } + visited.add(currentId); + currentId = boardById.get(currentId)?.feedingDistributionBoardId ?? null; + } + if (feedingCircuitId === null) { + return; + } + const feedingCircuit = database + .select({ + projectId: circuitLists.projectId, + distributionBoardId: circuitLists.distributionBoardId, + }) + .from(circuits) + .innerJoin( + circuitLists, + eq(circuitLists.id, circuits.circuitListId) + ) + .where(eq(circuits.id, feedingCircuitId)) + .get(); + if ( + !feedingCircuit || + feedingCircuit.projectId !== projectId || + feedingCircuit.distributionBoardId !== feedingDistributionBoardId + ) { + throw new Error( + "Der speisende Stromkreis gehört nicht zur gewählten Verteilung." + ); + } + } } function assertSupplyTypeEnabled( @@ -392,11 +474,20 @@ function structureMatches( location, fedFrom, remark, + feedingDistributionBoardId, + feedingCircuitId, ...actualDistributionBoard } = actual.board; if ( simultaneityFactor !== 1 || - [boardNumber, location, fedFrom, remark].some((value) => value !== null) || + [ + boardNumber, + location, + fedFrom, + remark, + feedingDistributionBoardId, + feedingCircuitId, + ].some((value) => value !== null) || !sameRecord( expected.distributionBoard, actualDistributionBoard 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 6788dcc..5d33b75 100644 --- a/src/db/repositories/distribution-board-subtree-project-command.repository.ts +++ b/src/db/repositories/distribution-board-subtree-project-command.repository.ts @@ -300,6 +300,69 @@ export class DistributionBoardSubtreeProjectCommandRepository ); } } + const feedingDistributionBoardId = + snapshot.distributionBoard.feedingDistributionBoardId; + const feedingCircuitId = snapshot.distributionBoard.feedingCircuitId; + if (feedingDistributionBoardId === null) { + if (feedingCircuitId !== null) { + throw new Error( + "Ein speisender Stromkreis erfordert eine speisende Verteilung." + ); + } + } else { + if (feedingDistributionBoardId === snapshot.distributionBoard.id) { + throw new Error("Eine Verteilung kann sich nicht selbst speisen."); + } + const projectBoards = database + .select({ + id: distributionBoards.id, + feedingDistributionBoardId: + distributionBoards.feedingDistributionBoardId, + }) + .from(distributionBoards) + .where(eq(distributionBoards.projectId, projectId)) + .all(); + const boardById = new Map(projectBoards.map((board) => [board.id, board])); + if (!boardById.has(feedingDistributionBoardId)) { + throw new Error( + "Die speisende Verteilung gehört nicht zum Projekt." + ); + } + const visited = new Set([snapshot.distributionBoard.id]); + let currentId: string | null = feedingDistributionBoardId; + while (currentId !== null) { + if (visited.has(currentId)) { + throw new Error( + "Die Einspeisungsverknüpfung würde einen Zyklus erzeugen." + ); + } + visited.add(currentId); + currentId = boardById.get(currentId)?.feedingDistributionBoardId ?? null; + } + if (feedingCircuitId !== null) { + const feedingCircuit = database + .select({ + projectId: circuitLists.projectId, + distributionBoardId: circuitLists.distributionBoardId, + }) + .from(circuits) + .innerJoin( + circuitLists, + eq(circuitLists.id, circuits.circuitListId) + ) + .where(eq(circuits.id, feedingCircuitId)) + .get(); + if ( + !feedingCircuit || + feedingCircuit.projectId !== projectId || + feedingCircuit.distributionBoardId !== feedingDistributionBoardId + ) { + throw new Error( + "Der speisende Stromkreis gehört nicht zur gewählten Verteilung." + ); + } + } + } const linkedProjectDeviceIds = uniqueStrings( snapshot.circuits.flatMap((circuit) => circuit.deviceRows.flatMap((row) => diff --git a/src/db/repositories/project-state-restore-command.repository.ts b/src/db/repositories/project-state-restore-command.repository.ts index 97cfe17..386d805 100644 --- a/src/db/repositories/project-state-restore-command.repository.ts +++ b/src/db/repositories/project-state-restore-command.repository.ts @@ -270,6 +270,14 @@ function replaceProjectState( database: AppDatabase, state: ProjectStateSnapshot ) { + database + .update(distributionBoards) + .set({ + feedingDistributionBoardId: null, + feedingCircuitId: null, + }) + .where(eq(distributionBoards.projectId, state.project.id)) + .run(); database .delete(externalModelSources) .where(eq(externalModelSources.projectId, state.project.id)) @@ -330,7 +338,15 @@ function replaceProjectState( } insertMany(database, rooms, state.rooms); insertMany(database, projectDevices, state.projectDevices); - insertMany(database, distributionBoards, state.distributionBoards); + insertMany( + database, + distributionBoards, + state.distributionBoards.map((board) => ({ + ...board, + feedingDistributionBoardId: null, + feedingCircuitId: null, + })) + ); insertMany(database, circuitLists, state.circuitLists); insertMany(database, circuitSections, state.circuitSections); insertMany( @@ -341,6 +357,7 @@ function replaceProjectState( isReserve: circuit.isReserve ? 1 : 0, })) ); + restoreDistributionBoardFeedingReferences(database, state); insertMany( database, distributionBoardComponents, @@ -378,6 +395,31 @@ function replaceProjectState( insertMany(database, externalModelObjects, state.externalModel.objects); } +function restoreDistributionBoardFeedingReferences( + database: AppDatabase, + state: ProjectStateSnapshot +) { + for (const board of state.distributionBoards) { + if ( + board.feedingDistributionBoardId === null && + board.feedingCircuitId === null + ) { + continue; + } + const updated = database + .update(distributionBoards) + .set({ + feedingDistributionBoardId: board.feedingDistributionBoardId, + feedingCircuitId: board.feedingCircuitId, + }) + .where(eq(distributionBoards.id, board.id)) + .run(); + if (updated.changes !== 1) { + throw new Error("Distribution-board feeding reference could not be restored."); + } + } +} + function insertMany[0]>( database: AppDatabase, table: TTable, diff --git a/src/db/repositories/project-transfer.repository.ts b/src/db/repositories/project-transfer.repository.ts index 21647ff..1d93afd 100644 --- a/src/db/repositories/project-transfer.repository.ts +++ b/src/db/repositories/project-transfer.repository.ts @@ -173,7 +173,15 @@ function insertProjectState( insertMany(database, floors, state.floors); insertMany(database, rooms, state.rooms); insertMany(database, projectDevices, state.projectDevices); - insertMany(database, distributionBoards, state.distributionBoards); + insertMany( + database, + distributionBoards, + state.distributionBoards.map((board) => ({ + ...board, + feedingDistributionBoardId: null, + feedingCircuitId: null, + })) + ); insertMany(database, circuitLists, state.circuitLists); insertMany(database, circuitSections, state.circuitSections); insertMany( @@ -184,6 +192,25 @@ function insertProjectState( isReserve: circuit.isReserve ? 1 : 0, })) ); + for (const board of state.distributionBoards) { + if ( + board.feedingDistributionBoardId === null && + board.feedingCircuitId === null + ) { + continue; + } + const updated = database + .update(distributionBoards) + .set({ + feedingDistributionBoardId: board.feedingDistributionBoardId, + feedingCircuitId: board.feedingCircuitId, + }) + .where(eq(distributionBoards.id, board.id)) + .run(); + if (updated.changes !== 1) { + throw new Error("Distribution-board feeding reference could not be imported."); + } + } insertMany( database, distributionBoardComponents, diff --git a/src/db/schema/circuit-lists.ts b/src/db/schema/circuit-lists.ts index 0af5138..2283aeb 100644 --- a/src/db/schema/circuit-lists.ts +++ b/src/db/schema/circuit-lists.ts @@ -1,4 +1,4 @@ -import { sqliteTable, text, unique } from "drizzle-orm/sqlite-core"; +import { sqliteTable, text, unique, type AnySQLiteColumn } from "drizzle-orm/sqlite-core"; import { distributionBoards } from "./distribution-boards.js"; import { projects } from "./projects.js"; @@ -11,7 +11,7 @@ export const circuitLists = sqliteTable( .references(() => projects.id, { onDelete: "cascade" }), distributionBoardId: text("distribution_board_id") .notNull() - .references(() => distributionBoards.id, { onDelete: "cascade" }), + .references((): AnySQLiteColumn => distributionBoards.id, { onDelete: "cascade" }), name: text("name").notNull(), }, (table) => [unique("circuit_lists_distribution_board_id_unique").on(table.distributionBoardId)] diff --git a/src/db/schema/circuits.ts b/src/db/schema/circuits.ts index 6e3ed3e..15ef0e3 100644 --- a/src/db/schema/circuits.ts +++ b/src/db/schema/circuits.ts @@ -1,4 +1,4 @@ -import { index, integer, real, sqliteTable, text, unique } from "drizzle-orm/sqlite-core"; +import { index, integer, real, sqliteTable, text, unique, type AnySQLiteColumn } from "drizzle-orm/sqlite-core"; import { circuitLists } from "./circuit-lists.js"; import { circuitSections } from "./circuit-sections.js"; @@ -8,7 +8,7 @@ export const circuits = sqliteTable( id: text("id").primaryKey(), circuitListId: text("circuit_list_id") .notNull() - .references(() => circuitLists.id, { onDelete: "cascade" }), + .references((): AnySQLiteColumn => circuitLists.id, { onDelete: "cascade" }), sectionId: text("section_id") .notNull() .references(() => circuitSections.id, { onDelete: "cascade" }), diff --git a/src/db/schema/distribution-boards.ts b/src/db/schema/distribution-boards.ts index eb63486..60bbe4d 100644 --- a/src/db/schema/distribution-boards.ts +++ b/src/db/schema/distribution-boards.ts @@ -1,24 +1,44 @@ -import { real, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { index, real, sqliteTable, text, type AnySQLiteColumn } from "drizzle-orm/sqlite-core"; +import { circuits } from "./circuits.js"; import { floors } from "./floors.js"; import { projects } from "./projects.js"; import type { DistributionBoardSupplyType } from "../../shared/constants/distribution-board.js"; -export const distributionBoards = sqliteTable("distribution_boards", { - id: text("id").primaryKey(), - projectId: text("project_id") - .notNull() - .references(() => projects.id, { onDelete: "cascade" }), - name: text("name").notNull(), - floorId: text("floor_id").references(() => floors.id, { - onDelete: "set null", - }), - supplyType: text("supply_type").$type(), - simultaneityFactor: real("simultaneity_factor") - .notNull() - .default(1), - boardNumber: text("board_number"), - location: text("location"), - fedFrom: text("fed_from"), - remark: text("remark"), -}); +export const distributionBoards = sqliteTable( + "distribution_boards", + { + id: text("id").primaryKey(), + projectId: text("project_id") + .notNull() + .references(() => projects.id, { onDelete: "cascade" }), + name: text("name").notNull(), + floorId: text("floor_id").references(() => floors.id, { + onDelete: "set null", + }), + supplyType: text("supply_type").$type(), + simultaneityFactor: real("simultaneity_factor") + .notNull() + .default(1), + boardNumber: text("board_number"), + location: text("location"), + fedFrom: text("fed_from"), + remark: text("remark"), + feedingDistributionBoardId: text("feeding_distribution_board_id").references( + (): AnySQLiteColumn => distributionBoards.id, + { onDelete: "restrict" } + ), + feedingCircuitId: text("feeding_circuit_id").references( + (): AnySQLiteColumn => circuits.id, + { onDelete: "restrict" } + ), + }, + (table) => [ + index("distribution_boards_feeding_board_idx").on( + table.feedingDistributionBoardId + ), + index("distribution_boards_feeding_circuit_idx").on( + table.feedingCircuitId + ), + ] +); diff --git a/src/domain/models/distribution-board-project-command.model.ts b/src/domain/models/distribution-board-project-command.model.ts index 2e917d0..961cf53 100644 --- a/src/domain/models/distribution-board-project-command.model.ts +++ b/src/domain/models/distribution-board-project-command.model.ts @@ -9,16 +9,18 @@ import type { SerializedProjectCommand } from "./project-command.model.js"; export const distributionBoardUpdateCommandType = "distribution-board.update" as const; -// The info-card fields were added additively to schema version 1: every -// previously stored command remains valid, and the inverse is still derived -// from the persisted row for exactly the changed fields. -export const distributionBoardUpdateCommandSchemaVersion = 1 as const; +// Version 2 adds logical feeding references. Version-1 commands remain +// executable and can only change the fields they originally knew. +export const distributionBoardUpdateCommandSchemaVersion = 2 as const; +const legacyDistributionBoardUpdateCommandSchemaVersion = 1 as const; export interface DistributionBoardUpdateValues extends Record { floorId: string | null; supplyType: DistributionBoardSupplyType | null; simultaneityFactor: number; + feedingDistributionBoardId: string | null; + feedingCircuitId: string | null; } export type DistributionBoardUpdateField = @@ -41,16 +43,20 @@ export interface DistributionBoardUpdateCommandPayload { export interface DistributionBoardUpdateProjectCommand extends SerializedProjectCommand { - schemaVersion: typeof distributionBoardUpdateCommandSchemaVersion; + schemaVersion: + | typeof legacyDistributionBoardUpdateCommandSchemaVersion + | typeof distributionBoardUpdateCommandSchemaVersion; type: typeof distributionBoardUpdateCommandType; } export function createDistributionBoardUpdateProjectCommand( distributionBoardId: string, - patch: DistributionBoardUpdatePatch + patch: DistributionBoardUpdatePatch, + schemaVersion: DistributionBoardUpdateProjectCommand["schemaVersion"] = + distributionBoardUpdateCommandSchemaVersion ): DistributionBoardUpdateProjectCommand { const command: DistributionBoardUpdateProjectCommand = { - schemaVersion: distributionBoardUpdateCommandSchemaVersion, + schemaVersion, type: distributionBoardUpdateCommandType, payload: { distributionBoardId, @@ -68,7 +74,8 @@ export function assertDistributionBoardUpdateProjectCommand( command: SerializedProjectCommand ): asserts command is DistributionBoardUpdateProjectCommand { if ( - command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion || + (command.schemaVersion !== legacyDistributionBoardUpdateCommandSchemaVersion && + command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion) || command.type !== distributionBoardUpdateCommandType || !isPlainObject(command.payload) ) { @@ -93,7 +100,12 @@ export function assertDistributionBoardUpdateProjectCommand( (change.field !== "floorId" && change.field !== "supplyType" && change.field !== "simultaneityFactor" && + change.field !== "feedingDistributionBoardId" && + change.field !== "feedingCircuitId" && !isInfoField(change.field)) || + (command.schemaVersion === legacyDistributionBoardUpdateCommandSchemaVersion && + (change.field === "feedingDistributionBoardId" || + change.field === "feedingCircuitId")) || seen.has(change.field) ) { throw new Error( @@ -105,7 +117,9 @@ export function assertDistributionBoardUpdateProjectCommand( } else if (change.field === "floorId") { if ( change.value !== null && - (typeof change.value !== "string" || !change.value.trim()) + (typeof change.value !== "string" || + !change.value.trim() || + change.value !== change.value.trim()) ) { throw new Error("floorId must be a non-empty string or null."); } @@ -118,6 +132,16 @@ export function assertDistributionBoardUpdateProjectCommand( ) { throw new Error("supplyType is invalid."); } + } else if ( + change.field === "feedingDistributionBoardId" || + change.field === "feedingCircuitId" + ) { + if ( + change.value !== null && + (typeof change.value !== "string" || !change.value.trim()) + ) { + throw new Error(`${change.field} must be a non-empty string or null.`); + } } else if ( typeof change.value !== "number" || !Number.isFinite(change.value) || diff --git a/src/domain/models/distribution-board-subtree-snapshot.model.ts b/src/domain/models/distribution-board-subtree-snapshot.model.ts index a0269fc..700a52b 100644 --- a/src/domain/models/distribution-board-subtree-snapshot.model.ts +++ b/src/domain/models/distribution-board-subtree-snapshot.model.ts @@ -74,6 +74,8 @@ export interface DistributionBoardSubtreeSnapshot { floorId: string | null; supplyType: DistributionBoardSupplyType | null; simultaneityFactor: number; + feedingDistributionBoardId: string | null; + feedingCircuitId: string | null; } & Record; circuitList: { id: string; @@ -184,6 +186,9 @@ export function normalizeDistributionBoardSubtreeSnapshot( for (const field of distributionBoardInfoFields) { board[field] = board[field] ?? null; } + board.feedingDistributionBoardId = + board.feedingDistributionBoardId ?? null; + board.feedingCircuitId = board.feedingCircuitId ?? null; return { ...snapshot, distributionBoard: board }; } @@ -227,6 +232,18 @@ export function assertDistributionBoardSubtreeSnapshot( assertNullableString(board[field], `distributionBoard.${field}`); } } + if (board.feedingDistributionBoardId !== undefined) { + assertNullableString( + board.feedingDistributionBoardId, + "distributionBoard.feedingDistributionBoardId" + ); + } + if (board.feedingCircuitId !== undefined) { + assertNullableString( + board.feedingCircuitId, + "distributionBoard.feedingCircuitId" + ); + } for (const field of [ "id", "projectId", diff --git a/src/domain/models/project-state-snapshot.model.ts b/src/domain/models/project-state-snapshot.model.ts index b03bfbd..ea3aba7 100644 --- a/src/domain/models/project-state-snapshot.model.ts +++ b/src/domain/models/project-state-snapshot.model.ts @@ -35,7 +35,8 @@ import { type ExternalModelStateSnapshot, } from "../../external-model/domain/external-model-contracts.js"; -export const projectStateSnapshotSchemaVersion = 8 as const; +export const projectStateSnapshotSchemaVersion = 9 as const; +const roomAreaProjectStateSnapshotSchemaVersion = 8 as const; const buildingProjectStateSnapshotSchemaVersion = 7 as const; const coverProjectStateSnapshotSchemaVersion = 6 as const; const boardInfoProjectStateSnapshotSchemaVersion = 5 as const; @@ -112,7 +113,7 @@ const distributionBoardInfoValueSchema = (field: DistributionBoardInfoField) => .max(distributionBoardInfoFieldMaxLengths[field]) .nullable(); -const distributionBoardSchema = legacyDistributionBoardSchema +const distributionBoardWithoutFeedingSchema = legacyDistributionBoardSchema .extend({ boardNumber: distributionBoardInfoValueSchema("boardNumber"), location: distributionBoardInfoValueSchema("location"), @@ -121,6 +122,13 @@ const distributionBoardSchema = legacyDistributionBoardSchema }) .strict(); +const distributionBoardSchema = distributionBoardWithoutFeedingSchema + .extend({ + feedingDistributionBoardId: idSchema.nullable(), + feedingCircuitId: idSchema.nullable(), + }) + .strict(); + function withEmptyDistributionBoardInfo( boards: Array> ) { @@ -133,6 +141,16 @@ function withEmptyDistributionBoardInfo( })); } +function withEmptyDistributionBoardFeeding( + boards: Array> +) { + return boards.map((board) => ({ + ...board, + feedingDistributionBoardId: null, + feedingCircuitId: null, + })); +} + const circuitListSchema = z .object({ id: idSchema, @@ -553,11 +571,22 @@ export const projectStateSnapshotSchema = z }) .strict(); +const roomAreaProjectStateSnapshotSchema = z + .object({ + schemaVersion: z.literal(roomAreaProjectStateSnapshotSchemaVersion), + project: projectSchema, + distributionBoards: z.array(distributionBoardWithoutFeedingSchema), + externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(), + externalModel: externalModelStateSchema, + ...projectStateSnapshotContents, + }) + .strict(); + const buildingProjectStateSnapshotSchema = z .object({ schemaVersion: z.literal(buildingProjectStateSnapshotSchemaVersion), project: projectSchema, - distributionBoards: z.array(distributionBoardSchema), + distributionBoards: z.array(distributionBoardWithoutFeedingSchema), externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(), externalModel: externalModelStateSchema, ...buildingProjectStateSnapshotContents, @@ -568,7 +597,7 @@ const coverProjectStateSnapshotSchema = z .object({ schemaVersion: z.literal(coverProjectStateSnapshotSchemaVersion), project: projectSchema, - distributionBoards: z.array(distributionBoardSchema), + distributionBoards: z.array(distributionBoardWithoutFeedingSchema), externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(), externalModel: externalModelStateSchema, ...previousProjectStateSnapshotContents, @@ -655,6 +684,27 @@ export function parseProjectStateSnapshot( } function upgradePreviousProjectStateSnapshot(value: unknown): unknown { + const roomArea = upgradeToRoomAreaProjectStateSnapshot(value); + if ( + roomArea !== null && + typeof roomArea === "object" && + !Array.isArray(roomArea) && + (roomArea as { schemaVersion?: unknown }).schemaVersion === + roomAreaProjectStateSnapshotSchemaVersion + ) { + const previous = roomAreaProjectStateSnapshotSchema.parse(roomArea); + return { + ...previous, + schemaVersion: projectStateSnapshotSchemaVersion, + distributionBoards: withEmptyDistributionBoardFeeding( + previous.distributionBoards + ), + }; + } + return roomArea; +} + +function upgradeToRoomAreaProjectStateSnapshot(value: unknown): unknown { const cover = upgradeToCoverProjectStateSnapshot(value); if ( cover !== null && @@ -666,7 +716,7 @@ function upgradePreviousProjectStateSnapshot(value: unknown): unknown { const previous = buildingProjectStateSnapshotSchema.parse(cover); return { ...previous, - schemaVersion: projectStateSnapshotSchemaVersion, + schemaVersion: roomAreaProjectStateSnapshotSchemaVersion, rooms: previous.rooms.map((room) => ({ ...room, areaM2: null })), }; } @@ -683,7 +733,7 @@ function upgradePreviousProjectStateSnapshot(value: unknown): unknown { // building. Build version 7 first so the room-area upgrade above remains a // single exact transition. const previous = coverProjectStateSnapshotSchema.parse(cover); - return upgradePreviousProjectStateSnapshot({ + return upgradeToRoomAreaProjectStateSnapshot({ ...previous, schemaVersion: buildingProjectStateSnapshotSchemaVersion, buildings: [], @@ -873,6 +923,15 @@ function assertProjectStateSnapshotRelations( const sectionById = new Map( snapshot.circuitSections.map((entry) => [entry.id, entry]) ); + const boardById = new Map( + snapshot.distributionBoards.map((entry) => [entry.id, entry]) + ); + const circuitListById = new Map( + snapshot.circuitLists.map((entry) => [entry.id, entry]) + ); + const circuitById = new Map( + snapshot.circuits.map((entry) => [entry.id, entry]) + ); const componentById = new Map( snapshot.distributionBoardComponents.map((entry) => [ entry.id, @@ -901,6 +960,47 @@ function assertProjectStateSnapshotRelations( "Snapshot distribution board uses a disabled supply type." ); } + if (board.feedingDistributionBoardId === board.id) { + throw new Error("Snapshot distribution board cannot feed itself."); + } + if (board.feedingDistributionBoardId !== null) { + assertReference( + boardIds, + board.feedingDistributionBoardId, + "feeding distribution board" + ); + } + if (board.feedingCircuitId !== null) { + if (board.feedingDistributionBoardId === null) { + throw new Error( + "Snapshot feeding circuit requires a feeding distribution board." + ); + } + assertReference(circuitIds, board.feedingCircuitId, "feeding circuit"); + const feedingCircuit = circuitById.get(board.feedingCircuitId); + const feedingList = feedingCircuit + ? circuitListById.get(feedingCircuit.circuitListId) + : undefined; + if ( + !feedingList || + feedingList.distributionBoardId !== board.feedingDistributionBoardId + ) { + throw new Error( + "Snapshot feeding circuit belongs to another distribution board." + ); + } + } + } + for (const board of snapshot.distributionBoards) { + const visited = new Set([board.id]); + let currentId = board.feedingDistributionBoardId; + while (currentId !== null) { + if (visited.has(currentId)) { + throw new Error("Snapshot distribution-board feeding graph contains a cycle."); + } + visited.add(currentId); + currentId = boardById.get(currentId)?.feedingDistributionBoardId ?? null; + } } for (const list of snapshot.circuitLists) { assertProjectOwnership(list.projectId, projectId, "circuit list"); diff --git a/src/domain/models/project-transfer.model.ts b/src/domain/models/project-transfer.model.ts index 80b9d59..d68b29e 100644 --- a/src/domain/models/project-transfer.model.ts +++ b/src/domain/models/project-transfer.model.ts @@ -223,6 +223,14 @@ export function remapProjectState( ...board, id: requiredId(boardIds, board.id), projectId: targetProjectId, + feedingDistributionBoardId: + board.feedingDistributionBoardId === null + ? null + : requiredId(boardIds, board.feedingDistributionBoardId), + feedingCircuitId: + board.feedingCircuitId === null + ? null + : requiredId(circuitIds, board.feedingCircuitId), floorId: board.floorId === null ? null diff --git a/src/frontend/types.ts b/src/frontend/types.ts index e8a2289..a1cc7bc 100644 --- a/src/frontend/types.ts +++ b/src/frontend/types.ts @@ -195,6 +195,8 @@ export interface DistributionBoardDto extends DistributionBoardInfoDto { floorId: string | null; supplyType: DistributionBoardSupplyType | null; simultaneityFactor: number; + feedingDistributionBoardId: string | null; + feedingCircuitId: string | null; } export interface DocumentLogoMetadataDto { diff --git a/src/frontend/utils/api.ts b/src/frontend/utils/api.ts index 501ed01..5e814c9 100644 --- a/src/frontend/utils/api.ts +++ b/src/frontend/utils/api.ts @@ -372,7 +372,13 @@ export function updateDistributionBoard( floorId: string | null; supplyType: NonNullable; simultaneityFactor: number; - } & Partial, + } & Partial & + Partial< + Pick< + DistributionBoardDto, + "feedingDistributionBoardId" | "feedingCircuitId" + > + >, expectedRevision: number ) { return request( diff --git a/src/server/controllers/distribution-board.controller.ts b/src/server/controllers/distribution-board.controller.ts index 9ea713a..d2e9bb9 100644 --- a/src/server/controllers/distribution-board.controller.ts +++ b/src/server/controllers/distribution-board.controller.ts @@ -69,6 +69,8 @@ export async function createDistributionBoard(req: Request, res: Response) { location: null, fedFrom: null, remark: null, + feedingDistributionBoardId: null, + feedingCircuitId: null, }, }); } catch (error) { @@ -98,6 +100,17 @@ export async function updateDistributionBoard( .filter((field) => parsed.data[field] !== undefined) .map((field) => [field, parsed.data[field]]) ); + const feedingPatch = { + ...(parsed.data.feedingDistributionBoardId === undefined + ? {} + : { + feedingDistributionBoardId: + parsed.data.feedingDistributionBoardId, + }), + ...(parsed.data.feedingCircuitId === undefined + ? {} + : { feedingCircuitId: parsed.data.feedingCircuitId }), + }; try { const result = projectCommandService.executeUser({ projectId, @@ -110,6 +123,7 @@ export async function updateDistributionBoard( supplyType: parsed.data.supplyType, simultaneityFactor: parsed.data.simultaneityFactor, ...infoPatch, + ...feedingPatch, } ), }); diff --git a/src/shared/validation/project-structure.schemas.ts b/src/shared/validation/project-structure.schemas.ts index b8655ba..ede5152 100644 --- a/src/shared/validation/project-structure.schemas.ts +++ b/src/shared/validation/project-structure.schemas.ts @@ -86,6 +86,8 @@ export const updateDistributionBoardSchema = z location: distributionBoardInfoValueSchema("location"), fedFrom: distributionBoardInfoValueSchema("fedFrom"), remark: distributionBoardInfoValueSchema("remark"), + feedingDistributionBoardId: z.string().trim().min(1).nullable().optional(), + feedingCircuitId: z.string().trim().min(1).nullable().optional(), }) .strict(); diff --git a/tests/distribution-board-structure-project-command.repository.test.ts b/tests/distribution-board-structure-project-command.repository.test.ts index f829edf..e6aff4b 100644 --- a/tests/distribution-board-structure-project-command.repository.test.ts +++ b/tests/distribution-board-structure-project-command.repository.test.ts @@ -491,6 +491,159 @@ describe("distribution-board structure project command", () => { } }); + it("links a feeding board and circuit while preserving version-1 updates", () => { + const context = createTestDatabase(); + try { + const repository = + new DistributionBoardStructureProjectCommandRepository(context.db); + const upstream = createDistributionBoardStructureSnapshot( + "project-1", + "NSHV", + { supplyType: "AV" } + ); + const downstream = createDistributionBoardStructureSnapshot( + "project-1", + "UV EG", + { supplyType: "AV" } + ); + repository.execute({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + command: createDistributionBoardInsertProjectCommand(upstream), + }); + repository.execute({ + projectId: "project-1", + expectedRevision: 1, + source: "user", + command: createDistributionBoardInsertProjectCommand(downstream), + }); + context.db.insert(circuits).values([ + { + id: "feed-circuit", + circuitListId: upstream.circuitList.id, + sectionId: upstream.sections[2]!.id, + equipmentIdentifier: "-3F1.1", + sortOrder: 10, + }, + { + id: "downstream-circuit", + circuitListId: downstream.circuitList.id, + sectionId: downstream.sections[2]!.id, + equipmentIdentifier: "-3F1.1", + sortOrder: 10, + }, + ]).run(); + + const linked = repository.executeUpdate({ + projectId: "project-1", + expectedRevision: 2, + source: "user", + command: createDistributionBoardUpdateProjectCommand( + downstream.distributionBoard.id, + { + feedingDistributionBoardId: upstream.distributionBoard.id, + feedingCircuitId: "feed-circuit", + } + ), + }); + assert.deepEqual( + context.db + .select({ + feedingDistributionBoardId: + distributionBoards.feedingDistributionBoardId, + feedingCircuitId: distributionBoards.feedingCircuitId, + }) + .from(distributionBoards) + .where(eq(distributionBoards.id, downstream.distributionBoard.id)) + .get(), + { + feedingDistributionBoardId: upstream.distributionBoard.id, + feedingCircuitId: "feed-circuit", + } + ); + + repository.executeUpdate({ + projectId: "project-1", + expectedRevision: 3, + source: "undo", + historyTargetChangeSetId: new ProjectHistoryRepository( + context.db + ).getNextCommand("project-1", "undo")?.changeSetId, + command: linked.inverse, + }); + repository.executeUpdate({ + projectId: "project-1", + expectedRevision: 4, + source: "user", + command: createDistributionBoardUpdateProjectCommand( + downstream.distributionBoard.id, + { + feedingDistributionBoardId: upstream.distributionBoard.id, + feedingCircuitId: null, + } + ), + }); + + assert.throws( + () => repository.executeUpdate({ + projectId: "project-1", + expectedRevision: 5, + source: "user", + command: createDistributionBoardUpdateProjectCommand( + upstream.distributionBoard.id, + { + feedingDistributionBoardId: downstream.distributionBoard.id, + feedingCircuitId: null, + } + ), + }), + /Zyklus/ + ); + assert.throws( + () => repository.executeUpdate({ + projectId: "project-1", + expectedRevision: 5, + source: "user", + command: createDistributionBoardUpdateProjectCommand( + downstream.distributionBoard.id, + { + feedingDistributionBoardId: upstream.distributionBoard.id, + feedingCircuitId: "downstream-circuit", + } + ), + }), + /gehört nicht zur gewählten Verteilung/ + ); + + repository.executeUpdate({ + projectId: "project-1", + expectedRevision: 5, + source: "user", + command: { + schemaVersion: 1, + type: "distribution-board.update", + payload: { + distributionBoardId: downstream.distributionBoard.id, + changes: [{ field: "boardNumber", value: "UV-01" }], + }, + }, + }); + const afterLegacyUpdate = context.db + .select() + .from(distributionBoards) + .where(eq(distributionBoards.id, downstream.distributionBoard.id)) + .get(); + assert.equal(afterLegacyUpdate?.boardNumber, "UV-01"); + assert.equal( + afterLegacyUpdate?.feedingDistributionBoardId, + upstream.distributionBoard.id + ); + } 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 33d5318..6e48294 100644 --- a/tests/distribution-board-subtree.test.ts +++ b/tests/distribution-board-subtree.test.ts @@ -19,6 +19,8 @@ function createSnapshot(): DistributionBoardSubtreeSnapshot { floorId: "floor-1", supplyType: "AV", simultaneityFactor: 0.8, + feedingDistributionBoardId: "board-upstream", + feedingCircuitId: "circuit-upstream", }, circuitList: { id: "list-1", @@ -157,6 +159,11 @@ describe("distribution-board subtree snapshot", () => { assert.equal(clone.distributionBoard.id, "new-1"); assert.equal(clone.circuitList.id, "new-1"); assert.equal(clone.distributionBoard.name, "UV 1 Kopie"); + assert.equal( + clone.distributionBoard.feedingDistributionBoardId, + "board-upstream" + ); + assert.equal(clone.distributionBoard.feedingCircuitId, "circuit-upstream"); assert.equal(clone.circuitList.name, "UV 1 Kopie Stromkreisliste"); assert.equal(clone.sections[0].id, "new-2"); assert.equal(clone.circuits[0].id, "new-3"); diff --git a/tests/print-view-migration.test.ts b/tests/print-view-migration.test.ts index 19e4dcc..343de70 100644 --- a/tests/print-view-migration.test.ts +++ b/tests/print-view-migration.test.ts @@ -50,6 +50,8 @@ it("upgrades a populated pre-print database through the current schema without c for (const [key, value] of Object.entries(oldRoom)) assert.deepEqual(room[key], value); for (const field of ["site_address", "work_stage", "project_lead", "project_editor"]) assert.equal(project[field], null); for (const field of ["board_number", "location", "fed_from", "remark"]) assert.equal(board[field], null); + assert.equal(board.feeding_distribution_board_id, null); + assert.equal(board.feeding_circuit_id, null); assert.equal(floor.building_id, null); assert.equal(room.area_m2, null); assert.equal(context.sqlite.prepare("SELECT count(*) AS count FROM buildings").get()!.count, 0); diff --git a/tests/project-snapshot.repository.test.ts b/tests/project-snapshot.repository.test.ts index 7a02fec..bc4cd12 100644 --- a/tests/project-snapshot.repository.test.ts +++ b/tests/project-snapshot.repository.test.ts @@ -476,7 +476,14 @@ describe("project snapshot repository", () => { delete payload.project[field]; } for (const board of payload.distributionBoards) { - for (const field of ["boardNumber", "location", "fedFrom", "remark"]) { + for (const field of [ + "boardNumber", + "location", + "fedFrom", + "remark", + "feedingDistributionBoardId", + "feedingCircuitId", + ]) { delete board[field]; } } diff --git a/tests/project-state-snapshot.test.ts b/tests/project-state-snapshot.test.ts index c067c19..aa2371b 100644 --- a/tests/project-state-snapshot.test.ts +++ b/tests/project-state-snapshot.test.ts @@ -6,12 +6,25 @@ import { projectStateSnapshotSchemaVersion, serializeProjectStateSnapshot, } from "../src/domain/models/project-state-snapshot.model.js"; +import { remapProjectState } from "../src/domain/models/project-transfer.model.js"; import { createNamedProjectSnapshotSchema } from "../src/shared/validation/project-snapshot.schemas.js"; +/** Removes the fields introduced by snapshot schema version 9. */ +function withoutVersionNineFields< + T extends { distributionBoards: object[] }, +>(snapshot: T): T { + for (const board of snapshot.distributionBoards) { + delete (board as Record).feedingDistributionBoardId; + delete (board as Record).feedingCircuitId; + } + return snapshot; +} + /** Removes the fields introduced by snapshot schema version 7. */ function withoutVersionSevenFields< - T extends { floors: object[] }, + T extends { floors: object[]; distributionBoards: object[] }, >(snapshot: T): T { + withoutVersionNineFields(snapshot); delete (snapshot as Record).buildings; for (const floor of snapshot.floors) { delete (floor as Record).buildingId; @@ -117,6 +130,8 @@ function protectedSnapshot() { location: "Raum 003 Technik" as string | null, fedFrom: "HV 0.1" as string | null, remark: null as string | null, + feedingDistributionBoardId: null as string | null, + feedingCircuitId: null as string | null, }, ], circuitLists: [ @@ -206,6 +221,96 @@ describe("project state snapshot model", () => { ); }); + it("upgrades snapshot version 8 with empty feeding references", () => { + const previous = withoutVersionNineFields(protectedSnapshot()); + previous.schemaVersion = 8 as typeof previous.schemaVersion; + + const upgraded = parseProjectStateSnapshot(previous); + + assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion); + assert.equal( + upgraded.distributionBoards[0]?.feedingDistributionBoardId, + null + ); + assert.equal(upgraded.distributionBoards[0]?.feedingCircuitId, null); + }); + + it("validates logical feeding references and cycles", () => { + const linked = protectedSnapshot(); + linked.distributionBoards.push({ + id: "board-2", + projectId: "project-1", + name: "UV 2", + floorId: null, + supplyType: null, + simultaneityFactor: 1, + boardNumber: "UV 2.1", + location: null, + fedFrom: null, + remark: null, + feedingDistributionBoardId: "board-1", + feedingCircuitId: "circuit-1", + }); + linked.circuitLists.push({ + id: "list-2", + projectId: "project-1", + distributionBoardId: "board-2", + name: "UV 2 Stromkreisliste", + }); + linked.circuitSections.push({ + id: "section-2", + circuitListId: "list-2", + key: "three_phase", + displayName: "Dreiphasig 1", + prefix: "-3F1.", + sortOrder: 10, + category: "three_phase", + groupNumber: 1, + }); + const parsed = parseProjectStateSnapshot(linked); + assert.equal(parsed.distributionBoards[1]?.feedingCircuitId, "circuit-1"); + let nextId = 0; + const remapped = remapProjectState( + parsed, + "project-copy", + () => `copy-${++nextId}` + ); + const remappedUpstream = remapped.distributionBoards.find( + (board) => board.name === "UV 1" + ); + const remappedDownstream = remapped.distributionBoards.find( + (board) => board.name === "UV 2" + ); + assert.ok(remappedUpstream); + assert.ok(remappedDownstream); + assert.equal( + remappedDownstream.feedingDistributionBoardId, + remappedUpstream.id + ); + assert.notEqual(remappedDownstream.feedingCircuitId, "circuit-1"); + assert.ok( + remapped.circuits.some( + (circuit) => circuit.id === remappedDownstream.feedingCircuitId + ) + ); + + const missingBoard = structuredClone(linked); + missingBoard.distributionBoards[1]!.feedingDistributionBoardId = null; + assert.throws( + () => parseProjectStateSnapshot(missingBoard), + /requires a feeding distribution board/ + ); + + const cycle = structuredClone(linked); + cycle.distributionBoards[0]!.feedingDistributionBoardId = "board-2"; + cycle.distributionBoards[0]!.feedingCircuitId = null; + cycle.distributionBoards[1]!.feedingCircuitId = null; + assert.throws( + () => parseProjectStateSnapshot(cycle), + /contains a cycle/ + ); + }); + it("upgrades the supported baseline snapshot with a safe building default", () => { const baseline = withoutVersionSixFields(structuredClone(minimalSnapshot())); baseline.schemaVersion = 1 as typeof baseline.schemaVersion;