Files
CableGUI/internal/db/models.go
mAi 4202d0465f feat(v5 slice 1): clamps schema + store helpers + snapshot
Migration 007 introduces the v5 routing primitive:
- clamps table (project-scoped, optional frame_id, excalidraw_id).
- cable_clamps join (cable_id, clamp_id, ord) with PK on (cable_id, ord)
  and UNIQUE (cable_id, clamp_id) to block a clamp visiting the same
  cable twice.

Store helpers in internal/db/clamps.go:
- CreateClamp / GetClamp / ListClamps / UpdateClamp / DeleteClamp —
  standard project-scoped CRUD. UpdateClamp uses FrameRef tri-state.
- AttachClampToCable — appends or inserts at a given ord. Mid-sequence
  inserts use a two-pass shift (bump by 10000, settle to ord+1) since
  SQLite UPDATE doesn't support ORDER BY and a single bulk +1 would
  collide with the UNIQUE (cable_id, ord) PK.
- DetachClampFromCable — removes the row then closes the gap.
- ReorderCableClamps — replaces the whole sequence in one tx.
- ListClampsForCable / ListCableClamps — read helpers.

Snapshot now carries clamps + cable_clamps arrays so the frontend can
hydrate everything in one call.

Tests cover create / update / cascade-delete / attach (append + insert
+ duplicate-rejected) / detach (gap closes) / reorder / snapshot.
2026-05-16 13:40:53 +02:00

249 lines
9.8 KiB
Go

package db
// Project is the top-level entity. One project ↔ one .excalidraw drawing.
type Project struct {
ID int64 `json:"id"`
Name string `json:"name"`
DrawingName string `json:"drawing_name"`
Description string `json:"description"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// CableType is global. Renaming/recolouring affects every project.
type CableType struct {
ID int64 `json:"id"`
Name string `json:"name"`
Color string `json:"color"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// Frame is a sub-zone inside a project (`desk`, `rack`, …).
type Frame struct {
ID int64 `json:"id"`
ProjectID int64 `json:"project_id"`
Name string `json:"name"`
X float64 `json:"x"`
Y float64 `json:"y"`
Width float64 `json:"width"`
Height float64 `json:"height"`
ExcalidrawID *string `json:"excalidraw_id,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// Device is a hardware item inside a project, optionally inside a frame.
// v4: type_id (nullable) lets a device inherit its port profile from a
// device_types catalog row.
type Device struct {
ID int64 `json:"id"`
ProjectID int64 `json:"project_id"`
FrameID *int64 `json:"frame_id"` // nullable: device "outside" any frame
TypeID *int64 `json:"type_id"` // nullable: freeform device when null
Name string `json:"name"`
Color string `json:"color"`
X float64 `json:"x"`
Y float64 `json:"y"`
Width float64 `json:"width"`
Height float64 `json:"height"`
ExcalidrawID *string `json:"excalidraw_id,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// DeviceType is a catalog row. Built-in rows have ProjectID nil and
// BuiltIn true. Project-custom rows have ProjectID set.
type DeviceType struct {
ID int64 `json:"id"`
ProjectID *int64 `json:"project_id"`
Name string `json:"name"`
Kind string `json:"kind"`
Icon *string `json:"icon,omitempty"`
Description string `json:"description"`
BuiltIn bool `json:"built_in"`
Ports []DeviceTypePort `json:"ports"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// DeviceTypePort is a row of a type's port profile. The seeder uses
// (cable_type_id, count, label_prefix, edge, sort_order) to lay out
// concrete ports on a freshly-created device.
type DeviceTypePort struct {
ID int64 `json:"id"`
DeviceTypeID int64 `json:"device_type_id"`
CableTypeID int64 `json:"cable_type_id"`
LabelPrefix string `json:"label_prefix"`
Count int `json:"count"`
Edge string `json:"edge"`
SortOrder int `json:"sort_order"`
}
// Port is a connector on a device. cable_type colour drives the visual
// rendering; ports are instance-owned even when seeded from a type.
type Port struct {
ID int64 `json:"id"`
ProjectID int64 `json:"project_id"`
DeviceID int64 `json:"device_id"`
TypeID int64 `json:"type_id"` // cable type
Label *string `json:"label"`
XOffset float64 `json:"x_offset"`
YOffset float64 `json:"y_offset"`
ExcalidrawID *string `json:"excalidraw_id,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// ConnectionRequirement is the solver's per-project input.
// pair_lo/pair_hi are the ordered (MIN,MAX) of (from, to) so the
// UNIQUE on (project_id, pair_lo, pair_hi, preferred_cable_type_id)
// prevents (A,B,T) AND (B,A,T) from coexisting.
type ConnectionRequirement struct {
ID int64 `json:"id"`
ProjectID int64 `json:"project_id"`
FromDeviceID int64 `json:"from_device_id"`
ToDeviceID int64 `json:"to_device_id"`
PreferredCableTypeID *int64 `json:"preferred_cable_type_id"`
MustConnect bool `json:"must_connect"`
Notes string `json:"notes"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// Cable is a typed connection. Each endpoint is exactly one of
// (port, device, io-marker). Auto=true means the solver placed it.
type Cable struct {
ID int64 `json:"id"`
ProjectID int64 `json:"project_id"`
TypeID int64 `json:"type_id"`
Label *string `json:"label"`
FromPortID *int64 `json:"from_port_id"`
FromDeviceID *int64 `json:"from_device_id"`
FromIOID *int64 `json:"from_io_id"`
ToPortID *int64 `json:"to_port_id"`
ToDeviceID *int64 `json:"to_device_id"`
ToIOID *int64 `json:"to_io_id"`
Auto bool `json:"auto"`
ExcalidrawID *string `json:"excalidraw_id,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// Bundle is a named group of cables that physically run together.
type Bundle struct {
ID int64 `json:"id"`
ProjectID int64 `json:"project_id"`
Name string `json:"name"`
Auto bool `json:"auto"`
CableIDs []int64 `json:"cable_ids"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// SetupTemplate is a named recipe of device-types + requirements.
type SetupTemplate struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
BuiltIn bool `json:"built_in"`
Devices []SetupTemplateDevice `json:"devices"`
Requirements []SetupTemplateRequirement `json:"requirements"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type SetupTemplateDevice struct {
ID int64 `json:"id"`
TemplateID int64 `json:"template_id"`
DeviceTypeID int64 `json:"device_type_id"`
DeviceType *DeviceType `json:"device_type,omitempty"`
SuggestedName *string `json:"suggested_name"`
SortOrder int `json:"sort_order"`
}
type SetupTemplateRequirement struct {
ID int64 `json:"id"`
TemplateID int64 `json:"template_id"`
FromTemplateDeviceID int64 `json:"from_template_device_id"`
ToTemplateDeviceID int64 `json:"to_template_device_id"`
PreferredCableTypeID *int64 `json:"preferred_cable_type_id"`
MustConnect bool `json:"must_connect"`
}
// SolveResult is the response shape from POST /api/projects/:pid/solve.
type SolveResult struct {
CablesAdded []Cable `json:"cables_added"`
CablesKept []int64 `json:"cables_kept"`
CablesRemoved []int64 `json:"cables_removed"`
BundlesAdded []Bundle `json:"bundles_added"`
BundlesRemoved []int64 `json:"bundles_removed"`
Unsatisfied []UnsatisfiedReq `json:"unsatisfied"`
Warnings []string `json:"warnings"`
}
type UnsatisfiedReq struct {
RequirementID int64 `json:"requirement_id"`
Reason string `json:"reason"`
WhichSide string `json:"which_side,omitempty"` // "from" | "to" | "" when both/neither
CableType string `json:"cable_type,omitempty"` // when known
}
// ApplyTemplateResult is the response from POST /apply-template.
type ApplyTemplateResult struct {
FramesAdded []Frame `json:"frames_added"`
DevicesAdded []Device `json:"devices_added"`
RequirementsAdded []ConnectionRequirement `json:"requirements_added"`
SkippedDevices []SkippedTemplateDevice `json:"skipped_devices"`
RequirementsSkipped []SkippedTemplateReq `json:"requirements_skipped"`
}
type SkippedTemplateDevice struct {
TemplateDeviceID int64 `json:"template_device_id"`
Reason string `json:"reason"`
}
type SkippedTemplateReq struct {
TemplateRequirementID int64 `json:"template_requirement_id"`
Reason string `json:"reason"`
}
// Snapshot is the editor's one-shot loader payload for a single project.
// Arrays for collections still gated by future slices stay non-nil [] so
// JSON encodes as [] not null.
type Snapshot struct {
Project Project `json:"project"`
Frames []Frame `json:"frames"`
Devices []Device `json:"devices"`
Ports []Port `json:"ports"`
Cables []Cable `json:"cables"`
IOMarkers []IOMarker `json:"io_markers"`
Bundles []Bundle `json:"bundles"`
CableTypes []CableType `json:"cable_types"`
ConnectionRequirements []ConnectionRequirement `json:"connection_requirements"`
Clamps []Clamp `json:"clamps"`
CableClamps []CableClamp `json:"cable_clamps"`
}
// Clamp is a routing anchor on the canvas. Cables route through clamps
// in `ord` sequence (see cable_clamps), giving m a physical handle on
// where bundles converge.
type Clamp struct {
ID int64 `json:"id"`
ProjectID int64 `json:"project_id"`
X float64 `json:"x"`
Y float64 `json:"y"`
Label string `json:"label"`
FrameID *int64 `json:"frame_id"`
ExcalidrawID *string `json:"excalidraw_id,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// CableClamp is one (cable, clamp, ord) row. Ord is 1-based along the
// cable's from→to direction.
type CableClamp struct {
CableID int64 `json:"cable_id"`
ClampID int64 `json:"clamp_id"`
Ord int `json:"ord"`
}