Skip to content

Commit 3b671cc

Browse files
committed
refactor(store): wave B.18 — assignment repo (#279)
AssignmentRepo closes the action-chain migration (Action → ActionSet → Definition → Execution → Assignment). Read side of assignments_projection: Get / GetByID / List(filter) / Count(filter) / ListAvailableForDevice / ListAssignedUserIDsForDevice List returns AssignmentWithNames (joined source_name + target_name) for the list UI. ListAvailableForDevice serves the user-selection catalog; ListAssignedUserIDsForDevice is the narrow string-list used by device_handler. Non-goals (join-row shapes — defer): - ListAssignedActionsForDevice (returns action data — lives on the action-domain seam) - ListDirectAssignmentsForDevice / ListGroupAssignmentsForDevice (join-row shapes) - ListAssignmentsForUser (different row shape — transitional inline conversion at the one call site for now) Call sites migrated: - assignment_handler.go (Create/Delete/Get/List/Count + assignmentToProto signature) - user_selection_handler.go (ListAvailableAssignmentsForDevice ×2) - device_handler.go (ListDeviceAssignedUserIDs) Closes #279.
1 parent 9b7d876 commit 3b671cc

7 files changed

Lines changed: 246 additions & 33 deletions

File tree

internal/api/assignment_handler.go

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"github.com/manchtools/power-manage/server/internal/eventtypes"
1414
"github.com/manchtools/power-manage/server/internal/eventtypes/payloads"
1515
"github.com/manchtools/power-manage/server/internal/store"
16-
db "github.com/manchtools/power-manage/server/internal/store/generated"
1716
)
1817

1918
// AssignmentHandler handles assignment RPCs.
@@ -123,12 +122,7 @@ func (h *AssignmentHandler) CreateAssignment(ctx context.Context, req *connect.R
123122
targetTypeStr := assignmentTargetTypeToString(req.Msg.TargetType)
124123

125124
// Check if an active assignment already exists
126-
existingAssignment, err := h.store.Queries().GetAssignment(ctx, db.GetAssignmentParams{
127-
SourceType: sourceTypeStr,
128-
SourceID: req.Msg.SourceId,
129-
TargetType: targetTypeStr,
130-
TargetID: req.Msg.TargetId,
131-
})
125+
existingAssignment, err := h.store.Repos().Assignment.Get(ctx, store.AssignmentKey{SourceType: sourceTypeStr, SourceID: req.Msg.SourceId, TargetType: targetTypeStr, TargetID: req.Msg.TargetId})
132126
if err == nil {
133127
// Assignment already exists, return it
134128
return connect.NewResponse(&pm.CreateAssignmentResponse{
@@ -159,12 +153,7 @@ func (h *AssignmentHandler) CreateAssignment(ctx context.Context, req *connect.R
159153

160154
// Use GetAssignment instead of GetAssignmentByID because the upsert
161155
// may have updated an existing soft-deleted record with a different ID
162-
assignment, err := h.store.Queries().GetAssignment(ctx, db.GetAssignmentParams{
163-
SourceType: sourceTypeStr,
164-
SourceID: req.Msg.SourceId,
165-
TargetType: targetTypeStr,
166-
TargetID: req.Msg.TargetId,
167-
})
156+
assignment, err := h.store.Repos().Assignment.Get(ctx, store.AssignmentKey{SourceType: sourceTypeStr, SourceID: req.Msg.SourceId, TargetType: targetTypeStr, TargetID: req.Msg.TargetId})
168157
if err != nil {
169158
return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal, "failed to get assignment")
170159
}
@@ -186,7 +175,7 @@ func (h *AssignmentHandler) DeleteAssignment(ctx context.Context, req *connect.R
186175
}
187176

188177
// Verify assignment exists before emitting delete event
189-
_, err = h.store.Queries().GetAssignmentByID(ctx, req.Msg.Id)
178+
_, err = h.store.Repos().Assignment.GetByID(ctx, req.Msg.Id)
190179
if err != nil {
191180
return nil, handleGetError(ctx, err, ErrAssignmentNotFound, "assignment not found")
192181
}
@@ -218,24 +207,12 @@ func (h *AssignmentHandler) ListAssignments(ctx context.Context, req *connect.Re
218207
sourceTypeStr := assignmentSourceTypeToString(req.Msg.SourceType)
219208
targetTypeStr := assignmentTargetTypeToString(req.Msg.TargetType)
220209

221-
assignments, err := h.store.Queries().ListAssignments(ctx, db.ListAssignmentsParams{
222-
Column1: sourceTypeStr,
223-
Column2: req.Msg.SourceId,
224-
Column3: targetTypeStr,
225-
Column4: req.Msg.TargetId,
226-
Limit: pageSize,
227-
Offset: offset,
228-
})
210+
assignments, err := h.store.Repos().Assignment.List(ctx, store.ListAssignmentsFilter{SourceType: sourceTypeStr, SourceID: req.Msg.SourceId, TargetType: targetTypeStr, TargetID: req.Msg.TargetId, Limit: pageSize, Offset: offset})
229211
if err != nil {
230212
return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal, "failed to list assignments")
231213
}
232214

233-
count, err := h.store.Queries().CountAssignments(ctx, db.CountAssignmentsParams{
234-
Column1: sourceTypeStr,
235-
Column2: req.Msg.SourceId,
236-
Column3: targetTypeStr,
237-
Column4: req.Msg.TargetId,
238-
})
215+
count, err := h.store.Repos().Assignment.Count(ctx, store.CountAssignmentsFilter{SourceType: sourceTypeStr, SourceID: req.Msg.SourceId, TargetType: targetTypeStr, TargetID: req.Msg.TargetId})
239216
if err != nil {
240217
return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal, "failed to count assignments")
241218
}
@@ -459,15 +436,28 @@ func (h *AssignmentHandler) GetUserAssignments(ctx context.Context, req *connect
459436

460437
protoAssignments := make([]*pm.Assignment, len(assignments))
461438
for i, a := range assignments {
462-
protoAssignments[i] = h.assignmentToProto(a)
439+
// Transitional: ListAssignmentsForUser hasn't migrated yet
440+
// (different row shape), so convert to store.Assignment for
441+
// assignmentToProto. Falls away when that query moves.
442+
protoAssignments[i] = h.assignmentToProto(store.Assignment{
443+
ID: a.ID,
444+
SourceType: a.SourceType,
445+
SourceID: a.SourceID,
446+
TargetType: a.TargetType,
447+
TargetID: a.TargetID,
448+
SortOrder: a.SortOrder,
449+
Mode: a.Mode,
450+
CreatedAt: a.CreatedAt,
451+
CreatedBy: a.CreatedBy,
452+
})
463453
}
464454

465455
return connect.NewResponse(&pm.GetUserAssignmentsResponse{
466456
Assignments: protoAssignments,
467457
}), nil
468458
}
469459

470-
func (h *AssignmentHandler) assignmentToProto(a db.AssignmentsProjection) *pm.Assignment {
460+
func (h *AssignmentHandler) assignmentToProto(a store.Assignment) *pm.Assignment {
471461
assignment := &pm.Assignment{
472462
Id: a.ID,
473463
SourceType: assignmentSourceTypeFromString(a.SourceType),

internal/api/device_handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -819,7 +819,7 @@ func (h *DeviceHandler) CreateLuksToken(ctx context.Context, req *connect.Reques
819819
}
820820

821821
// Only an assigned owner can create a LUKS token
822-
assignedUserIDs, err := h.store.Queries().ListDeviceAssignedUserIDs(ctx, req.Msg.DeviceId)
822+
assignedUserIDs, err := h.store.Repos().Assignment.ListAssignedUserIDsForDevice(ctx, req.Msg.DeviceId)
823823
if err != nil {
824824
return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal, "failed to check device assignments")
825825
}

internal/api/user_selection_handler.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func (h *UserSelectionHandler) SetUserSelection(ctx context.Context, req *connec
5454
sourceTypeStr := assignmentSourceTypeToString(req.Msg.SourceType)
5555

5656
// Verify an available-mode assignment exists for this source targeting this device
57-
availableAssignments, err := h.store.Queries().ListAvailableAssignmentsForDevice(ctx, req.Msg.DeviceId)
57+
availableAssignments, err := h.store.Repos().Assignment.ListAvailableForDevice(ctx, req.Msg.DeviceId)
5858
if err != nil {
5959
return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal, "failed to check available assignments")
6060
}
@@ -118,7 +118,7 @@ func (h *UserSelectionHandler) ListAvailableActions(ctx context.Context, req *co
118118
}
119119

120120
// Get available assignments for this device
121-
assignments, err := h.store.Queries().ListAvailableAssignmentsForDevice(ctx, req.Msg.DeviceId)
121+
assignments, err := h.store.Repos().Assignment.ListAvailableForDevice(ctx, req.Msg.DeviceId)
122122
if err != nil {
123123
return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal, "failed to list available assignments")
124124
}

internal/store/assignment.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package store
2+
3+
import (
4+
"context"
5+
"time"
6+
)
7+
8+
// Assignment is the basic assignment-projection row. Mode is the
9+
// pm.AssignmentMode enum value (Required / Available); SortOrder
10+
// drives the dispatch ordering on the agent.
11+
type Assignment struct {
12+
ID string
13+
SourceType string
14+
SourceID string
15+
TargetType string
16+
TargetID string
17+
SortOrder int32
18+
Mode int32
19+
CreatedAt *time.Time
20+
CreatedBy string
21+
}
22+
23+
// AssignmentWithNames is the List-side shape — wraps Assignment with
24+
// the joined SourceName + TargetName so the list UI can render without
25+
// per-row lookups for display.
26+
type AssignmentWithNames struct {
27+
Assignment
28+
SourceName string
29+
TargetName string
30+
}
31+
32+
// AssignmentKey is the composite (source, target) lookup used by
33+
// GetAssignment and the duplicate-pre-check path. The projection's
34+
// unique index on this tuple guarantees at most one active row.
35+
type AssignmentKey struct {
36+
SourceType string
37+
SourceID string
38+
TargetType string
39+
TargetID string
40+
}
41+
42+
// ListAssignmentsFilter pairs pagination with the four optional
43+
// filter axes the UI exposes. Empty strings disable each axis
44+
// independently — the projection treats "" as "no filter".
45+
type ListAssignmentsFilter struct {
46+
SourceType string
47+
SourceID string
48+
TargetType string
49+
TargetID string
50+
Limit int32
51+
Offset int32
52+
}
53+
54+
// CountAssignmentsFilter mirrors ListAssignmentsFilter's filter
55+
// fields for the matching count side.
56+
type CountAssignmentsFilter struct {
57+
SourceType string
58+
SourceID string
59+
TargetType string
60+
TargetID string
61+
}
62+
63+
// AssignmentRepo reads assignment state. Writes flow through events
64+
// (AssignmentCreated / AssignmentDeleted / AssignmentModeChanged /
65+
// AssignmentSortOrderChanged) and the projector listener.
66+
//
67+
// Per-device join queries that hydrate action / source-type details
68+
// (ListAssignedActionsForDevice, ListDirectAssignmentsForDevice,
69+
// ListGroupAssignmentsForDevice, ListAssignmentsForUser) stay on
70+
// Store.Queries() — they return join-shaped rows that migrate with
71+
// their respective domain consumers.
72+
type AssignmentRepo interface {
73+
// Get returns the assignment for a (source, target) tuple.
74+
// Returns ErrNotFound when no active assignment matches.
75+
Get(ctx context.Context, key AssignmentKey) (Assignment, error)
76+
77+
// GetByID returns the assignment by its ID.
78+
GetByID(ctx context.Context, id string) (Assignment, error)
79+
80+
// List returns a page of assignments hydrated with source +
81+
// target display names, ordered by created_at descending.
82+
List(ctx context.Context, filter ListAssignmentsFilter) ([]AssignmentWithNames, error)
83+
84+
// Count returns the total matching the filter.
85+
Count(ctx context.Context, filter CountAssignmentsFilter) (int64, error)
86+
87+
// ListAvailableForDevice returns every "Available"-mode
88+
// assignment whose effective target reaches the given device
89+
// (directly, via device-group, via user assigned to the device,
90+
// or via that user's user-groups). Used to build the per-device
91+
// "what can the user opt into" catalog.
92+
ListAvailableForDevice(ctx context.Context, deviceID string) ([]Assignment, error)
93+
94+
// ListAssignedUserIDsForDevice returns the user IDs with at
95+
// least one assignment that resolves to the given device.
96+
// Returns an empty slice when the device has no user
97+
// assignments.
98+
ListAssignedUserIDsForDevice(ctx context.Context, deviceID string) ([]string, error)
99+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package postgres
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/manchtools/power-manage/server/internal/store"
8+
"github.com/manchtools/power-manage/server/internal/store/generated"
9+
)
10+
11+
// Assignment implements store.AssignmentRepo against
12+
// assignments_projection.
13+
type Assignment struct {
14+
q *generated.Queries
15+
}
16+
17+
// NewAssignment returns an Assignment repo bound to the given sqlc
18+
// handle.
19+
func NewAssignment(q *generated.Queries) *Assignment {
20+
return &Assignment{q: q}
21+
}
22+
23+
func (a *Assignment) Get(ctx context.Context, key store.AssignmentKey) (store.Assignment, error) {
24+
row, err := a.q.GetAssignment(ctx, generated.GetAssignmentParams{
25+
SourceType: key.SourceType,
26+
SourceID: key.SourceID,
27+
TargetType: key.TargetType,
28+
TargetID: key.TargetID,
29+
})
30+
if err != nil {
31+
return store.Assignment{}, fmt.Errorf("assignment: get: %w", translateNotFound(err))
32+
}
33+
return assignmentFromRow(row), nil
34+
}
35+
36+
func (a *Assignment) GetByID(ctx context.Context, id string) (store.Assignment, error) {
37+
row, err := a.q.GetAssignmentByID(ctx, id)
38+
if err != nil {
39+
return store.Assignment{}, fmt.Errorf("assignment: get by id: %w", translateNotFound(err))
40+
}
41+
return assignmentFromRow(row), nil
42+
}
43+
44+
func (a *Assignment) List(ctx context.Context, filter store.ListAssignmentsFilter) ([]store.AssignmentWithNames, error) {
45+
rows, err := a.q.ListAssignments(ctx, generated.ListAssignmentsParams{
46+
Column1: filter.SourceType,
47+
Column2: filter.SourceID,
48+
Column3: filter.TargetType,
49+
Column4: filter.TargetID,
50+
Limit: filter.Limit,
51+
Offset: filter.Offset,
52+
})
53+
if err != nil {
54+
return nil, fmt.Errorf("assignment: list: %w", err)
55+
}
56+
out := make([]store.AssignmentWithNames, len(rows))
57+
for i, r := range rows {
58+
out[i] = store.AssignmentWithNames{
59+
Assignment: store.Assignment{
60+
ID: r.ID,
61+
SourceType: r.SourceType,
62+
SourceID: r.SourceID,
63+
TargetType: r.TargetType,
64+
TargetID: r.TargetID,
65+
SortOrder: r.SortOrder,
66+
Mode: r.Mode,
67+
CreatedAt: r.CreatedAt,
68+
CreatedBy: r.CreatedBy,
69+
},
70+
SourceName: r.SourceName,
71+
TargetName: r.TargetName,
72+
}
73+
}
74+
return out, nil
75+
}
76+
77+
func (a *Assignment) Count(ctx context.Context, filter store.CountAssignmentsFilter) (int64, error) {
78+
n, err := a.q.CountAssignments(ctx, generated.CountAssignmentsParams{
79+
Column1: filter.SourceType,
80+
Column2: filter.SourceID,
81+
Column3: filter.TargetType,
82+
Column4: filter.TargetID,
83+
})
84+
if err != nil {
85+
return 0, fmt.Errorf("assignment: count: %w", translateNotFound(err))
86+
}
87+
return n, nil
88+
}
89+
90+
func (a *Assignment) ListAvailableForDevice(ctx context.Context, deviceID string) ([]store.Assignment, error) {
91+
rows, err := a.q.ListAvailableAssignmentsForDevice(ctx, deviceID)
92+
if err != nil {
93+
return nil, fmt.Errorf("assignment: list available for device: %w", err)
94+
}
95+
out := make([]store.Assignment, len(rows))
96+
for i, r := range rows {
97+
out[i] = assignmentFromRow(r)
98+
}
99+
return out, nil
100+
}
101+
102+
func (a *Assignment) ListAssignedUserIDsForDevice(ctx context.Context, deviceID string) ([]string, error) {
103+
ids, err := a.q.ListDeviceAssignedUserIDs(ctx, deviceID)
104+
if err != nil {
105+
return nil, fmt.Errorf("assignment: list assigned user ids for device: %w", err)
106+
}
107+
return ids, nil
108+
}
109+
110+
func assignmentFromRow(r generated.AssignmentsProjection) store.Assignment {
111+
return store.Assignment{
112+
ID: r.ID,
113+
SourceType: r.SourceType,
114+
SourceID: r.SourceID,
115+
TargetType: r.TargetType,
116+
TargetID: r.TargetID,
117+
SortOrder: r.SortOrder,
118+
Mode: r.Mode,
119+
CreatedAt: r.CreatedAt,
120+
CreatedBy: r.CreatedBy,
121+
}
122+
}

internal/store/postgres/wire.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ func NewRepos(q *generated.Queries) *store.Repos {
1616
return &store.Repos{
1717
Action: NewAction(q),
1818
ActionSet: NewActionSet(q),
19+
Assignment: NewAssignment(q),
1920
AuthState: NewAuthState(q),
2021
Compliance: NewCompliance(q),
2122
Definition: NewDefinition(q),

internal/store/repos.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package store
1515
type Repos struct {
1616
Action ActionRepo
1717
ActionSet ActionSetRepo
18+
Assignment AssignmentRepo
1819
AuthState AuthStateRepo
1920
Compliance ComplianceRepo
2021
Definition DefinitionRepo

0 commit comments

Comments
 (0)