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.
249 lines
9.8 KiB
Go
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"`
|
|
}
|