Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 20 additions & 30 deletions internal/api/assignment_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"github.com/manchtools/power-manage/server/internal/eventtypes"
"github.com/manchtools/power-manage/server/internal/eventtypes/payloads"
"github.com/manchtools/power-manage/server/internal/store"
db "github.com/manchtools/power-manage/server/internal/store/generated"
)

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

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

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

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

assignments, err := h.store.Queries().ListAssignments(ctx, db.ListAssignmentsParams{
Column1: sourceTypeStr,
Column2: req.Msg.SourceId,
Column3: targetTypeStr,
Column4: req.Msg.TargetId,
Limit: pageSize,
Offset: offset,
})
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})
if err != nil {
return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal, "failed to list assignments")
}

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

protoAssignments := make([]*pm.Assignment, len(assignments))
for i, a := range assignments {
protoAssignments[i] = h.assignmentToProto(a)
// Transitional: ListAssignmentsForUser hasn't migrated yet
// (different row shape), so convert to store.Assignment for
// assignmentToProto. Falls away when that query moves.
protoAssignments[i] = h.assignmentToProto(store.Assignment{
ID: a.ID,
SourceType: a.SourceType,
SourceID: a.SourceID,
TargetType: a.TargetType,
TargetID: a.TargetID,
SortOrder: a.SortOrder,
Mode: a.Mode,
CreatedAt: a.CreatedAt,
CreatedBy: a.CreatedBy,
})
}

return connect.NewResponse(&pm.GetUserAssignmentsResponse{
Assignments: protoAssignments,
}), nil
}

func (h *AssignmentHandler) assignmentToProto(a db.AssignmentsProjection) *pm.Assignment {
func (h *AssignmentHandler) assignmentToProto(a store.Assignment) *pm.Assignment {
assignment := &pm.Assignment{
Id: a.ID,
SourceType: assignmentSourceTypeFromString(a.SourceType),
Expand Down
2 changes: 1 addition & 1 deletion internal/api/device_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,7 @@ func (h *DeviceHandler) CreateLuksToken(ctx context.Context, req *connect.Reques
}

// Only an assigned owner can create a LUKS token
assignedUserIDs, err := h.store.Queries().ListDeviceAssignedUserIDs(ctx, req.Msg.DeviceId)
assignedUserIDs, err := h.store.Repos().Assignment.ListAssignedUserIDsForDevice(ctx, req.Msg.DeviceId)
if err != nil {
return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal, "failed to check device assignments")
}
Expand Down
4 changes: 2 additions & 2 deletions internal/api/user_selection_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func (h *UserSelectionHandler) SetUserSelection(ctx context.Context, req *connec
sourceTypeStr := assignmentSourceTypeToString(req.Msg.SourceType)

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

// Get available assignments for this device
assignments, err := h.store.Queries().ListAvailableAssignmentsForDevice(ctx, req.Msg.DeviceId)
assignments, err := h.store.Repos().Assignment.ListAvailableForDevice(ctx, req.Msg.DeviceId)
if err != nil {
return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal, "failed to list available assignments")
}
Expand Down
99 changes: 99 additions & 0 deletions internal/store/assignment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package store

import (
"context"
"time"
)

// Assignment is the basic assignment-projection row. Mode is the
// pm.AssignmentMode enum value (Required / Available); SortOrder
// drives the dispatch ordering on the agent.
type Assignment struct {
ID string
SourceType string
SourceID string
TargetType string
TargetID string
SortOrder int32
Mode int32
CreatedAt *time.Time
CreatedBy string
}

// AssignmentWithNames is the List-side shape — wraps Assignment with
// the joined SourceName + TargetName so the list UI can render without
// per-row lookups for display.
type AssignmentWithNames struct {
Assignment
SourceName string
TargetName string
}

// AssignmentKey is the composite (source, target) lookup used by
// GetAssignment and the duplicate-pre-check path. The projection's
// unique index on this tuple guarantees at most one active row.
type AssignmentKey struct {
SourceType string
SourceID string
TargetType string
TargetID string
}

// ListAssignmentsFilter pairs pagination with the four optional
// filter axes the UI exposes. Empty strings disable each axis
// independently — the projection treats "" as "no filter".
type ListAssignmentsFilter struct {
SourceType string
SourceID string
TargetType string
TargetID string
Limit int32
Offset int32
}

// CountAssignmentsFilter mirrors ListAssignmentsFilter's filter
// fields for the matching count side.
type CountAssignmentsFilter struct {
SourceType string
SourceID string
TargetType string
TargetID string
}

// AssignmentRepo reads assignment state. Writes flow through events
// (AssignmentCreated / AssignmentDeleted / AssignmentModeChanged /
// AssignmentSortOrderChanged) and the projector listener.
//
// Per-device join queries that hydrate action / source-type details
// (ListAssignedActionsForDevice, ListDirectAssignmentsForDevice,
// ListGroupAssignmentsForDevice, ListAssignmentsForUser) stay on
// Store.Queries() — they return join-shaped rows that migrate with
// their respective domain consumers.
type AssignmentRepo interface {
// Get returns the assignment for a (source, target) tuple.
// Returns ErrNotFound when no active assignment matches.
Get(ctx context.Context, key AssignmentKey) (Assignment, error)

// GetByID returns the assignment by its ID.
GetByID(ctx context.Context, id string) (Assignment, error)

// List returns a page of assignments hydrated with source +
// target display names, ordered by created_at descending.
List(ctx context.Context, filter ListAssignmentsFilter) ([]AssignmentWithNames, error)

// Count returns the total matching the filter.
Count(ctx context.Context, filter CountAssignmentsFilter) (int64, error)

// ListAvailableForDevice returns every "Available"-mode
// assignment whose effective target reaches the given device
// (directly, via device-group, via user assigned to the device,
// or via that user's user-groups). Used to build the per-device
// "what can the user opt into" catalog.
ListAvailableForDevice(ctx context.Context, deviceID string) ([]Assignment, error)

// ListAssignedUserIDsForDevice returns the user IDs with at
// least one assignment that resolves to the given device.
// Returns an empty slice when the device has no user
// assignments.
ListAssignedUserIDsForDevice(ctx context.Context, deviceID string) ([]string, error)
}
122 changes: 122 additions & 0 deletions internal/store/postgres/assignment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package postgres

import (
"context"
"fmt"

"github.com/manchtools/power-manage/server/internal/store"
"github.com/manchtools/power-manage/server/internal/store/generated"
)

// Assignment implements store.AssignmentRepo against
// assignments_projection.
type Assignment struct {
q *generated.Queries
}

// NewAssignment returns an Assignment repo bound to the given sqlc
// handle.
func NewAssignment(q *generated.Queries) *Assignment {
return &Assignment{q: q}
}

func (a *Assignment) Get(ctx context.Context, key store.AssignmentKey) (store.Assignment, error) {
row, err := a.q.GetAssignment(ctx, generated.GetAssignmentParams{
SourceType: key.SourceType,
SourceID: key.SourceID,
TargetType: key.TargetType,
TargetID: key.TargetID,
})
if err != nil {
return store.Assignment{}, fmt.Errorf("assignment: get: %w", translateNotFound(err))
}
return assignmentFromRow(row), nil
}

func (a *Assignment) GetByID(ctx context.Context, id string) (store.Assignment, error) {
row, err := a.q.GetAssignmentByID(ctx, id)
if err != nil {
return store.Assignment{}, fmt.Errorf("assignment: get by id: %w", translateNotFound(err))
}
return assignmentFromRow(row), nil
}

func (a *Assignment) List(ctx context.Context, filter store.ListAssignmentsFilter) ([]store.AssignmentWithNames, error) {
rows, err := a.q.ListAssignments(ctx, generated.ListAssignmentsParams{
Column1: filter.SourceType,
Column2: filter.SourceID,
Column3: filter.TargetType,
Column4: filter.TargetID,
Limit: filter.Limit,
Offset: filter.Offset,
})
if err != nil {
return nil, fmt.Errorf("assignment: list: %w", err)
}
out := make([]store.AssignmentWithNames, len(rows))
for i, r := range rows {
out[i] = store.AssignmentWithNames{
Assignment: store.Assignment{
ID: r.ID,
SourceType: r.SourceType,
SourceID: r.SourceID,
TargetType: r.TargetType,
TargetID: r.TargetID,
SortOrder: r.SortOrder,
Mode: r.Mode,
CreatedAt: r.CreatedAt,
CreatedBy: r.CreatedBy,
},
SourceName: r.SourceName,
TargetName: r.TargetName,
}
}
return out, nil
}

func (a *Assignment) Count(ctx context.Context, filter store.CountAssignmentsFilter) (int64, error) {
n, err := a.q.CountAssignments(ctx, generated.CountAssignmentsParams{
Column1: filter.SourceType,
Column2: filter.SourceID,
Column3: filter.TargetType,
Column4: filter.TargetID,
})
if err != nil {
return 0, fmt.Errorf("assignment: count: %w", translateNotFound(err))
}
return n, nil
}

func (a *Assignment) ListAvailableForDevice(ctx context.Context, deviceID string) ([]store.Assignment, error) {
rows, err := a.q.ListAvailableAssignmentsForDevice(ctx, deviceID)
if err != nil {
return nil, fmt.Errorf("assignment: list available for device: %w", err)
}
out := make([]store.Assignment, len(rows))
for i, r := range rows {
out[i] = assignmentFromRow(r)
}
return out, nil
}

func (a *Assignment) ListAssignedUserIDsForDevice(ctx context.Context, deviceID string) ([]string, error) {
ids, err := a.q.ListDeviceAssignedUserIDs(ctx, deviceID)
if err != nil {
return nil, fmt.Errorf("assignment: list assigned user ids for device: %w", err)
}
return ids, nil
}

func assignmentFromRow(r generated.AssignmentsProjection) store.Assignment {
return store.Assignment{
ID: r.ID,
SourceType: r.SourceType,
SourceID: r.SourceID,
TargetType: r.TargetType,
TargetID: r.TargetID,
SortOrder: r.SortOrder,
Mode: r.Mode,
CreatedAt: r.CreatedAt,
CreatedBy: r.CreatedBy,
}
}
1 change: 1 addition & 0 deletions internal/store/postgres/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ func NewRepos(q *generated.Queries) *store.Repos {
return &store.Repos{
Action: NewAction(q),
ActionSet: NewActionSet(q),
Assignment: NewAssignment(q),
AuthState: NewAuthState(q),
Compliance: NewCompliance(q),
Definition: NewDefinition(q),
Expand Down
1 change: 1 addition & 0 deletions internal/store/repos.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package store
type Repos struct {
Action ActionRepo
ActionSet ActionSetRepo
Assignment AssignmentRepo
AuthState AuthStateRepo
Compliance ComplianceRepo
Definition DefinitionRepo
Expand Down