feat(db): connection_requirements + cables.auto
Migration 003 adds the solver's per-project input table + the auto flag
that slice 6 will use to distinguish solver-owned cables from m's
hand-drawn ones.
connection_requirements:
- (from_device_id, to_device_id, preferred_cable_type_id) with
preferred_cable_type_id nullable ("solver picks if exactly one type
matches both ends").
- (pair_lo, pair_hi) is the order-normalised MIN/MAX of (from, to),
stored alongside the m-facing from/to so the UI doesn't have to
denormalise.
- UNIQUE (project_id, pair_lo, pair_hi, preferred_cable_type_id) →
(A,B,T) and (B,A,T) collide; (A,B,Power) + (A,B,RJ45) coexist.
- CHECK (from != to). FK CASCADE from devices → requirement vanishes
if either endpoint device is deleted.
Store + 11 new tests:
- pair normalisation rejects the reversed-direction duplicate
- different cable types on the same pair coexist
- self-loop rejected (ErrInvalidInput)
- cross-project device reference rejected
- two null-cable-type reqs on the same pair both succeed (SQLite NULL
!= NULL in UNIQUE — semantically "solver picks both times", second
wins)
- partial PATCH: preferred_cable_type_id tri-state (leave/set/clear),
must_connect bool, notes string
- device delete cascades to its requirements
- snapshot.connection_requirements is non-nil and populated
cables.auto:
- ALTER TABLE cables ADD COLUMN auto INTEGER NOT NULL DEFAULT 0 CHECK
(auto IN (0,1)). Slice 6 sets 1 from the solver; slice 7's manual
cable POST keeps the default 0.
This commit is contained in:
192
internal/db/connection_requirements.go
Normal file
192
internal/db/connection_requirements.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ConnectionRequirementCreate is the create-shape. Server normalises
|
||||
// from/to into (pair_lo, pair_hi) so (A,B,T) and (B,A,T) collide.
|
||||
type ConnectionRequirementCreate struct {
|
||||
FromDeviceID int64
|
||||
ToDeviceID int64
|
||||
PreferredCableTypeID *int64
|
||||
MustConnect *bool // pointer so "absent" defaults to true
|
||||
Notes string
|
||||
}
|
||||
|
||||
// ConnectionRequirementUpdate is the partial-update shape. project_id +
|
||||
// the device pair are immutable post-create (changing either is best
|
||||
// modelled as delete-then-create — keeps pair_lo/pair_hi semantics simple).
|
||||
type ConnectionRequirementUpdate struct {
|
||||
PreferredCableTypeID FrameRef // tri-state: leave / set / clear
|
||||
MustConnect *bool
|
||||
Notes *string
|
||||
}
|
||||
|
||||
// CreateConnectionRequirement inserts a new requirement. Validates that
|
||||
// both devices live in projectID, that from != to, and that the
|
||||
// (project, pair_lo, pair_hi, preferred_cable_type_id) tuple is unique.
|
||||
func (s *Store) CreateConnectionRequirement(projectID int64, r ConnectionRequirementCreate) (*ConnectionRequirement, error) {
|
||||
if r.FromDeviceID == r.ToDeviceID {
|
||||
return nil, fmt.Errorf("%w: from_device_id and to_device_id must differ", ErrInvalidInput)
|
||||
}
|
||||
if _, err := s.GetProject(projectID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := s.GetDevice(projectID, r.FromDeviceID); err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return nil, fmt.Errorf("%w: from_device_id %d not in project %d", ErrInvalidInput, r.FromDeviceID, projectID)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if _, err := s.GetDevice(projectID, r.ToDeviceID); err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return nil, fmt.Errorf("%w: to_device_id %d not in project %d", ErrInvalidInput, r.ToDeviceID, projectID)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if r.PreferredCableTypeID != nil {
|
||||
if _, err := s.GetCableType(*r.PreferredCableTypeID); err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return nil, fmt.Errorf("%w: preferred_cable_type_id %d not found", ErrInvalidInput, *r.PreferredCableTypeID)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
must := true
|
||||
if r.MustConnect != nil {
|
||||
must = *r.MustConnect
|
||||
}
|
||||
mustInt := 0
|
||||
if must {
|
||||
mustInt = 1
|
||||
}
|
||||
lo, hi := r.FromDeviceID, r.ToDeviceID
|
||||
if lo > hi {
|
||||
lo, hi = hi, lo
|
||||
}
|
||||
res, err := s.db.Exec(
|
||||
`INSERT INTO connection_requirements
|
||||
(project_id, from_device_id, to_device_id, preferred_cable_type_id,
|
||||
must_connect, notes, pair_lo, pair_hi)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
projectID, r.FromDeviceID, r.ToDeviceID, nullableInt64(r.PreferredCableTypeID),
|
||||
mustInt, r.Notes, lo, hi,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, mapWriteErr(err)
|
||||
}
|
||||
id, _ := res.LastInsertId()
|
||||
return s.GetConnectionRequirement(projectID, id)
|
||||
}
|
||||
|
||||
// GetConnectionRequirement loads one by id, project-scoped.
|
||||
func (s *Store) GetConnectionRequirement(projectID, id int64) (*ConnectionRequirement, error) {
|
||||
var r ConnectionRequirement
|
||||
var ct sql.NullInt64
|
||||
var must int
|
||||
err := s.db.QueryRow(
|
||||
`SELECT id, project_id, from_device_id, to_device_id, preferred_cable_type_id,
|
||||
must_connect, notes, created_at, updated_at
|
||||
FROM connection_requirements WHERE id = ? AND project_id = ?`, id, projectID,
|
||||
).Scan(&r.ID, &r.ProjectID, &r.FromDeviceID, &r.ToDeviceID, &ct,
|
||||
&must, &r.Notes, &r.CreatedAt, &r.UpdatedAt)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ct.Valid {
|
||||
v := ct.Int64
|
||||
r.PreferredCableTypeID = &v
|
||||
}
|
||||
r.MustConnect = must != 0
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
// ListConnectionRequirements returns every requirement in a project,
|
||||
// ordered by id (insertion order).
|
||||
func (s *Store) ListConnectionRequirements(projectID int64) ([]ConnectionRequirement, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT id, project_id, from_device_id, to_device_id, preferred_cable_type_id,
|
||||
must_connect, notes, created_at, updated_at
|
||||
FROM connection_requirements WHERE project_id = ? ORDER BY id`, projectID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := []ConnectionRequirement{}
|
||||
for rows.Next() {
|
||||
var r ConnectionRequirement
|
||||
var ct sql.NullInt64
|
||||
var must int
|
||||
if err := rows.Scan(&r.ID, &r.ProjectID, &r.FromDeviceID, &r.ToDeviceID, &ct,
|
||||
&must, &r.Notes, &r.CreatedAt, &r.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ct.Valid {
|
||||
v := ct.Int64
|
||||
r.PreferredCableTypeID = &v
|
||||
}
|
||||
r.MustConnect = must != 0
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// UpdateConnectionRequirement applies a partial update. preferred_cable_type_id
|
||||
// uses the FrameRef tri-state; must_connect + notes are plain pointers.
|
||||
// The (from, to) pair is immutable on PATCH — delete + recreate to change.
|
||||
func (s *Store) UpdateConnectionRequirement(projectID, id int64, u ConnectionRequirementUpdate) (*ConnectionRequirement, error) {
|
||||
cur, err := s.GetConnectionRequirement(projectID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.PreferredCableTypeID.Set {
|
||||
if u.PreferredCableTypeID.ID != nil {
|
||||
if _, err := s.GetCableType(*u.PreferredCableTypeID.ID); err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return nil, fmt.Errorf("%w: preferred_cable_type_id %d not found", ErrInvalidInput, *u.PreferredCableTypeID.ID)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
cur.PreferredCableTypeID = u.PreferredCableTypeID.ID
|
||||
}
|
||||
if u.MustConnect != nil {
|
||||
cur.MustConnect = *u.MustConnect
|
||||
}
|
||||
if u.Notes != nil {
|
||||
cur.Notes = *u.Notes
|
||||
}
|
||||
mustInt := 0
|
||||
if cur.MustConnect {
|
||||
mustInt = 1
|
||||
}
|
||||
if _, err := s.db.Exec(
|
||||
`UPDATE connection_requirements
|
||||
SET preferred_cable_type_id = ?, must_connect = ?, notes = ?, updated_at = datetime('now')
|
||||
WHERE id = ? AND project_id = ?`,
|
||||
nullableInt64(cur.PreferredCableTypeID), mustInt, cur.Notes, id, projectID,
|
||||
); err != nil {
|
||||
return nil, mapWriteErr(err)
|
||||
}
|
||||
return s.GetConnectionRequirement(projectID, id)
|
||||
}
|
||||
|
||||
// DeleteConnectionRequirement removes a requirement by id, project-scoped.
|
||||
func (s *Store) DeleteConnectionRequirement(projectID, id int64) error {
|
||||
if _, err := s.GetConnectionRequirement(projectID, id); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := s.db.Exec(
|
||||
`DELETE FROM connection_requirements WHERE id = ? AND project_id = ?`, id, projectID,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
181
internal/db/connection_requirements_test.go
Normal file
181
internal/db/connection_requirements_test.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func setupTwoDevices(t *testing.T, s *Store) (int64, int64, int64) {
|
||||
t.Helper()
|
||||
p, _ := s.CreateProject("LOFT", "", "")
|
||||
a, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "NAS", X: 0, Y: 0, Width: 100, Height: 35})
|
||||
b, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "Switch", X: 200, Y: 0, Width: 100, Height: 35})
|
||||
return p.ID, a.ID, b.ID
|
||||
}
|
||||
|
||||
func TestCreateConnReq_Basic(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
pid, a, b := setupTwoDevices(t, s)
|
||||
rj45 := int64(5)
|
||||
r, err := s.CreateConnectionRequirement(pid, ConnectionRequirementCreate{
|
||||
FromDeviceID: a, ToDeviceID: b, PreferredCableTypeID: &rj45,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
if !r.MustConnect {
|
||||
t.Errorf("must_connect default should be true")
|
||||
}
|
||||
if r.PreferredCableTypeID == nil || *r.PreferredCableTypeID != rj45 {
|
||||
t.Errorf("preferred_cable_type_id wrong: %+v", r.PreferredCableTypeID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateConnReq_PairNormalisationRejectsReverse(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
pid, a, b := setupTwoDevices(t, s)
|
||||
rj45 := int64(5)
|
||||
if _, err := s.CreateConnectionRequirement(pid, ConnectionRequirementCreate{
|
||||
FromDeviceID: a, ToDeviceID: b, PreferredCableTypeID: &rj45,
|
||||
}); err != nil {
|
||||
t.Fatalf("first: %v", err)
|
||||
}
|
||||
// (B, A, RJ45) should collide on UNIQUE (pair_lo, pair_hi, type).
|
||||
_, err := s.CreateConnectionRequirement(pid, ConnectionRequirementCreate{
|
||||
FromDeviceID: b, ToDeviceID: a, PreferredCableTypeID: &rj45,
|
||||
})
|
||||
if !errors.Is(err, ErrConflict) {
|
||||
t.Errorf("reverse pair err = %v, want ErrConflict", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateConnReq_DifferentCableTypesCoexist(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
pid, a, b := setupTwoDevices(t, s)
|
||||
rj45, power := int64(5), int64(1)
|
||||
if _, err := s.CreateConnectionRequirement(pid, ConnectionRequirementCreate{
|
||||
FromDeviceID: a, ToDeviceID: b, PreferredCableTypeID: &rj45,
|
||||
}); err != nil {
|
||||
t.Fatalf("rj45: %v", err)
|
||||
}
|
||||
if _, err := s.CreateConnectionRequirement(pid, ConnectionRequirementCreate{
|
||||
FromDeviceID: a, ToDeviceID: b, PreferredCableTypeID: &power,
|
||||
}); err != nil {
|
||||
t.Errorf("power on same pair should be allowed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateConnReq_SelfLoopRejected(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
pid, a, _ := setupTwoDevices(t, s)
|
||||
_, err := s.CreateConnectionRequirement(pid, ConnectionRequirementCreate{
|
||||
FromDeviceID: a, ToDeviceID: a,
|
||||
})
|
||||
if !errors.Is(err, ErrInvalidInput) {
|
||||
t.Errorf("self-loop err = %v, want ErrInvalidInput", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateConnReq_CrossProjectDeviceRejected(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
pid, a, _ := setupTwoDevices(t, s)
|
||||
p2, _ := s.CreateProject("OFFICE", "", "")
|
||||
b2, _ := s.CreateDevice(p2.ID, DeviceCreate{Name: "X", X: 0, Y: 0, Width: 100, Height: 35})
|
||||
_, err := s.CreateConnectionRequirement(pid, ConnectionRequirementCreate{
|
||||
FromDeviceID: a, ToDeviceID: b2.ID,
|
||||
})
|
||||
if !errors.Is(err, ErrInvalidInput) {
|
||||
t.Errorf("cross-project to-device err = %v, want ErrInvalidInput", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateConnReq_NullCableTypeUniqueByPair(t *testing.T) {
|
||||
// Two NULL-cable-type reqs on the same pair are NOT a conflict in
|
||||
// SQLite (NULL != NULL in UNIQUE comparisons). This is fine — they
|
||||
// represent "solver picks" both times; the second wins when solving.
|
||||
s := newTestStore(t)
|
||||
pid, a, b := setupTwoDevices(t, s)
|
||||
if _, err := s.CreateConnectionRequirement(pid, ConnectionRequirementCreate{
|
||||
FromDeviceID: a, ToDeviceID: b,
|
||||
}); err != nil {
|
||||
t.Fatalf("first: %v", err)
|
||||
}
|
||||
if _, err := s.CreateConnectionRequirement(pid, ConnectionRequirementCreate{
|
||||
FromDeviceID: a, ToDeviceID: b,
|
||||
}); err != nil {
|
||||
t.Errorf("second NULL-type req should be allowed (SQLite NULL != NULL): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateConnReq_PartialFields(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
pid, a, b := setupTwoDevices(t, s)
|
||||
rj45, power := int64(5), int64(1)
|
||||
r, _ := s.CreateConnectionRequirement(pid, ConnectionRequirementCreate{
|
||||
FromDeviceID: a, ToDeviceID: b, PreferredCableTypeID: &rj45,
|
||||
})
|
||||
notes := "important"
|
||||
must := false
|
||||
updated, err := s.UpdateConnectionRequirement(pid, r.ID, ConnectionRequirementUpdate{
|
||||
PreferredCableTypeID: FrameRef{Set: true, ID: &power},
|
||||
MustConnect: &must,
|
||||
Notes: ¬es,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("update: %v", err)
|
||||
}
|
||||
if updated.PreferredCableTypeID == nil || *updated.PreferredCableTypeID != power {
|
||||
t.Errorf("cable type not switched: %+v", updated.PreferredCableTypeID)
|
||||
}
|
||||
if updated.MustConnect {
|
||||
t.Errorf("must_connect should be false")
|
||||
}
|
||||
if updated.Notes != "important" {
|
||||
t.Errorf("notes = %q", updated.Notes)
|
||||
}
|
||||
|
||||
// Clear the cable type.
|
||||
cleared, _ := s.UpdateConnectionRequirement(pid, r.ID, ConnectionRequirementUpdate{
|
||||
PreferredCableTypeID: FrameRef{Set: true, ID: nil},
|
||||
})
|
||||
if cleared.PreferredCableTypeID != nil {
|
||||
t.Errorf("preferred_cable_type_id should be nil after clear; got %v", *cleared.PreferredCableTypeID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteConnReq_CascadesOnDeviceDelete(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
pid, a, b := setupTwoDevices(t, s)
|
||||
r, _ := s.CreateConnectionRequirement(pid, ConnectionRequirementCreate{
|
||||
FromDeviceID: a, ToDeviceID: b,
|
||||
})
|
||||
if err := s.DeleteDevice(pid, a); err != nil {
|
||||
t.Fatalf("delete device a: %v", err)
|
||||
}
|
||||
if _, err := s.GetConnectionRequirement(pid, r.ID); !errors.Is(err, ErrNotFound) {
|
||||
t.Errorf("requirement should be gone after device delete; got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnapshot_IncludesConnectionRequirements(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
pid, a, b := setupTwoDevices(t, s)
|
||||
_, _ = s.CreateConnectionRequirement(pid, ConnectionRequirementCreate{
|
||||
FromDeviceID: a, ToDeviceID: b,
|
||||
})
|
||||
snap, err := s.Snapshot(pid)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot: %v", err)
|
||||
}
|
||||
if len(snap.ConnectionRequirements) != 1 {
|
||||
t.Errorf("snapshot.connection_requirements = %d, want 1", len(snap.ConnectionRequirements))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteConnReq_NotFound(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
p, _ := s.CreateProject("LOFT", "", "")
|
||||
if err := s.DeleteConnectionRequirement(p.ID, 999); !errors.Is(err, ErrNotFound) {
|
||||
t.Errorf("got %v, want ErrNotFound", err)
|
||||
}
|
||||
}
|
||||
34
internal/db/migrations/003_connection_requirements.sql
Normal file
34
internal/db/migrations/003_connection_requirements.sql
Normal file
@@ -0,0 +1,34 @@
|
||||
-- mCables v4.1 connection requirements + solver-owned cable flag.
|
||||
-- See docs/design.md §2.1 + §2 connection_requirements + §5b.3.
|
||||
|
||||
-- The solver's input: "device A must connect to device B via cable type T".
|
||||
-- Many per device. (from, to) is normalised on insert as
|
||||
-- (pair_lo, pair_hi) = (MIN(from, to), MAX(from, to)) so (A,B,T) and (B,A,T)
|
||||
-- can't coexist (UNIQUE enforces it).
|
||||
CREATE TABLE connection_requirements (
|
||||
id INTEGER PRIMARY KEY,
|
||||
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
from_device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
||||
to_device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
|
||||
preferred_cable_type_id INTEGER REFERENCES cable_types(id) ON DELETE SET NULL,
|
||||
must_connect INTEGER NOT NULL DEFAULT 1 CHECK (must_connect IN (0, 1)),
|
||||
notes TEXT NOT NULL DEFAULT '',
|
||||
pair_lo INTEGER NOT NULL,
|
||||
pair_hi INTEGER NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
CHECK (from_device_id != to_device_id),
|
||||
UNIQUE (project_id, pair_lo, pair_hi, preferred_cable_type_id)
|
||||
);
|
||||
CREATE INDEX conn_reqs_project_idx ON connection_requirements(project_id);
|
||||
CREATE INDEX conn_reqs_pair_idx ON connection_requirements(project_id, pair_lo, pair_hi);
|
||||
CREATE INDEX conn_reqs_from_idx ON connection_requirements(from_device_id);
|
||||
CREATE INDEX conn_reqs_to_idx ON connection_requirements(to_device_id);
|
||||
|
||||
-- Solver-owned cable flag (§5b.3): 1 = the solver placed this cable,
|
||||
-- replaceable on re-solve. 0 = m hand-drew it, left alone by the solver.
|
||||
-- Slice 6 ships the solver that writes auto=1; slice 7 ships hand-drawn
|
||||
-- cable creation that writes auto=0.
|
||||
ALTER TABLE cables ADD COLUMN auto INTEGER NOT NULL DEFAULT 0
|
||||
CHECK (auto IN (0, 1));
|
||||
CREATE INDEX cables_auto_idx ON cables(auto);
|
||||
@@ -95,16 +95,33 @@ type Port struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
// 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 []any `json:"cables"`
|
||||
IOMarkers []IOMarker `json:"io_markers"`
|
||||
Bundles []any `json:"bundles"`
|
||||
CableTypes []CableType `json:"cable_types"`
|
||||
Project Project `json:"project"`
|
||||
Frames []Frame `json:"frames"`
|
||||
Devices []Device `json:"devices"`
|
||||
Ports []Port `json:"ports"`
|
||||
Cables []any `json:"cables"`
|
||||
IOMarkers []IOMarker `json:"io_markers"`
|
||||
Bundles []any `json:"bundles"`
|
||||
CableTypes []CableType `json:"cable_types"`
|
||||
ConnectionRequirements []ConnectionRequirement `json:"connection_requirements"`
|
||||
}
|
||||
|
||||
@@ -175,15 +175,20 @@ func (s *Store) Snapshot(id int64) (*Snapshot, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqs, err := s.ListConnectionRequirements(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Snapshot{
|
||||
Project: *p,
|
||||
Frames: frames,
|
||||
Devices: devices,
|
||||
Ports: ports,
|
||||
Cables: []any{},
|
||||
IOMarkers: ios,
|
||||
Bundles: []any{},
|
||||
CableTypes: types,
|
||||
Project: *p,
|
||||
Frames: frames,
|
||||
Devices: devices,
|
||||
Ports: ports,
|
||||
Cables: []any{},
|
||||
IOMarkers: ios,
|
||||
Bundles: []any{},
|
||||
CableTypes: types,
|
||||
ConnectionRequirements: reqs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user