diff --git a/AGENTS.md b/AGENTS.md index cf4bd23..5d285fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,6 +58,11 @@ See `docs/current-architecture.md` for the complete module and request flow. - A circuit contains zero, one or multiple device rows. - A single-device circuit may be displayed as one compact row. - A multi-device circuit must be displayed with a circuit summary row and indented device rows. +- A circuit with exactly one device row and no fed distribution board carries + that row's name (`syncSingleDeviceCircuitDisplayNames`). Commands that create + or leave such a circuit, rename its row or unlink its downstream board store + the circuit-name changes as optional `circuitDisplayNames`, so Undo restores + an overwritten free name. Migration `0016` aligned existing data. - Equipment identifiers belong to circuits, not to every device row. - Device rows inside a circuit do not have their own equipment identifiers. - Protection and cable data belong to the circuit, not to individual devices. diff --git a/src/db/migrations/0016_single_device_circuit_names.sql b/src/db/migrations/0016_single_device_circuit_names.sql new file mode 100644 index 0000000..049dfa7 --- /dev/null +++ b/src/db/migrations/0016_single_device_circuit_names.sql @@ -0,0 +1,23 @@ +-- A circuit with exactly one device row and no fed distribution board is +-- shown as that row, so its hidden name follows the device row name. +UPDATE `circuits` +SET `display_name` = ( + SELECT `circuit_device_rows`.`display_name` + FROM `circuit_device_rows` + WHERE `circuit_device_rows`.`circuit_id` = `circuits`.`id` +) +WHERE ( + SELECT count(*) + FROM `circuit_device_rows` + WHERE `circuit_device_rows`.`circuit_id` = `circuits`.`id` +) = 1 +AND NOT EXISTS ( + SELECT 1 + FROM `distribution_boards` + WHERE `distribution_boards`.`feeding_circuit_id` = `circuits`.`id` +) +AND `display_name` IS NOT ( + SELECT `circuit_device_rows`.`display_name` + FROM `circuit_device_rows` + WHERE `circuit_device_rows`.`circuit_id` = `circuits`.`id` +); diff --git a/src/db/migrations/meta/0016_snapshot.json b/src/db/migrations/meta/0016_snapshot.json new file mode 100644 index 0000000..72d24e0 --- /dev/null +++ b/src/db/migrations/meta/0016_snapshot.json @@ -0,0 +1,3210 @@ +{ + "id": "38c81122-b9a7-4680-8d99-38a59b90698f", + "prevId": "eab4abfa-dc46-4330-9ddd-b4676343d610", + "version": "6", + "dialect": "sqlite", + "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", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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", + "columnsFrom": [ + "circuit_id" + ], + "tableTo": "circuits", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { + "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", + "tableFrom": "circuit_device_rows", + "columnsFrom": [ + "linked_project_device_id" + ], + "tableTo": "project_devices", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "circuit_device_rows_room_id_rooms_id_fk": { + "name": "circuit_device_rows_room_id_rooms_id_fk", + "tableFrom": "circuit_device_rows", + "columnsFrom": [ + "room_id" + ], + "tableTo": "rooms", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_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", + "columnsFrom": [ + "circuit_list_id" + ], + "tableTo": "circuit_lists", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_lists": { + "name": "circuit_lists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "distribution_board_id": { + "name": "distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "circuit_lists_distribution_board_id_unique": { + "name": "circuit_lists_distribution_board_id_unique", + "columns": [ + "distribution_board_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_lists_project_id_projects_id_fk": { + "name": "circuit_lists_project_id_projects_id_fk", + "tableFrom": "circuit_lists", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "circuit_lists_distribution_board_id_distribution_boards_id_fk": { + "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "circuit_lists", + "columnsFrom": [ + "distribution_board_id" + ], + "tableTo": "distribution_boards", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_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", + "columnsFrom": [ + "circuit_id" + ], + "tableTo": "circuits", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_sections": { + "name": "circuit_sections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "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", + "columnsFrom": [ + "circuit_list_id" + ], + "tableTo": "circuit_lists", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuits": { + "name": "circuits", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "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", + "columnsFrom": [ + "circuit_list_id" + ], + "tableTo": "circuit_lists", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "circuits_section_id_circuit_sections_id_fk": { + "name": "circuits_section_id_circuit_sections_id_fk", + "tableFrom": "circuits", + "columnsFrom": [ + "section_id" + ], + "tableTo": "circuit_sections", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "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", + "columnsFrom": [ + "component_id" + ], + "tableTo": "distribution_board_components", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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", + "columnsFrom": [ + "circuit_list_id" + ], + "tableTo": "circuit_lists", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "distribution_board_components_section_id_circuit_sections_id_fk": { + "name": "distribution_board_components_section_id_circuit_sections_id_fk", + "tableFrom": "distribution_board_components", + "columnsFrom": [ + "section_id" + ], + "tableTo": "circuit_sections", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "distribution_boards_floor_id_floors_id_fk": { + "name": "distribution_boards_floor_id_floors_id_fk", + "tableFrom": "distribution_boards", + "columnsFrom": [ + "floor_id" + ], + "tableTo": "floors", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "distribution_boards_feeding_distribution_board_id_distribution_boards_id_fk": { + "name": "distribution_boards_feeding_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "distribution_boards", + "columnsFrom": [ + "feeding_distribution_board_id" + ], + "tableTo": "distribution_boards", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "distribution_boards_feeding_circuit_id_circuits_id_fk": { + "name": "distribution_boards_feeding_circuit_id_circuits_id_fk", + "tableFrom": "distribution_boards", + "columnsFrom": [ + "feeding_circuit_id" + ], + "tableTo": "circuits", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "restrict" + } + }, + "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", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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", + "columnsFrom": [ + "balance_id" + ], + "tableTo": "estimate_balances", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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", + "columnsFrom": [ + "group_id" + ], + "tableTo": "estimate_groups", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_export_batches": { + "name": "external_export_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 + }, + "project_revision": { + "name": "project_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "import_batch_id": { + "name": "import_batch_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exported_at_iso": { + "name": "exported_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 + }, + "configuration_snapshot": { + "name": "configuration_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration_sha256": { + "name": "configuration_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_count": { + "name": "object_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_export_batches_project_exported_idx": { + "name": "external_export_batches_project_exported_idx", + "columns": [ + "project_id", + "exported_at_iso" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_export_batches_project_id_projects_id_fk": { + "name": "external_export_batches_project_id_projects_id_fk", + "tableFrom": "external_export_batches", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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 + }, + "object_count": { + "name": "object_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_project_revision": { + "name": "applied_project_revision", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "configuration_snapshot": { + "name": "configuration_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "original_bytes": { + "name": "original_bytes", + "type": "blob", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "document": { + "name": "document", + "type": "text", + "primaryKey": false, + "notNull": false, + "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", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "source_id" + ], + "tableTo": "external_model_sources", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_model_configurations": { + "name": "external_model_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": { + "name": "configuration", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_model_configurations_project_id_unique": { + "name": "external_model_configurations_project_id_unique", + "columns": [ + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "external_model_configurations_project_id_projects_id_fk": { + "name": "external_model_configurations_project_id_projects_id_fk", + "tableFrom": "external_model_configurations", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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 + }, + "worksharing_id": { + "name": "worksharing_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ifc_guid": { + "name": "ifc_guid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "device_type_id": { + "name": "device_type_id", + "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_worksharing_id_unique": { + "name": "external_model_objects_source_worksharing_id_unique", + "columns": [ + "source_id", + "worksharing_id" + ], + "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_project_device_type_idx": { + "name": "external_model_objects_project_device_type_idx", + "columns": [ + "project_id", + "device_type_id" + ], + "isUnique": false + }, + "external_model_objects_external_room_mapping_idx": { + "name": "external_model_objects_external_room_mapping_idx", + "columns": [ + "external_room_mapping_id" + ], + "isUnique": false + }, + "external_model_objects_distribution_board_idx": { + "name": "external_model_objects_distribution_board_idx", + "columns": [ + "distribution_board_id" + ], + "isUnique": false + }, + "external_model_objects_linked_project_device_idx": { + "name": "external_model_objects_linked_project_device_idx", + "columns": [ + "linked_project_device_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", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "source_id" + ], + "tableTo": "external_model_sources", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "last_seen_import_batch_id" + ], + "tableTo": "external_import_batches", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "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", + "columnsFrom": [ + "last_accepted_import_batch_id" + ], + "tableTo": "external_import_batches", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "restrict" + }, + "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", + "columnsFrom": [ + "external_room_mapping_id" + ], + "tableTo": "external_room_mappings", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "distribution_board_id" + ], + "tableTo": "distribution_boards", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "linked_project_device_id" + ], + "tableTo": "project_devices", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "circuit_device_row_id" + ], + "tableTo": "circuit_device_rows", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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_xlsx'" + } + }, + "indexes": { + "external_model_sources_project_unique": { + "name": "external_model_sources_project_unique", + "columns": [ + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "external_model_sources_project_id_projects_id_fk": { + "name": "external_model_sources_project_id_projects_id_fk", + "tableFrom": "external_model_sources", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": [ + "source_id" + ], + "tableTo": "external_model_sources", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "external_room_mappings_room_id_rooms_id_fk": { + "name": "external_room_mappings_room_id_rooms_id_fk", + "tableFrom": "external_room_mappings", + "columnsFrom": [ + "room_id" + ], + "tableTo": "rooms", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "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", + "columnsFrom": [ + "default_distribution_board_id" + ], + "tableTo": "distribution_boards", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "floors": { + "name": "floors", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "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", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "floors_building_id_buildings_id_fk": { + "name": "floors_building_id_buildings_id_fk", + "tableFrom": "floors", + "columnsFrom": [ + "building_id" + ], + "tableTo": "buildings", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "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", + "columnsFrom": [ + "project_revision_id" + ], + "tableTo": "project_revisions", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_devices": { + "name": "project_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase_type": { + "name": "phase_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'single_phase'" + }, + "connection_kind": { + "name": "connection_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_group": { + "name": "cost_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "power_per_unit": { + "name": "power_per_unit", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "cos_phi": { + "name": "cos_phi", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voltage_v": { + "name": "voltage_v", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "project_devices_project_id_projects_id_fk": { + "name": "project_devices_project_id_projects_id_fk", + "tableFrom": "project_devices", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_history_stack_entries": { + "name": "project_history_stack_entries", + "columns": { + "change_set_id": { + "name": "change_set_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stack": { + "name": "stack", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_history_stack_entries_position_unique": { + "name": "project_history_stack_entries_position_unique", + "columns": [ + "project_id", + "stack", + "position" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { + "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", + "tableFrom": "project_history_stack_entries", + "columnsFrom": [ + "change_set_id" + ], + "tableTo": "project_change_sets", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "project_history_stack_entries_project_id_projects_id_fk": { + "name": "project_history_stack_entries_project_id_projects_id_fk", + "tableFrom": "project_history_stack_entries", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "project_history_stack_entries_stack_check": { + "name": "project_history_stack_entries_stack_check", + "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" + } + } + }, + "project_revisions": { + "name": "project_revisions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "project_revisions_project_number_unique": { + "name": "project_revisions_project_number_unique", + "columns": [ + "project_id", + "revision_number" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_revisions_project_id_projects_id_fk": { + "name": "project_revisions_project_id_projects_id_fk", + "tableFrom": "project_revisions", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_snapshots": { + "name": "project_snapshots", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_revision": { + "name": "source_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'named'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_sha256": { + "name": "payload_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "project_snapshots_project_created_idx": { + "name": "project_snapshots_project_created_idx", + "columns": [ + "project_id", + "created_at_iso" + ], + "isUnique": false + }, + "project_snapshots_project_kind_revision_idx": { + "name": "project_snapshots_project_kind_revision_idx", + "columns": [ + "project_id", + "kind", + "source_revision" + ], + "isUnique": false + }, + "project_snapshots_project_name_unique": { + "name": "project_snapshots_project_name_unique", + "columns": [ + "project_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_snapshots_project_id_projects_id_fk": { + "name": "project_snapshots_project_id_projects_id_fk", + "tableFrom": "project_snapshots", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_project_number": { + "name": "internal_project_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_project_number": { + "name": "external_project_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "building_owner": { + "name": "building_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "rooms_floor_id_floors_id_fk": { + "name": "rooms_floor_id_floors_id_fk", + "tableFrom": "rooms", + "columnsFrom": [ + "floor_id" + ], + "tableTo": "floors", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "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, + "default": "'user'" + }, + "active": { + "name": "active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "must_change_password": { + "name": "must_change_password", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "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 0ab138e..11b0282 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -113,6 +113,13 @@ "when": 1790773066696, "tag": "0015_wide_night_thrasher", "breakpoints": true + }, + { + "idx": 16, + "version": "6", + "when": 1793277078601, + "tag": "0016_single_device_circuit_names", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/repositories/circuit-device-row-move-project-command.repository.ts b/src/db/repositories/circuit-device-row-move-project-command.repository.ts index e1e3f5a..c642c23 100644 --- a/src/db/repositories/circuit-device-row-move-project-command.repository.ts +++ b/src/db/repositories/circuit-device-row-move-project-command.repository.ts @@ -10,6 +10,7 @@ import { type CircuitDeviceRowMoveHistoryProjectCommand, } from "../../domain/models/circuit-device-row-move-project-command.model.js"; import type { CircuitSnapshot } from "../../domain/models/circuit-structure-project-command.model.js"; +import { invertCircuitDisplayNameTransitions } from "../../domain/models/circuit-display-name-sync.model.js"; import type { CircuitDeviceRowMoveProjectCommandStore, ExecuteCircuitDeviceRowMoveCommandInput, @@ -20,7 +21,11 @@ import { circuitLists } from "../schema/circuit-lists.js"; import { circuitProtectionDevices } from "../schema/circuit-protection-devices.js"; import { circuitSections } from "../schema/circuit-sections.js"; import { circuits } from "../schema/circuits.js"; -import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { syncSingleDeviceCircuitDisplayNames } from "./circuit-display-name-sync.persistence.js"; +import { + executeProjectCommandTransactionWithAppliedForward, + type AppliedProjectCommand, +} from "./project-command-transaction.persistence.js"; import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js"; import { resolveCircuitVoltage } from "./project-voltage.persistence.js"; import { assertCircuitsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js"; @@ -30,6 +35,7 @@ interface PersistedMoveRow { circuitId: string; sortOrder: number; phaseType: string | null; + displayName: string; } export class CircuitDeviceRowMoveProjectCommandRepository @@ -40,11 +46,16 @@ export class CircuitDeviceRowMoveProjectCommandRepository execute(input: ExecuteCircuitDeviceRowMoveCommandInput) { this.assertSupportedCommand(input.command); - return executeProjectCommandTransaction( + return executeProjectCommandTransactionWithAppliedForward( this.database, input, (tx) => - this.applyCommand(tx, input.projectId, input.command) + this.applyCommand( + tx, + input.projectId, + input.source, + input.command + ) ); } @@ -70,8 +81,12 @@ export class CircuitDeviceRowMoveProjectCommandRepository private applyCommand( database: AppDatabase, projectId: string, + source: ExecuteCircuitDeviceRowMoveCommandInput["source"], command: CircuitDeviceRowMoveHistoryProjectCommand - ): CircuitDeviceRowMoveHistoryProjectCommand { + ): AppliedProjectCommand< + CircuitDeviceRowMoveHistoryProjectCommand, + CircuitDeviceRowMoveHistoryProjectCommand + > { if (command.type === circuitDeviceRowMoveCommandType) { const rowsById = this.loadExpectedRows( database, @@ -85,21 +100,43 @@ export class CircuitDeviceRowMoveProjectCommandRepository projectId, circuitIds ); - const inverse = createCircuitDeviceRowMoveProjectCommand( - this.reverseMoves(command.payload.moves, rowsById) - ); this.applyMoves(database, command.payload.moves); this.updateReserveStates(database, circuitIds); this.updateVoltages(database, projectId, circuitIds); - return inverse; + const circuitDisplayNames = syncSingleDeviceCircuitDisplayNames( + database, + source, + circuitIds, + command.payload.circuitDisplayNames + ); + return { + forward: createCircuitDeviceRowMoveProjectCommand( + command.payload.moves, + circuitDisplayNames + ), + inverse: createCircuitDeviceRowMoveProjectCommand( + this.reverseMoves(command.payload.moves, rowsById), + invertCircuitDisplayNameTransitions(circuitDisplayNames) + ), + }; } assertCircuitDeviceRowMoveWithNewCircuitProjectCommand( command ); - const { targetCircuit, targetCircuitAction, moves } = - command.payload; + const { targetCircuitAction, moves } = command.payload; const rowsById = this.loadExpectedRows(database, moves); + // A new circuit receiving exactly one row is shown as that row, so it + // takes the row's name instead of the client's placeholder name. + const targetCircuit = + source === "user" && + targetCircuitAction === "create" && + moves.length === 1 + ? { + ...command.payload.targetCircuit, + displayName: rowsById.get(moves[0]!.rowId)!.displayName, + } + : command.payload.targetCircuit; const appliedTargetCircuit = { ...targetCircuit, voltage: resolveCircuitVoltage( @@ -124,12 +161,6 @@ export class CircuitDeviceRowMoveProjectCommandRepository this.assertTargetCircuitAvailable(database, appliedTargetCircuit); this.insertTargetCircuit(database, appliedTargetCircuit); - const inverse = - createCircuitDeviceRowMoveWithNewCircuitProjectCommand( - "delete", - appliedTargetCircuit, - this.reverseMoves(moves, rowsById) - ); this.applyMoves(database, moves); this.updateReserveStates(database, [ ...sourceCircuitIds, @@ -139,7 +170,26 @@ export class CircuitDeviceRowMoveProjectCommandRepository ...sourceCircuitIds, targetCircuit.id, ]); - return inverse; + const circuitDisplayNames = syncSingleDeviceCircuitDisplayNames( + database, + source, + [...sourceCircuitIds, targetCircuit.id], + command.payload.circuitDisplayNames + ); + return { + forward: createCircuitDeviceRowMoveWithNewCircuitProjectCommand( + "create", + targetCircuit, + moves, + circuitDisplayNames + ), + inverse: createCircuitDeviceRowMoveWithNewCircuitProjectCommand( + "delete", + appliedTargetCircuit, + this.reverseMoves(moves, rowsById), + invertCircuitDisplayNameTransitions(circuitDisplayNames) + ), + }; } this.assertTargetCircuitUnchanged( @@ -156,12 +206,6 @@ export class CircuitDeviceRowMoveProjectCommandRepository destinationCircuitIds, targetCircuit.circuitListId ); - const inverse = - createCircuitDeviceRowMoveWithNewCircuitProjectCommand( - "create", - appliedTargetCircuit, - this.reverseMoves(moves, rowsById) - ); this.applyMoves(database, moves); this.updateReserveStates(database, [ targetCircuit.id, @@ -172,6 +216,12 @@ export class CircuitDeviceRowMoveProjectCommandRepository projectId, destinationCircuitIds ); + const circuitDisplayNames = syncSingleDeviceCircuitDisplayNames( + database, + source, + destinationCircuitIds, + command.payload.circuitDisplayNames + ); assertCircuitsAreNotReferencedAsFeeders(database, projectId, [ targetCircuit.id, ]); @@ -193,7 +243,20 @@ export class CircuitDeviceRowMoveProjectCommandRepository "Created target circuit changed before deletion." ); } - return inverse; + return { + forward: createCircuitDeviceRowMoveWithNewCircuitProjectCommand( + "delete", + targetCircuit, + moves, + circuitDisplayNames + ), + inverse: createCircuitDeviceRowMoveWithNewCircuitProjectCommand( + "create", + appliedTargetCircuit, + this.reverseMoves(moves, rowsById), + invertCircuitDisplayNameTransitions(circuitDisplayNames) + ), + }; } private updateVoltages( @@ -217,6 +280,7 @@ export class CircuitDeviceRowMoveProjectCommandRepository circuitId: circuitDeviceRows.circuitId, sortOrder: circuitDeviceRows.sortOrder, phaseType: circuitDeviceRows.phaseType, + displayName: circuitDeviceRows.displayName, }) .from(circuitDeviceRows) .where(inArray(circuitDeviceRows.id, rowIds)) diff --git a/src/db/repositories/circuit-device-row-project-command.repository.ts b/src/db/repositories/circuit-device-row-project-command.repository.ts index ecfa646..cb34ada 100644 --- a/src/db/repositories/circuit-device-row-project-command.repository.ts +++ b/src/db/repositories/circuit-device-row-project-command.repository.ts @@ -6,6 +6,10 @@ import { type CircuitDeviceRowUpdatePatch, type CircuitDeviceRowUpdateValues, } from "../../domain/models/circuit-device-row-project-command.model.js"; +import { + assertCircuitDisplayNameTransitions, + invertCircuitDisplayNameTransitions, +} from "../../domain/models/circuit-display-name-sync.model.js"; import type { CircuitDeviceRowProjectCommandStore, ExecuteCircuitDeviceRowUpdateCommandInput, @@ -22,6 +26,7 @@ import { toCircuitDeviceRowPatchValues, type CircuitDeviceRowPatchInput, } from "./circuit-device-row.persistence.js"; +import { syncSingleDeviceCircuitDisplayNames } from "./circuit-display-name-sync.persistence.js"; import { executeProjectCommandTransactionWithAppliedForward } from "./project-command-transaction.persistence.js"; import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js"; @@ -34,6 +39,9 @@ export class CircuitDeviceRowProjectCommandRepository executeUpdate(input: ExecuteCircuitDeviceRowUpdateCommandInput) { assertCircuitDeviceRowUpdateProjectCommand(input.command); + assertCircuitDisplayNameTransitions( + input.command.payload.circuitDisplayNames + ); return executeProjectCommandTransactionWithAppliedForward( this.database, @@ -111,20 +119,16 @@ export class CircuitDeviceRowProjectCommandRepository patch.overriddenFields = overriddenFields; } - const appliedForward = createCircuitDeviceRowUpdateProjectCommand( + const appliedPatch = createCircuitDeviceRowUpdateProjectCommand( current.id, patch as CircuitDeviceRowUpdatePatch ); const inversePatch = Object.fromEntries( - appliedForward.payload.changes.map((change) => [ + appliedPatch.payload.changes.map((change) => [ change.field, getCircuitDeviceRowFieldValue(current, change.field), ]) ) as CircuitDeviceRowUpdatePatch; - const inverse = createCircuitDeviceRowUpdateProjectCommand( - current.id, - inversePatch - ); const update = tx .update(circuitDeviceRows) @@ -141,10 +145,24 @@ export class CircuitDeviceRowProjectCommandRepository current.circuitId ); } + const circuitDisplayNames = syncSingleDeviceCircuitDisplayNames( + tx, + input.source, + patch.displayName === undefined ? [] : [current.circuitId], + input.command.payload.circuitDisplayNames + ); return { - forward: appliedForward, - inverse, + forward: createCircuitDeviceRowUpdateProjectCommand( + current.id, + patch as CircuitDeviceRowUpdatePatch, + circuitDisplayNames + ), + inverse: createCircuitDeviceRowUpdateProjectCommand( + current.id, + inversePatch, + invertCircuitDisplayNameTransitions(circuitDisplayNames) + ), }; } diff --git a/src/db/repositories/circuit-device-row-structure-project-command.repository.ts b/src/db/repositories/circuit-device-row-structure-project-command.repository.ts index f69dc78..91e476b 100644 --- a/src/db/repositories/circuit-device-row-structure-project-command.repository.ts +++ b/src/db/repositories/circuit-device-row-structure-project-command.repository.ts @@ -9,6 +9,10 @@ import { type CircuitDeviceRowSnapshot, type CircuitDeviceRowStructureProjectCommand, } from "../../domain/models/circuit-device-row-structure-project-command.model.js"; +import { + invertCircuitDisplayNameTransitions, + type CircuitDisplayNameTransition, +} from "../../domain/models/circuit-display-name-sync.model.js"; import type { CircuitDeviceRowStructureProjectCommandStore, ExecuteCircuitDeviceRowStructureCommandInput, @@ -23,6 +27,7 @@ import { toCircuitDeviceRowInsertValues, toCircuitDeviceRowSnapshot, } from "./circuit-device-row-structure.persistence.js"; +import { syncSingleDeviceCircuitDisplayNames } from "./circuit-display-name-sync.persistence.js"; import { executeProjectCommandTransactionWithAppliedForward, type AppliedProjectCommand } from "./project-command-transaction.persistence.js"; import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js"; import { @@ -69,7 +74,8 @@ export class CircuitDeviceRowStructureProjectCommandRepository projectId, source, command.payload.row, - command.payload.externalObjects ?? [] + command.payload.externalObjects ?? [], + command.payload.circuitDisplayNames ); } if (command.type === circuitDeviceRowDeleteCommandType) { @@ -77,9 +83,11 @@ export class CircuitDeviceRowStructureProjectCommandRepository return this.delete( database, projectId, + source, command.payload.rowId, command.payload.expectedCircuitId, - command.payload.externalObjects + command.payload.externalObjects, + command.payload.circuitDisplayNames ); } throw new Error("Unsupported circuit device-row structure command."); @@ -90,7 +98,8 @@ export class CircuitDeviceRowStructureProjectCommandRepository projectId: string, source: ExecuteCircuitDeviceRowStructureCommandInput["source"], row: CircuitDeviceRowSnapshot, - externalObjects: ExternalObjectLinkTransition[] + externalObjects: ExternalObjectLinkTransition[], + storedCircuitDisplayNames: CircuitDisplayNameTransition[] | undefined ) { if (source === "user" && !isElectricalPhaseType(row.phaseType)) { throw new Error("Device-row phase type is invalid."); @@ -125,19 +134,32 @@ export class CircuitDeviceRowStructureProjectCommandRepository loadExpectedExternalObjectTransitions(database, projectId, externalObjects, "Revit-Objekt wurde vor dem Wiederherstellen geändert."); applyExternalObjectLinks(database, externalObjects, "Revit-Objekt wurde beim Wiederherstellen geändert."); } + const circuitDisplayNames = syncSingleDeviceCircuitDisplayNames( + database, + source, + [row.circuitId], + storedCircuitDisplayNames + ); return { - forward: createCircuitDeviceRowInsertProjectCommand(row, externalObjects), - inverse: createCircuitDeviceRowDeleteProjectCommand(row.id, row.circuitId, invertExternalObjectLinkTransitions(externalObjects)), + forward: createCircuitDeviceRowInsertProjectCommand(row, externalObjects, circuitDisplayNames), + inverse: createCircuitDeviceRowDeleteProjectCommand( + row.id, + row.circuitId, + invertExternalObjectLinkTransitions(externalObjects), + invertCircuitDisplayNameTransitions(circuitDisplayNames) + ), }; } private delete( database: AppDatabase, projectId: string, + source: ExecuteCircuitDeviceRowStructureCommandInput["source"], rowId: string, expectedCircuitId: string, - suppliedExternalObjects?: ExternalObjectLinkTransition[] + suppliedExternalObjects: ExternalObjectLinkTransition[] | undefined, + storedCircuitDisplayNames: CircuitDisplayNameTransition[] | undefined ) { const row = database .select() @@ -187,10 +209,20 @@ export class CircuitDeviceRowStructureProjectCommandRepository projectId, expectedCircuitId ); + const circuitDisplayNames = syncSingleDeviceCircuitDisplayNames( + database, + source, + [expectedCircuitId], + storedCircuitDisplayNames + ); return { - forward: createCircuitDeviceRowDeleteProjectCommand(rowId, expectedCircuitId, externalObjects), - inverse: createCircuitDeviceRowInsertProjectCommand(toCircuitDeviceRowSnapshot(row), invertExternalObjectLinkTransitions(externalObjects)), + forward: createCircuitDeviceRowDeleteProjectCommand(rowId, expectedCircuitId, externalObjects, circuitDisplayNames), + inverse: createCircuitDeviceRowInsertProjectCommand( + toCircuitDeviceRowSnapshot(row), + invertExternalObjectLinkTransitions(externalObjects), + invertCircuitDisplayNameTransitions(circuitDisplayNames) + ), }; } diff --git a/src/db/repositories/circuit-display-name-sync.persistence.ts b/src/db/repositories/circuit-display-name-sync.persistence.ts new file mode 100644 index 0000000..a28de36 --- /dev/null +++ b/src/db/repositories/circuit-display-name-sync.persistence.ts @@ -0,0 +1,89 @@ +import { and, eq, isNull } from "drizzle-orm"; +import { + resolveSingleDeviceCircuitDisplayName, + type CircuitDisplayNameTransition, +} from "../../domain/models/circuit-display-name-sync.model.js"; +import type { ProjectRevisionSource } from "../../domain/ports/project-revision.store.js"; +import type { AppDatabase } from "../database-context.js"; +import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; +import { circuits } from "../schema/circuits.js"; +import { distributionBoards } from "../schema/distribution-boards.js"; + +/** + * Keeps the name of single-device circuits equal to their device row name. + * A user command derives the changes for the affected circuits after its own + * write; history replays apply exactly the stored changes. Commands stored + * before this rule carry no changes and keep their original behavior. + */ +export function syncSingleDeviceCircuitDisplayNames( + database: AppDatabase, + source: ProjectRevisionSource, + circuitIds: Iterable, + storedTransitions: readonly CircuitDisplayNameTransition[] | undefined +): CircuitDisplayNameTransition[] { + if (source !== "user") { + const transitions = [...(storedTransitions ?? [])]; + for (const transition of transitions) { + applyCircuitDisplayNameTransition(database, transition); + } + return transitions; + } + + const transitions: CircuitDisplayNameTransition[] = []; + for (const circuitId of new Set(circuitIds)) { + const circuit = database + .select({ displayName: circuits.displayName }) + .from(circuits) + .where(eq(circuits.id, circuitId)) + .get(); + if (!circuit) continue; + const deviceRowDisplayNames = database + .select({ displayName: circuitDeviceRows.displayName }) + .from(circuitDeviceRows) + .where(eq(circuitDeviceRows.circuitId, circuitId)) + .limit(2) + .all() + .map((row) => row.displayName); + const fedBoard = database + .select({ id: distributionBoards.id }) + .from(distributionBoards) + .where(eq(distributionBoards.feedingCircuitId, circuitId)) + .limit(1) + .get(); + const target = resolveSingleDeviceCircuitDisplayName({ + displayName: circuit.displayName, + deviceRowDisplayNames, + feedsDistributionBoard: fedBoard !== undefined, + }); + if (target === null) continue; + const transition = { + circuitId, + expected: circuit.displayName, + target, + }; + applyCircuitDisplayNameTransition(database, transition); + transitions.push(transition); + } + return transitions; +} + +function applyCircuitDisplayNameTransition( + database: AppDatabase, + transition: CircuitDisplayNameTransition +) { + const updated = database + .update(circuits) + .set({ displayName: transition.target }) + .where( + and( + eq(circuits.id, transition.circuitId), + transition.expected === null + ? isNull(circuits.displayName) + : eq(circuits.displayName, transition.expected) + ) + ) + .run(); + if (updated.changes !== 1) { + throw new Error("Circuit name changed before command execution."); + } +} diff --git a/src/db/repositories/circuit-structure-project-command.repository.ts b/src/db/repositories/circuit-structure-project-command.repository.ts index f4fc420..8446af6 100644 --- a/src/db/repositories/circuit-structure-project-command.repository.ts +++ b/src/db/repositories/circuit-structure-project-command.repository.ts @@ -106,9 +106,17 @@ export class CircuitStructureProjectCommandRepository database: AppDatabase, projectId: string, source: ExecuteCircuitStructureCommandInput["source"], - snapshot: CircuitSnapshot, + requestedSnapshot: CircuitSnapshot, externalObjects: ExternalObjectLinkTransition[] ) { + // A single-device circuit is shown as its device row and carries its name. + const snapshot = + source === "user" && requestedSnapshot.deviceRows.length === 1 + ? { + ...requestedSnapshot, + displayName: requestedSnapshot.deviceRows[0]!.displayName, + } + : requestedSnapshot; this.assertCircuitLocation(database, projectId, snapshot); if (source === "user") { const derivedVoltage = resolveCircuitVoltage( 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 9854092..4a2de7e 100644 --- a/src/db/repositories/distribution-board-structure-project-command.repository.ts +++ b/src/db/repositories/distribution-board-structure-project-command.repository.ts @@ -30,6 +30,7 @@ import { createCircuitDeleteProjectCommand, createCircuitInsertProjectCommand, } from "../../domain/models/circuit-structure-project-command.model.js"; +import { invertCircuitDisplayNameTransitions } from "../../domain/models/circuit-display-name-sync.model.js"; import type { DistributionBoardStructureProjectCommandStore, ExecuteDistributionBoardStructureCommandInput, @@ -42,7 +43,12 @@ import { distributionBoardComponents } from "../schema/distribution-board-compon import { distributionBoards } from "../schema/distribution-boards.js"; import { floors } from "../schema/floors.js"; import { projects } from "../schema/projects.js"; -import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { syncSingleDeviceCircuitDisplayNames } from "./circuit-display-name-sync.persistence.js"; +import { + executeProjectCommandTransaction, + executeProjectCommandTransactionWithAppliedForward, + type AppliedProjectCommand, +} from "./project-command-transaction.persistence.js"; import { assertBoardsAreNotReferencedAsFeeders } from "./distribution-board-feeding-deletion-guard.persistence.js"; import { assertDistributionBoardHasNoExternalObjects } from "./external-object-deletion-guard.persistence.js"; import { CircuitStructureProjectCommandRepository } from "./circuit-structure-project-command.repository.js"; @@ -70,7 +76,7 @@ export class DistributionBoardStructureProjectCommandRepository } ) { assertDistributionBoardUpdateProjectCommand(input.command); - return executeProjectCommandTransaction( + return executeProjectCommandTransactionWithAppliedForward( this.database, input, (tx) => this.update(tx, input) @@ -94,7 +100,7 @@ export class DistributionBoardStructureProjectCommandRepository "Generated feeder circuits may only be removed through project history." ); } - return executeProjectCommandTransaction( + return executeProjectCommandTransactionWithAppliedForward( this.database, input, (tx) => this.applyFeederCircuitCommand(tx, input) @@ -384,11 +390,6 @@ export class DistributionBoardStructureProjectCommandRepository getDistributionBoardFieldValue(current, change.field), ]) ) as DistributionBoardUpdatePatch; - const inverse = createDistributionBoardUpdateProjectCommand( - current.id, - inversePatch, - input.command.schemaVersion - ); const updated = database .update(distributionBoards) .set(patch) @@ -404,7 +405,26 @@ export class DistributionBoardStructureProjectCommandRepository "Distribution board changed before command execution." ); } - return inverse; + const circuitDisplayNames = syncSingleDeviceCircuitDisplayNames( + database, + input.source, + current.feedingCircuitId === null ? [] : [current.feedingCircuitId], + input.command.payload.circuitDisplayNames + ); + return { + forward: createDistributionBoardUpdateProjectCommand( + current.id, + patch, + input.command.schemaVersion, + circuitDisplayNames + ), + inverse: createDistributionBoardUpdateProjectCommand( + current.id, + inversePatch, + input.command.schemaVersion, + invertCircuitDisplayNameTransitions(circuitDisplayNames) + ), + }; } private applyFeederCircuitCommand( @@ -415,7 +435,10 @@ export class DistributionBoardStructureProjectCommandRepository > & { command: DistributionBoardFeederCircuitProjectCommand; } - ): DistributionBoardFeederCircuitProjectCommand { + ): AppliedProjectCommand< + DistributionBoardFeederCircuitProjectCommand, + DistributionBoardFeederCircuitProjectCommand + > { const circuitRepository = new CircuitStructureProjectCommandRepository(database); const { circuit, boardUpdate } = input.command.payload; @@ -431,17 +454,23 @@ export class DistributionBoardStructureProjectCommandRepository if (circuitInverse.type !== circuitDeleteCommandType) { throw new Error("Created feeder circuit returned an invalid inverse."); } - const boardInverse = this.update(database, { + const board = this.update(database, { ...input, command: boardUpdate, }); - return createDistributionBoardRemoveFeederCircuitProjectCommand( - circuit, - boardInverse - ); + return { + forward: createDistributionBoardCreateFeederCircuitProjectCommand( + circuit, + board.forward + ), + inverse: createDistributionBoardRemoveFeederCircuitProjectCommand( + circuit, + board.inverse + ), + }; } - const boardInverse = this.update(database, { + const board = this.update(database, { ...input, command: boardUpdate, }); @@ -459,10 +488,16 @@ export class DistributionBoardStructureProjectCommandRepository "Generated feeder circuit changed before history removal." ); } - return createDistributionBoardCreateFeederCircuitProjectCommand( - circuit, - boardInverse - ); + return { + forward: createDistributionBoardRemoveFeederCircuitProjectCommand( + circuit, + board.forward + ), + inverse: createDistributionBoardCreateFeederCircuitProjectCommand( + circuit, + board.inverse + ), + }; } private assertFeedingReference( diff --git a/src/db/repositories/external-object-new-row-project-command.repository.ts b/src/db/repositories/external-object-new-row-project-command.repository.ts index 27146b8..05edee2 100644 --- a/src/db/repositories/external-object-new-row-project-command.repository.ts +++ b/src/db/repositories/external-object-new-row-project-command.repository.ts @@ -2,6 +2,7 @@ import { eq } from "drizzle-orm"; import { assertCircuitDeviceRowQuantity } from "../../domain/calculations/circuit-device-row-quantity.js"; import { assertExternalObjectNewRowProjectCommand, + createExternalObjectNewRowProjectCommand, externalObjectAssignToNewRowCommandType, externalObjectDeleteCreatedRowCommandType, invertExternalObjectNewRowProjectCommand, @@ -20,7 +21,8 @@ import { toCircuitDeviceRowInsertValues, toCircuitDeviceRowSnapshot, } from "./circuit-device-row-structure.persistence.js"; -import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { syncSingleDeviceCircuitDisplayNames } from "./circuit-display-name-sync.persistence.js"; +import { executeProjectCommandTransactionWithAppliedForward } from "./project-command-transaction.persistence.js"; import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js"; import { applyExternalObjectLinks, @@ -42,13 +44,26 @@ export class ExternalObjectNewRowProjectCommandRepository ) { throw new Error("Created external rows may only be removed through project history."); } - return executeProjectCommandTransaction(this.database, input, (tx) => { + return executeProjectCommandTransactionWithAppliedForward(this.database, input, (tx) => { if (input.command.type === externalObjectAssignToNewRowCommandType) { this.insert(tx, input.projectId, input.command); } else { this.remove(tx, input.projectId, input.command); } - return invertExternalObjectNewRowProjectCommand(input.command); + const circuitDisplayNames = syncSingleDeviceCircuitDisplayNames( + tx, + input.source, + [input.command.payload.row.circuitId], + input.command.payload.circuitDisplayNames + ); + const forward = createExternalObjectNewRowProjectCommand( + input.command.type, + { ...input.command.payload, circuitDisplayNames } + ); + return { + forward, + inverse: invertExternalObjectNewRowProjectCommand(forward), + }; }); } diff --git a/src/db/repositories/project-device-row-sync-project-command.repository.ts b/src/db/repositories/project-device-row-sync-project-command.repository.ts index eb722d7..4cf0fde 100644 --- a/src/db/repositories/project-device-row-sync-project-command.repository.ts +++ b/src/db/repositories/project-device-row-sync-project-command.repository.ts @@ -6,6 +6,7 @@ import { projectDeviceSyncRowSnapshotFields, type ProjectDeviceSyncRowSnapshot, } from "../../domain/models/project-device-row-sync-project-command.model.js"; +import { invertCircuitDisplayNameTransitions } from "../../domain/models/circuit-display-name-sync.model.js"; import type { ExecuteProjectDeviceRowSyncCommandInput, ProjectDeviceRowSyncProjectCommandStore, @@ -16,7 +17,8 @@ import { circuitLists } from "../schema/circuit-lists.js"; import { circuits } from "../schema/circuits.js"; import { externalModelObjects } from "../schema/external-model-objects.js"; import { projectDevices } from "../schema/project-devices.js"; -import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; +import { syncSingleDeviceCircuitDisplayNames } from "./circuit-display-name-sync.persistence.js"; +import { executeProjectCommandTransactionWithAppliedForward } from "./project-command-transaction.persistence.js"; import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js"; type CircuitDeviceRow = typeof circuitDeviceRows.$inferSelect; @@ -29,7 +31,7 @@ export class ProjectDeviceRowSyncProjectCommandRepository execute(input: ExecuteProjectDeviceRowSyncCommandInput) { assertProjectDeviceRowSyncProjectCommand(input.command); - return executeProjectCommandTransaction( + return executeProjectCommandTransactionWithAppliedForward( this.database, input, (tx) => this.applyCommand(tx, input) @@ -107,18 +109,6 @@ export class ProjectDeviceRowSyncProjectCommandRepository } } - const inverse = createProjectDeviceRowSyncProjectCommand( - projectDevice.id, - invertProjectDeviceRowSyncOperation( - input.command.payload.operation - ), - input.command.payload.rows.map((row) => ({ - rowId: row.rowId, - expected: row.target, - target: row.expected, - })) - ); - for (const assignment of input.command.payload.rows) { const values: typeof assignment.target & { manualQuantity?: number; @@ -173,8 +163,40 @@ export class ProjectDeviceRowSyncProjectCommandRepository circuitId ); } + const circuitDisplayNames = syncSingleDeviceCircuitDisplayNames( + tx, + input.source, + input.command.payload.rows + .filter( + (assignment) => + assignment.expected.displayName !== + assignment.target.displayName + ) + .map( + (assignment) => persistedById.get(assignment.rowId)!.circuitId + ), + input.command.payload.circuitDisplayNames + ); - return inverse; + const { projectDeviceId, operation, rows } = input.command.payload; + return { + forward: createProjectDeviceRowSyncProjectCommand( + projectDeviceId, + operation, + rows, + circuitDisplayNames + ), + inverse: createProjectDeviceRowSyncProjectCommand( + projectDevice.id, + invertProjectDeviceRowSyncOperation(operation), + rows.map((row) => ({ + rowId: row.rowId, + expected: row.target, + target: row.expected, + })), + invertCircuitDisplayNameTransitions(circuitDisplayNames) + ), + }; } } diff --git a/src/domain/models/circuit-device-row-move-project-command.model.ts b/src/domain/models/circuit-device-row-move-project-command.model.ts index f40001e..f0d5dfb 100644 --- a/src/domain/models/circuit-device-row-move-project-command.model.ts +++ b/src/domain/models/circuit-device-row-move-project-command.model.ts @@ -1,4 +1,9 @@ import type { SerializedProjectCommand } from "./project-command.model.js"; +import { + assertCircuitDisplayNameTransitions, + withCircuitDisplayNames, + type CircuitDisplayNameTransition, +} from "./circuit-display-name-sync.model.js"; import { assertCircuitInsertProjectCommand, circuitInsertCommandType, @@ -22,6 +27,7 @@ export interface CircuitDeviceRowMoveAssignment { export interface CircuitDeviceRowMoveCommandPayload { moves: CircuitDeviceRowMoveAssignment[]; + circuitDisplayNames?: CircuitDisplayNameTransition[]; } export interface CircuitDeviceRowMoveProjectCommand @@ -38,6 +44,7 @@ export interface CircuitDeviceRowMoveWithNewCircuitCommandPayload { targetCircuitAction: CircuitDeviceRowMoveTargetCircuitAction; targetCircuit: CircuitSnapshot; moves: CircuitDeviceRowMoveAssignment[]; + circuitDisplayNames?: CircuitDisplayNameTransition[]; } export interface CircuitDeviceRowMoveWithNewCircuitProjectCommand @@ -51,12 +58,13 @@ export type CircuitDeviceRowMoveHistoryProjectCommand = | CircuitDeviceRowMoveWithNewCircuitProjectCommand; export function createCircuitDeviceRowMoveProjectCommand( - moves: CircuitDeviceRowMoveAssignment[] + moves: CircuitDeviceRowMoveAssignment[], + circuitDisplayNames?: CircuitDisplayNameTransition[] ): CircuitDeviceRowMoveProjectCommand { const command: CircuitDeviceRowMoveProjectCommand = { schemaVersion: circuitDeviceRowMoveCommandSchemaVersion, type: circuitDeviceRowMoveCommandType, - payload: { moves }, + payload: withCircuitDisplayNames({ moves }, circuitDisplayNames), }; assertCircuitDeviceRowMoveProjectCommand(command); return command; @@ -65,16 +73,20 @@ export function createCircuitDeviceRowMoveProjectCommand( export function createCircuitDeviceRowMoveWithNewCircuitProjectCommand( targetCircuitAction: CircuitDeviceRowMoveTargetCircuitAction, targetCircuit: CircuitSnapshot, - moves: CircuitDeviceRowMoveAssignment[] + moves: CircuitDeviceRowMoveAssignment[], + circuitDisplayNames?: CircuitDisplayNameTransition[] ): CircuitDeviceRowMoveWithNewCircuitProjectCommand { const command: CircuitDeviceRowMoveWithNewCircuitProjectCommand = { schemaVersion: circuitDeviceRowMoveCommandSchemaVersion, type: circuitDeviceRowMoveWithNewCircuitCommandType, - payload: { - targetCircuitAction, - targetCircuit, - moves, - }, + payload: withCircuitDisplayNames( + { + targetCircuitAction, + targetCircuit, + moves, + }, + circuitDisplayNames + ), }; assertCircuitDeviceRowMoveWithNewCircuitProjectCommand(command); return command; @@ -135,6 +147,7 @@ export function assertCircuitDeviceRowMoveProjectCommand( rowIds.add(move.rowId); } + assertCircuitDisplayNameTransitions(command.payload.circuitDisplayNames); } export function assertCircuitDeviceRowMoveWithNewCircuitProjectCommand( @@ -205,6 +218,7 @@ export function assertCircuitDeviceRowMoveWithNewCircuitProjectCommand( ); } } + assertCircuitDisplayNameTransitions(command.payload.circuitDisplayNames); } function assertNonEmptyString( diff --git a/src/domain/models/circuit-device-row-project-command.model.ts b/src/domain/models/circuit-device-row-project-command.model.ts index 8e949a1..da396d2 100644 --- a/src/domain/models/circuit-device-row-project-command.model.ts +++ b/src/domain/models/circuit-device-row-project-command.model.ts @@ -1,3 +1,6 @@ +// The frontend imports this model as a value, so it keeps type-only imports. +// The repository validates the stored circuitDisplayNames. +import type { CircuitDisplayNameTransition } from "./circuit-display-name-sync.model.js"; import type { SerializedProjectCommand } from "./project-command.model.js"; export const circuitDeviceRowUpdateCommandType = @@ -40,6 +43,7 @@ export interface CircuitDeviceRowUpdateFieldChange< export interface CircuitDeviceRowUpdateCommandPayload { rowId: string; changes: CircuitDeviceRowUpdateFieldChange[]; + circuitDisplayNames?: CircuitDisplayNameTransition[]; } export interface CircuitDeviceRowUpdateProjectCommand @@ -50,7 +54,8 @@ export interface CircuitDeviceRowUpdateProjectCommand export function createCircuitDeviceRowUpdateProjectCommand( rowId: string, - patch: CircuitDeviceRowUpdatePatch + patch: CircuitDeviceRowUpdatePatch, + circuitDisplayNames?: CircuitDisplayNameTransition[] ): CircuitDeviceRowUpdateProjectCommand { const normalizedPatch = patch.quantity !== undefined && patch.manualQuantity === undefined @@ -70,7 +75,9 @@ export function createCircuitDeviceRowUpdateProjectCommand( const command: CircuitDeviceRowUpdateProjectCommand = { schemaVersion: circuitDeviceRowUpdateCommandSchemaVersion, type: circuitDeviceRowUpdateCommandType, - payload: { rowId, changes }, + payload: circuitDisplayNames?.length + ? { rowId, changes, circuitDisplayNames } + : { rowId, changes }, }; assertCircuitDeviceRowUpdateProjectCommand(command); return command; diff --git a/src/domain/models/circuit-device-row-structure-project-command.model.ts b/src/domain/models/circuit-device-row-structure-project-command.model.ts index ebba5ac..7196b42 100644 --- a/src/domain/models/circuit-device-row-structure-project-command.model.ts +++ b/src/domain/models/circuit-device-row-structure-project-command.model.ts @@ -1,4 +1,9 @@ import type { SerializedProjectCommand } from "./project-command.model.js"; +import { + assertCircuitDisplayNameTransitions, + withCircuitDisplayNames, + type CircuitDisplayNameTransition, +} from "./circuit-display-name-sync.model.js"; import type { ExternalModelObjectSnapshot } from "../../external-model/domain/external-model-contracts.js"; import { parseExternalModelStateSnapshot } from "./project-state-snapshot.model.js"; @@ -40,12 +45,14 @@ export interface CircuitDeviceRowSnapshot { export interface CircuitDeviceRowInsertCommandPayload { row: CircuitDeviceRowSnapshot; externalObjects?: ExternalObjectLinkSnapshotTransition[]; + circuitDisplayNames?: CircuitDisplayNameTransition[]; } export interface CircuitDeviceRowDeleteCommandPayload { rowId: string; expectedCircuitId: string; externalObjects?: ExternalObjectLinkSnapshotTransition[]; + circuitDisplayNames?: CircuitDisplayNameTransition[]; } export interface CircuitDeviceRowInsertProjectCommand @@ -66,7 +73,8 @@ export type CircuitDeviceRowStructureProjectCommand = export function createCircuitDeviceRowInsertProjectCommand( row: CircuitDeviceRowSnapshot, - externalObjects?: ExternalObjectLinkSnapshotTransition[] + externalObjects?: ExternalObjectLinkSnapshotTransition[], + circuitDisplayNames?: CircuitDisplayNameTransition[] ): CircuitDeviceRowInsertProjectCommand { const normalizedRow = { ...row, @@ -75,7 +83,10 @@ export function createCircuitDeviceRowInsertProjectCommand( const command: CircuitDeviceRowInsertProjectCommand = { schemaVersion: circuitDeviceRowStructureCommandSchemaVersion, type: circuitDeviceRowInsertCommandType, - payload: { row: normalizedRow, ...(externalObjects?.length ? { externalObjects } : {}) }, + payload: withCircuitDisplayNames( + { row: normalizedRow, ...(externalObjects?.length ? { externalObjects } : {}) }, + circuitDisplayNames + ), }; assertCircuitDeviceRowInsertProjectCommand(command); return command; @@ -84,12 +95,16 @@ export function createCircuitDeviceRowInsertProjectCommand( export function createCircuitDeviceRowDeleteProjectCommand( rowId: string, expectedCircuitId: string, - externalObjects?: ExternalObjectLinkSnapshotTransition[] + externalObjects?: ExternalObjectLinkSnapshotTransition[], + circuitDisplayNames?: CircuitDisplayNameTransition[] ): CircuitDeviceRowDeleteProjectCommand { const command: CircuitDeviceRowDeleteProjectCommand = { schemaVersion: circuitDeviceRowStructureCommandSchemaVersion, type: circuitDeviceRowDeleteCommandType, - payload: { rowId, expectedCircuitId, ...(externalObjects?.length ? { externalObjects } : {}) }, + payload: withCircuitDisplayNames( + { rowId, expectedCircuitId, ...(externalObjects?.length ? { externalObjects } : {}) }, + circuitDisplayNames + ), }; assertCircuitDeviceRowDeleteProjectCommand(command); return command; @@ -155,6 +170,7 @@ export function assertCircuitDeviceRowInsertProjectCommand( } } assertExternalObjectLinkTransitions(command.payload.externalObjects); + assertCircuitDisplayNameTransitions(command.payload.circuitDisplayNames); } export function assertCircuitDeviceRowDeleteProjectCommand( @@ -176,6 +192,7 @@ export function assertCircuitDeviceRowDeleteProjectCommand( "expectedCircuitId" ); assertExternalObjectLinkTransitions(command.payload.externalObjects); + assertCircuitDisplayNameTransitions(command.payload.circuitDisplayNames); } export function assertExternalObjectLinkTransitions(value: unknown) { diff --git a/src/domain/models/circuit-display-name-sync.model.ts b/src/domain/models/circuit-display-name-sync.model.ts new file mode 100644 index 0000000..24c03f2 --- /dev/null +++ b/src/domain/models/circuit-display-name-sync.model.ts @@ -0,0 +1,99 @@ +/** + * A circuit with exactly one device row and no fed distribution board is + * shown as one compact row, so its own name is not visible or editable there. + * Its name therefore always follows that device row's name. Commands that + * change this state store the resulting circuit-name changes, so Undo/Redo + * restore them exactly, including a free name that was overwritten when only + * one device row remained. + */ +export interface CircuitDisplayNameTransition { + circuitId: string; + expected: string | null; + target: string | null; +} + +/** + * Returns the name a circuit must carry, or `null` when its current name is + * already correct or freely editable. + */ +export function resolveSingleDeviceCircuitDisplayName(input: { + displayName: string | null; + deviceRowDisplayNames: readonly string[]; + feedsDistributionBoard: boolean; +}): string | null { + if ( + input.deviceRowDisplayNames.length !== 1 || + input.feedsDistributionBoard + ) { + return null; + } + const deviceRowDisplayName = input.deviceRowDisplayNames[0]!; + return input.displayName === deviceRowDisplayName + ? null + : deviceRowDisplayName; +} + +export function invertCircuitDisplayNameTransitions( + transitions: readonly CircuitDisplayNameTransition[] +): CircuitDisplayNameTransition[] { + return [...transitions].reverse().map((transition) => ({ + circuitId: transition.circuitId, + expected: transition.target, + target: transition.expected, + })); +} + +/** Sets the optional payload field only when it carries changes. */ +export function withCircuitDisplayNames( + payload: Payload, + circuitDisplayNames: readonly CircuitDisplayNameTransition[] | undefined +): Payload & { circuitDisplayNames?: CircuitDisplayNameTransition[] } { + const { circuitDisplayNames: _previous, ...rest } = payload as Payload & { + circuitDisplayNames?: unknown; + }; + return ( + circuitDisplayNames?.length + ? { ...rest, circuitDisplayNames: [...circuitDisplayNames] } + : rest + ) as Payload & { circuitDisplayNames?: CircuitDisplayNameTransition[] }; +} + +export function assertCircuitDisplayNameTransitions( + value: unknown +): asserts value is CircuitDisplayNameTransition[] | undefined { + if (value === undefined) return; + if (!Array.isArray(value) || value.length === 0) { + throw new Error("circuitDisplayNames must be a non-empty array."); + } + const circuitIds = new Set(); + for (const transition of value) { + if ( + transition === null || + typeof transition !== "object" || + Array.isArray(transition) + ) { + throw new Error("Invalid circuit display-name transition."); + } + const { circuitId, expected, target } = transition as Record< + string, + unknown + >; + if ( + typeof circuitId !== "string" || + !circuitId.trim() || + circuitIds.has(circuitId) + ) { + throw new Error( + "Circuit display-name transition has an invalid or duplicate circuit id." + ); + } + if ( + (expected !== null && typeof expected !== "string") || + (target !== null && typeof target !== "string") || + expected === target + ) { + throw new Error("Invalid circuit display-name transition values."); + } + circuitIds.add(circuitId); + } +} diff --git a/src/domain/models/distribution-board-project-command.model.ts b/src/domain/models/distribution-board-project-command.model.ts index 961cf53..4e8c972 100644 --- a/src/domain/models/distribution-board-project-command.model.ts +++ b/src/domain/models/distribution-board-project-command.model.ts @@ -5,6 +5,11 @@ import { type DistributionBoardInfoField, type DistributionBoardSupplyType, } from "../../shared/constants/distribution-board.js"; +import { + assertCircuitDisplayNameTransitions, + withCircuitDisplayNames, + type CircuitDisplayNameTransition, +} from "./circuit-display-name-sync.model.js"; import type { SerializedProjectCommand } from "./project-command.model.js"; export const distributionBoardUpdateCommandType = @@ -39,6 +44,8 @@ export interface DistributionBoardUpdateFieldChange< export interface DistributionBoardUpdateCommandPayload { distributionBoardId: string; changes: DistributionBoardUpdateFieldChange[]; + // Single-device feeding circuits that become compact rows take their row name. + circuitDisplayNames?: CircuitDisplayNameTransition[]; } export interface DistributionBoardUpdateProjectCommand @@ -53,18 +60,22 @@ export function createDistributionBoardUpdateProjectCommand( distributionBoardId: string, patch: DistributionBoardUpdatePatch, schemaVersion: DistributionBoardUpdateProjectCommand["schemaVersion"] = - distributionBoardUpdateCommandSchemaVersion + distributionBoardUpdateCommandSchemaVersion, + circuitDisplayNames?: CircuitDisplayNameTransition[] ): DistributionBoardUpdateProjectCommand { const command: DistributionBoardUpdateProjectCommand = { schemaVersion, type: distributionBoardUpdateCommandType, - payload: { - distributionBoardId, - changes: Object.entries(patch).map(([field, value]) => ({ - field: field as DistributionBoardUpdateField, - value, - })) as DistributionBoardUpdateFieldChange[], - }, + payload: withCircuitDisplayNames( + { + distributionBoardId, + changes: Object.entries(patch).map(([field, value]) => ({ + field: field as DistributionBoardUpdateField, + value, + })) as DistributionBoardUpdateFieldChange[], + }, + circuitDisplayNames + ), }; assertDistributionBoardUpdateProjectCommand(command); return command; @@ -154,6 +165,7 @@ export function assertDistributionBoardUpdateProjectCommand( } seen.add(change.field); } + assertCircuitDisplayNameTransitions(command.payload.circuitDisplayNames); } function isInfoField(field: string): field is DistributionBoardInfoField { diff --git a/src/domain/models/external-object-new-row-project-command.model.ts b/src/domain/models/external-object-new-row-project-command.model.ts index 6bae039..e62cec3 100644 --- a/src/domain/models/external-object-new-row-project-command.model.ts +++ b/src/domain/models/external-object-new-row-project-command.model.ts @@ -5,6 +5,12 @@ import { circuitDeviceRowStructureCommandSchemaVersion, type CircuitDeviceRowSnapshot, } from "./circuit-device-row-structure-project-command.model.js"; +import { + assertCircuitDisplayNameTransitions, + invertCircuitDisplayNameTransitions, + withCircuitDisplayNames, + type CircuitDisplayNameTransition, +} from "./circuit-display-name-sync.model.js"; import type { SerializedProjectCommand } from "./project-command.model.js"; import { parseExternalModelStateSnapshot } from "./project-state-snapshot.model.js"; @@ -21,6 +27,7 @@ export interface ExternalObjectNewRowPayload { target: ExternalModelObjectSnapshot; }>; confirmedConflictObjectIds: string[]; + circuitDisplayNames?: CircuitDisplayNameTransition[]; } export interface ExternalObjectNewRowProjectCommand @@ -38,14 +45,17 @@ export function createExternalObjectNewRowProjectCommand( const command: ExternalObjectNewRowProjectCommand = { schemaVersion: externalObjectNewRowCommandSchemaVersion, type, - payload: { - row: { - ...payload.row, - manualQuantity: payload.row.manualQuantity ?? payload.row.quantity, + payload: withCircuitDisplayNames( + { + row: { + ...payload.row, + manualQuantity: payload.row.manualQuantity ?? payload.row.quantity, + }, + objects: payload.objects, + confirmedConflictObjectIds: [...payload.confirmedConflictObjectIds], }, - objects: payload.objects, - confirmedConflictObjectIds: [...payload.confirmedConflictObjectIds], - }, + payload.circuitDisplayNames + ), }; assertExternalObjectNewRowProjectCommand(command); return command; @@ -65,6 +75,9 @@ export function invertExternalObjectNewRowProjectCommand( target: expected, })), confirmedConflictObjectIds: command.payload.confirmedConflictObjectIds, + circuitDisplayNames: invertCircuitDisplayNameTransitions( + command.payload.circuitDisplayNames ?? [] + ), } ); } @@ -112,6 +125,7 @@ export function assertExternalObjectNewRowProjectCommand( } confirmedIds.add(objectId); } + assertCircuitDisplayNameTransitions(command.payload.circuitDisplayNames); } function parseRow(value: unknown): CircuitDeviceRowSnapshot { diff --git a/src/domain/models/project-device-row-sync-project-command.model.ts b/src/domain/models/project-device-row-sync-project-command.model.ts index ac34121..8c11849 100644 --- a/src/domain/models/project-device-row-sync-project-command.model.ts +++ b/src/domain/models/project-device-row-sync-project-command.model.ts @@ -5,6 +5,11 @@ import { type CircuitDeviceRowUpdateField, type CircuitDeviceRowUpdateValues, } from "./circuit-device-row-project-command.model.js"; +import { + assertCircuitDisplayNameTransitions, + withCircuitDisplayNames, + type CircuitDisplayNameTransition, +} from "./circuit-display-name-sync.model.js"; import type { SerializedProjectCommand } from "./project-command.model.js"; export const projectDeviceRowSyncCommandType = @@ -47,6 +52,7 @@ export interface ProjectDeviceRowSyncCommandPayload { projectDeviceId: string; operation: ProjectDeviceRowSyncOperation; rows: ProjectDeviceRowSyncAssignment[]; + circuitDisplayNames?: CircuitDisplayNameTransition[]; } export interface ProjectDeviceRowSyncProjectCommand @@ -58,12 +64,16 @@ export interface ProjectDeviceRowSyncProjectCommand export function createProjectDeviceRowSyncProjectCommand( projectDeviceId: string, operation: ProjectDeviceRowSyncOperation, - rows: ProjectDeviceRowSyncAssignment[] + rows: ProjectDeviceRowSyncAssignment[], + circuitDisplayNames?: CircuitDisplayNameTransition[] ): ProjectDeviceRowSyncProjectCommand { const command: ProjectDeviceRowSyncProjectCommand = { schemaVersion: projectDeviceRowSyncCommandSchemaVersion, type: projectDeviceRowSyncCommandType, - payload: { projectDeviceId, operation, rows }, + payload: withCircuitDisplayNames( + { projectDeviceId, operation, rows }, + circuitDisplayNames + ), }; assertProjectDeviceRowSyncProjectCommand(command); return command; @@ -131,6 +141,7 @@ export function assertProjectDeviceRowSyncProjectCommand( ); rowIds.add(row.rowId); } + assertCircuitDisplayNameTransitions(command.payload.circuitDisplayNames); } export function invertProjectDeviceRowSyncOperation( diff --git a/src/server/controllers/external-objects.controller.ts b/src/server/controllers/external-objects.controller.ts index a381faa..ab8ca50 100644 --- a/src/server/controllers/external-objects.controller.ts +++ b/src/server/controllers/external-objects.controller.ts @@ -224,7 +224,8 @@ export async function assignExternalObjectsToNewCircuit(req: Request, res: Respo circuitListId: sectionLocation.tree.circuitListId, sectionId, equipmentIdentifier: await circuitNumberingService.getNextIdentifier(sectionId), - displayName: null, + // The single external row is shown compactly, so the circuit carries its name. + displayName: row.displayName, sortOrder: Math.max(0, ...sectionLocation.section.circuits.map((entry) => entry.sortOrder)) + 10, cableType: null, cableCrossSection: null, cableLength: null, rcdAssignment: null, terminalDesignation: null, diff --git a/tests/single-device-circuit-name.test.ts b/tests/single-device-circuit-name.test.ts new file mode 100644 index 0000000..7019414 --- /dev/null +++ b/tests/single-device-circuit-name.test.ts @@ -0,0 +1,516 @@ +import path from "node:path"; +import assert from "node:assert/strict"; +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { describe, it } from "node:test"; +import { eq } from "drizzle-orm"; +import { migrate } from "drizzle-orm/better-sqlite3/migrator"; +import { + createDatabaseContext, + type DatabaseContext, +} from "../src/db/database-context.js"; +import { CircuitDeviceRowMoveProjectCommandRepository } from "../src/db/repositories/circuit-device-row-move-project-command.repository.js"; +import { CircuitDeviceRowProjectCommandRepository } from "../src/db/repositories/circuit-device-row-project-command.repository.js"; +import { CircuitDeviceRowStructureProjectCommandRepository } from "../src/db/repositories/circuit-device-row-structure-project-command.repository.js"; +import { CircuitProjectCommandRepository } from "../src/db/repositories/circuit-project-command.repository.js"; +import { CircuitStructureProjectCommandRepository } from "../src/db/repositories/circuit-structure-project-command.repository.js"; +import { DistributionBoardStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-structure-project-command.repository.js"; +import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js"; +import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js"; +import { circuitSections } from "../src/db/schema/circuit-sections.js"; +import { circuits } from "../src/db/schema/circuits.js"; +import { distributionBoards } from "../src/db/schema/distribution-boards.js"; +import { projects } from "../src/db/schema/projects.js"; +import { resolveSingleDeviceCircuitDisplayName } from "../src/domain/models/circuit-display-name-sync.model.js"; +import { createCircuitDeviceRowMoveWithNewCircuitProjectCommand } from "../src/domain/models/circuit-device-row-move-project-command.model.js"; +import { createCircuitDeviceRowUpdateProjectCommand } from "../src/domain/models/circuit-device-row-project-command.model.js"; +import { + createCircuitDeviceRowDeleteProjectCommand, + createCircuitDeviceRowInsertProjectCommand, + type CircuitDeviceRowSnapshot, +} from "../src/domain/models/circuit-device-row-structure-project-command.model.js"; +import { createCircuitUpdateProjectCommand } from "../src/domain/models/circuit-project-command.model.js"; +import { + createCircuitInsertProjectCommand, + type CircuitSnapshot, +} from "../src/domain/models/circuit-structure-project-command.model.js"; +import { createDistributionBoardUpdateProjectCommand } from "../src/domain/models/distribution-board-project-command.model.js"; +import type { SerializedProjectCommand } from "../src/domain/models/project-command.model.js"; +import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js"; + +interface Fixture { + context: DatabaseContext; + boardId: string; + circuitListId: string; + sectionId: string; +} + +function createFixture(): Fixture { + const context = createDatabaseContext(":memory:"); + migrate(context.db, { + migrationsFolder: path.resolve("src", "db", "migrations"), + }); + context.db.insert(projects).values({ id: "project-1", name: "Projekt" }).run(); + const boards = new DistributionBoardFixtureRepository(context.db); + const board = boards.createWithCircuitListAndDefaultSections("project-1", "UV-01"); + const section = context.db + .select() + .from(circuitSections) + .where(eq(circuitSections.circuitListId, board.id)) + .get(); + assert.ok(section); + return { + context, + boardId: board.id, + circuitListId: section.circuitListId, + sectionId: section.id, + }; +} + +function insertCircuit( + fixture: Fixture, + id: string, + displayName: string | null, + rowNames: string[], + sortOrder = 10 +) { + fixture.context.db + .insert(circuits) + .values({ + id, + circuitListId: fixture.circuitListId, + sectionId: fixture.sectionId, + equipmentIdentifier: `-1F${sortOrder}`, + displayName, + sortOrder, + voltage: 230, + isReserve: rowNames.length === 0 ? 1 : 0, + }) + .run(); + rowNames.forEach((rowName, index) => { + fixture.context.db + .insert(circuitDeviceRows) + .values(rowValues(`${id}-row-${index + 1}`, id, rowName, (index + 1) * 10)) + .run(); + }); +} + +function rowValues( + id: string, + circuitId: string, + displayName: string, + sortOrder: number +): CircuitDeviceRowSnapshot { + return { + id, + circuitId, + linkedProjectDeviceId: null, + sortOrder, + name: displayName, + displayName, + phaseType: "single_phase", + connectionKind: null, + costGroup: null, + category: null, + level: null, + roomId: null, + roomNumberSnapshot: null, + roomNameSnapshot: null, + quantity: 1, + manualQuantity: 1, + powerPerUnit: 0.1, + simultaneityFactor: 1, + cosPhi: null, + remark: null, + overriddenFields: null, + }; +} + +function circuitName(fixture: Fixture, circuitId: string) { + return fixture.context.db + .select({ displayName: circuits.displayName }) + .from(circuits) + .where(eq(circuits.id, circuitId)) + .get()?.displayName; +} + +interface HistoryStore { + execute(input: { + projectId: string; + expectedRevision: number; + source: "user" | "undo" | "redo"; + historyTargetChangeSetId?: string; + command: never; + }): unknown; +} + +/** Replays the stored history step like the project command service does. */ +function replay( + fixture: Fixture, + store: HistoryStore, + direction: "undo" | "redo" +) { + const history = new ProjectHistoryRepository(fixture.context.db); + const step = history.getNextCommand("project-1", direction); + assert.ok(step); + store.execute({ + projectId: "project-1", + expectedRevision: history.getState("project-1")!.currentRevision, + source: direction, + historyTargetChangeSetId: step.changeSetId, + command: step.command as SerializedProjectCommand as never, + }); +} + +function currentRevision(fixture: Fixture) { + return new ProjectHistoryRepository(fixture.context.db).getState("project-1")! + .currentRevision; +} + +describe("single-device circuit name", () => { + it("follows the only device row unless a distribution board is fed", () => { + const resolve = ( + displayName: string | null, + deviceRowDisplayNames: string[], + feedsDistributionBoard = false + ) => + resolveSingleDeviceCircuitDisplayName({ + displayName, + deviceRowDisplayNames, + feedsDistributionBoard, + }); + assert.equal(resolve("Neuer Stromkreis", ["Ersatzlast"]), "Ersatzlast"); + assert.equal(resolve(null, ["Ersatzlast"]), "Ersatzlast"); + assert.equal(resolve("Ersatzlast", ["Ersatzlast"]), null); + assert.equal(resolve("Frei", ["A", "B"]), null); + assert.equal(resolve("Reserve", []), null); + assert.equal(resolve("Zuleitung UV", ["A"], true), null); + }); + + it("names a new single-device circuit after its row and keeps it on undo/redo", () => { + const fixture = createFixture(); + try { + const store = new CircuitStructureProjectCommandRepository(fixture.context.db); + const snapshot: CircuitSnapshot = { + id: "circuit-new", + circuitListId: fixture.circuitListId, + sectionId: fixture.sectionId, + equipmentIdentifier: "-1F1", + displayName: "Neuer Stromkreis", + sortOrder: 10, + cableType: null, + cableCrossSection: null, + cableLength: null, + rcdAssignment: null, + terminalDesignation: null, + voltage: 230, + controlRequirement: null, + status: null, + isReserve: false, + remark: null, + deviceRows: [rowValues("row-new", "circuit-new", "Ersatzlast", 10)], + }; + store.execute({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + command: createCircuitInsertProjectCommand(snapshot), + }); + assert.equal(circuitName(fixture, "circuit-new"), "Ersatzlast"); + + replay(fixture, store, "undo"); + assert.equal(circuitName(fixture, "circuit-new"), undefined); + replay(fixture, store, "redo"); + assert.equal(circuitName(fixture, "circuit-new"), "Ersatzlast"); + } finally { + fixture.context.close(); + } + }); + + it("renames the circuit with its only row and restores both on undo", () => { + const fixture = createFixture(); + try { + insertCircuit(fixture, "circuit-1", "Neuer Stromkreis", ["Gerät"]); + const store = new CircuitDeviceRowProjectCommandRepository(fixture.context.db); + store.executeUpdate({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + command: createCircuitDeviceRowUpdateProjectCommand("circuit-1-row-1", { + displayName: "Ersatzlast", + }), + }); + assert.equal(circuitName(fixture, "circuit-1"), "Ersatzlast"); + + replay(fixture, { execute: (input) => store.executeUpdate(input) }, "undo"); + assert.equal(circuitName(fixture, "circuit-1"), "Neuer Stromkreis"); + replay(fixture, { execute: (input) => store.executeUpdate(input) }, "redo"); + assert.equal(circuitName(fixture, "circuit-1"), "Ersatzlast"); + } finally { + fixture.context.close(); + } + }); + + it("leaves a multi-device circuit name free and does not touch it for other row edits", () => { + const fixture = createFixture(); + try { + insertCircuit(fixture, "circuit-1", "Frei", ["A", "B"]); + const store = new CircuitDeviceRowProjectCommandRepository(fixture.context.db); + store.executeUpdate({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + command: createCircuitDeviceRowUpdateProjectCommand("circuit-1-row-1", { + displayName: "A neu", + }), + }); + assert.equal(circuitName(fixture, "circuit-1"), "Frei"); + + insertCircuit(fixture, "circuit-2", "Bewusst abweichend", ["C"], 20); + store.executeUpdate({ + projectId: "project-1", + expectedRevision: 1, + source: "user", + command: createCircuitDeviceRowUpdateProjectCommand("circuit-2-row-1", { + quantity: 2, + }), + }); + assert.equal(circuitName(fixture, "circuit-2"), "Bewusst abweichend"); + } finally { + fixture.context.close(); + } + }); + + it("takes the remaining row name after a delete and restores the free name on undo", () => { + const fixture = createFixture(); + try { + insertCircuit(fixture, "circuit-1", "Gerät A", ["Gerät A"]); + const store = new CircuitDeviceRowStructureProjectCommandRepository(fixture.context.db); + store.execute({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + command: createCircuitDeviceRowInsertProjectCommand( + rowValues("row-b", "circuit-1", "Gerät B", 20) + ), + }); + assert.equal(circuitName(fixture, "circuit-1"), "Gerät A"); + + new CircuitProjectCommandRepository(fixture.context.db).executeUpdate({ + projectId: "project-1", + expectedRevision: 1, + source: "user", + command: createCircuitUpdateProjectCommand("circuit-1", { + displayName: "Küche", + }), + }); + store.execute({ + projectId: "project-1", + expectedRevision: 2, + source: "user", + command: createCircuitDeviceRowDeleteProjectCommand("circuit-1-row-1", "circuit-1"), + }); + assert.equal(circuitName(fixture, "circuit-1"), "Gerät B"); + + replay(fixture, store, "undo"); + assert.equal(circuitName(fixture, "circuit-1"), "Küche"); + replay(fixture, store, "redo"); + assert.equal(circuitName(fixture, "circuit-1"), "Gerät B"); + assert.equal(currentRevision(fixture), 5); + } finally { + fixture.context.close(); + } + }); + + it("keeps stored legacy commands without name changes executable", () => { + const fixture = createFixture(); + try { + insertCircuit(fixture, "circuit-1", "Küche", ["Gerät A", "Gerät B"]); + const store = new CircuitDeviceRowStructureProjectCommandRepository(fixture.context.db); + store.execute({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + command: createCircuitDeviceRowDeleteProjectCommand("circuit-1-row-1", "circuit-1"), + }); + assert.equal(circuitName(fixture, "circuit-1"), "Gerät B"); + const history = new ProjectHistoryRepository(fixture.context.db); + const step = history.getNextCommand("project-1", "undo"); + assert.ok(step); + const legacyInverse = structuredClone(step.command) as SerializedProjectCommand<{ + circuitDisplayNames?: unknown; + }>; + delete legacyInverse.payload.circuitDisplayNames; + store.execute({ + projectId: "project-1", + expectedRevision: 1, + source: "undo", + historyTargetChangeSetId: step.changeSetId, + command: legacyInverse as never, + }); + assert.equal(circuitName(fixture, "circuit-1"), "Gerät B"); + } finally { + fixture.context.close(); + } + }); + + it("names a new circuit after its single moved row and syncs the source circuit", () => { + const fixture = createFixture(); + try { + insertCircuit(fixture, "circuit-1", "Küche", ["Gerät A", "Gerät B"]); + const store = new CircuitDeviceRowMoveProjectCommandRepository(fixture.context.db); + store.execute({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + command: createCircuitDeviceRowMoveWithNewCircuitProjectCommand( + "create", + { + id: "circuit-new", + circuitListId: fixture.circuitListId, + sectionId: fixture.sectionId, + equipmentIdentifier: "-1F99", + displayName: "Neuer Stromkreis", + sortOrder: 99, + cableType: null, + cableCrossSection: null, + cableLength: null, + rcdAssignment: null, + terminalDesignation: null, + voltage: null, + controlRequirement: null, + status: null, + isReserve: true, + remark: null, + deviceRows: [], + }, + [ + { + rowId: "circuit-1-row-2", + expectedCircuitId: "circuit-1", + expectedSortOrder: 20, + targetCircuitId: "circuit-new", + targetSortOrder: 10, + }, + ] + ), + }); + assert.equal(circuitName(fixture, "circuit-new"), "Gerät B"); + assert.equal(circuitName(fixture, "circuit-1"), "Gerät A"); + + replay(fixture, store, "undo"); + assert.equal(circuitName(fixture, "circuit-new"), undefined); + assert.equal(circuitName(fixture, "circuit-1"), "Küche"); + replay(fixture, store, "redo"); + assert.equal(circuitName(fixture, "circuit-new"), "Gerät B"); + assert.equal(circuitName(fixture, "circuit-1"), "Gerät A"); + } finally { + fixture.context.close(); + } + }); + + it("does not rename a feeder circuit until its downstream board is unlinked", () => { + const fixture = createFixture(); + try { + insertCircuit(fixture, "circuit-1", "Zuleitung UV-02", ["Gerät A"]); + const boards = new DistributionBoardFixtureRepository(fixture.context.db); + const downstream = boards.createWithCircuitListAndDefaultSections("project-1", "UV-02"); + fixture.context.db + .update(distributionBoards) + .set({ feedingDistributionBoardId: fixture.boardId, feedingCircuitId: "circuit-1" }) + .where(eq(distributionBoards.id, downstream.id)) + .run(); + + const rowStore = new CircuitDeviceRowProjectCommandRepository(fixture.context.db); + rowStore.executeUpdate({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + command: createCircuitDeviceRowUpdateProjectCommand("circuit-1-row-1", { + displayName: "Gerät neu", + }), + }); + assert.equal(circuitName(fixture, "circuit-1"), "Zuleitung UV-02"); + + const boardStore = new DistributionBoardStructureProjectCommandRepository(fixture.context.db); + const execute = (input: Parameters[0]) => + boardStore.executeUpdate(input); + boardStore.executeUpdate({ + projectId: "project-1", + expectedRevision: 1, + source: "user", + command: createDistributionBoardUpdateProjectCommand(downstream.id, { + feedingDistributionBoardId: null, + feedingCircuitId: null, + }), + }); + assert.equal(circuitName(fixture, "circuit-1"), "Gerät neu"); + + replay(fixture, { execute }, "undo"); + assert.equal(circuitName(fixture, "circuit-1"), "Zuleitung UV-02"); + replay(fixture, { execute }, "redo"); + assert.equal(circuitName(fixture, "circuit-1"), "Gerät neu"); + } finally { + fixture.context.close(); + } + }); + + it("aligns existing single-device circuit names in migration 0016", () => { + const source = path.resolve("src", "db", "migrations"); + const staging = mkdtempSync(path.join(tmpdir(), "leistungsbilanz-circuit-name-migration-")); + const journal = JSON.parse(readFileSync(path.join(source, "meta", "_journal.json"), "utf8")); + journal.entries = journal.entries.filter((entry: { idx: number }) => entry.idx <= 15); + mkdirSync(path.join(staging, "meta")); + writeFileSync(path.join(staging, "meta", "_journal.json"), JSON.stringify(journal)); + for (const entry of journal.entries) { + copyFileSync(path.join(source, `${entry.tag}.sql`), path.join(staging, `${entry.tag}.sql`)); + } + + const context = createDatabaseContext(":memory:"); + try { + migrate(context.db, { migrationsFolder: staging }); + context.sqlite.exec(` + INSERT INTO projects (id, name) VALUES ('p', 'Projekt'); + INSERT INTO distribution_boards (id, project_id, name) VALUES ('b', 'p', 'UV'); + INSERT INTO circuit_lists (id, project_id, distribution_board_id, name) VALUES ('l', 'p', 'b', 'Liste'); + INSERT INTO circuit_sections (id, circuit_list_id, key, display_name, prefix) VALUES ('s', 'l', 'single_phase', 'Einphasig', '-2F1.'); + INSERT INTO circuits (id, circuit_list_id, section_id, equipment_identifier, display_name) VALUES + ('single', 'l', 's', '-2F1.1', 'Neuer Stromkreis'), + ('unnamed', 'l', 's', '-2F1.2', NULL), + ('multi', 'l', 's', '-2F1.3', 'Küche'), + ('reserve', 'l', 's', '-2F1.4', 'Reserve'), + ('feeder', 'l', 's', '-2F1.5', 'Zuleitung UV-02'); + INSERT INTO circuit_device_rows + (id, circuit_id, name, display_name, quantity, manual_quantity, power_per_unit, simultaneity_factor) + VALUES + ('r1', 'single', 'Gerät', 'Ersatzlast', 1, 1, 1, 1), + ('r2', 'unnamed', 'Gerät', 'Leuchte', 1, 1, 1, 1), + ('r3', 'multi', 'Gerät', 'A', 1, 1, 1, 1), + ('r4', 'multi', 'Gerät', 'B', 1, 1, 1, 1), + ('r5', 'feeder', 'Gerät', 'Pumpe', 1, 1, 1, 1); + INSERT INTO distribution_boards (id, project_id, name, feeding_distribution_board_id, feeding_circuit_id) + VALUES ('b2', 'p', 'UV-02', 'b', 'feeder'); + `); + + migrate(context.db, { migrationsFolder: source }); + + const names = Object.fromEntries( + (context.sqlite.prepare("SELECT id, display_name FROM circuits").all() as Array<{ + id: string; + display_name: string | null; + }>).map((row) => [row.id, row.display_name]) + ); + assert.deepEqual(names, { + single: "Ersatzlast", + unnamed: "Leuchte", + multi: "Küche", + reserve: "Reserve", + feeder: "Zuleitung UV-02", + }); + assert.deepEqual(context.sqlite.pragma("foreign_key_check"), []); + } finally { + context.close(); + rmSync(staging, { recursive: true, force: true }); + } + }); +});