Skip to content

refactor(store): wave B.2 — settings + identity_link + logs repos (#247)#248

Merged
PaulDotterer merged 1 commit into
mainfrom
feat/247-settings-idlink-logs-repos
May 14, 2026
Merged

refactor(store): wave B.2 — settings + identity_link + logs repos (#247)#248
PaulDotterer merged 1 commit into
mainfrom
feat/247-settings-idlink-logs-repos

Conversation

@PaulDotterer

@PaulDotterer PaulDotterer commented May 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Continues the per-domain migration started by Wave B.1 (#246). Three small single-handler domains move from `Store.Queries()` to domain-specific repos under `store.Repos()`; cross-domain reads stay on `Queries()` until those domains migrate in later sub-waves.

Stacked on #246. Diff currently shows Wave A + B.1 + B.2 commits; after #244 / #246 merge, the diff collapses to B.2 only.

Domains migrated

Repo Methods Handler
`SettingsRepo` `GetServer` `settings_handler.go`
`IdentityLinkRepo` `Get`, `ListForUser`, `CountForUser` `identity_link_handler.go` + `populateUserIdentityLinks` in `user_handler.go`
`LogsRepo` `CreateQueryResult`, `GetQueryResult`, `ExpirePendingQueryResult` `logs_handler.go` (first write-side migration in Wave B)

`ListForUser` returns the join-shape `IdentityLinkWithProvider` so handlers can't confuse it with the basic Get shape (which doesn't carry provider display fields).

Each Postgres impl translates `pgx.ErrNoRows` → `store.ErrNotFound` at the boundary, matching Wave A's contract.

Non-goals

  • User / device repos — cross-domain reads (`GetUserByID`, `GetDeviceByID`) stay on `Queries()` until those domains migrate.
  • Compliance-policy queries — separate sub-wave.
  • UnitOfWork. None of these three domains need cross-domain atomicity.

Acceptance

  • Build / vet / staticcheck / gofmt: clean.
  • Targeted tests (`TestGetDeviceCompliance*`, `TestLogin*`, `TestCreateAssignment*`, `TestGetCurrentUser*`, `TestSyncUserSystemActions*`, helpers): 221s of coverage passes.
  • No behaviour change in the three handlers.

Test plan

  • CI green on all checks
  • CodeRabbit PR review passes

Part of #242. Closes #247.

Summary by CodeRabbit

Release Notes

  • Refactor
    • Refactored internal data access patterns to use a repository abstraction layer for identity links, device logs, and server settings. This improves code organization and maintainability without affecting user-facing functionality.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The PR introduces three small domain repositories (IdentityLink, Logs, Settings) with models and interfaces, implements them using PostgreSQL and sqlc-generated queries, wires them into the store infrastructure, and migrates their respective API handlers to use the new abstractions instead of calling SQL query methods directly. Cross-domain reads remain on Store.Queries() until their owning domains migrate.

Changes

Domain repositories: settings, identity links, and logs

Layer / File(s) Summary
Identity link models and interface
internal/store/identity_link.go
IdentityLink struct with identity/provider IDs and linkage timestamps; IdentityLinkWithProvider embeds IdentityLink and adds provider display fields; IdentityLinkRepo interface defines Get, ListForUser (newest-linked first), and CountForUser read patterns.
Logs models and interface
internal/store/logs.go
LogQueryResult struct with query/device IDs, completion/success flags, error/log payloads, and created/completed timestamps; LogsRepo interface defines CreateQueryResult, GetQueryResult (with ErrNotFound for missing/expired rows), and ExpirePendingQueryResult write patterns.
Settings models and interface
internal/store/settings.go
ServerSettings struct with provisioning/SSH access flags and update timestamp; SettingsRepo interface defines GetServer(ctx) read-only pattern with documented ErrNotFound behavior when projection unseeded.
PostgreSQL identity link repository
internal/store/postgres/identity_link.go
IdentityLink repo wraps sqlc queries; Get translates not-found errors and maps to domain type; ListForUser composes joined rows into IdentityLinkWithProvider slice; CountForUser returns user link count.
PostgreSQL logs repository
internal/store/postgres/logs.go
Logs repo wraps sqlc queries; CreateQueryResult creates pending result rows; GetQueryResult fetches with not-found translation and error wrapping; ExpirePendingQueryResult marks rows failed with caller-supplied error message.
PostgreSQL settings repository
internal/store/postgres/settings.go
Settings repo wraps sqlc GetServerSettings query; GetServer translates not-found DB errors to store.ErrNotFound and maps row fields into ServerSettings with context-wrapped error messages.
Store infrastructure: Repos wiring
internal/store/repos.go, internal/store/postgres/wire.go
Repos struct gains IdentityLink, Logs, Settings fields; NewRepos constructor wires all three PostgreSQL implementations alongside retained Compliance repo.
Handler migrations to repositories
internal/api/identity_link_handler.go, internal/api/logs_handler.go, internal/api/settings_handler.go, internal/api/user_handler.go
Identity link handler calls Repos().IdentityLink.Get/ListForUser/CountForUser; logs handler calls Repos().Logs.CreateQueryResult/GetQueryResult/ExpirePendingQueryResult; settings handler and user helper call Repos().Settings.GetServer and Repos().IdentityLink.ListForUser respectively. Generated SQL package import removed from identity link handler.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

The changes follow a consistent, repetitive pattern: three independent domain repos with identical checkpoint structure (models → interface → postgres impl → handler migration). No intricate logic, cross-domain atomicity, or complex error handling beyond standard not-found translation. Handoff points are well-defined and testable in isolation.

Possibly related PRs

  • manchtools/power-manage-server#122: Modifies the Go listener and UpdateServerSettings logic that populates the server_settings_projection table that this PR reads via SettingsRepo.GetServer.
  • manchtools/power-manage-server#3: Updates AutoUpdateAgents mapping in GetServerSettings/UpdateServerSettings in the same handler that this PR migrates to use the new SettingsRepo.

Poem

🐰 Three little repos hop into the store,
Identity links, logs, and settings galore!
With models and interfaces all snugly aligned,
PostgreSQL binds them with queries so kind.
Handlers now dance with abstractions so lean! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically identifies the main change: a refactoring that introduces three new repository abstractions (settings, identity_link, logs) as part of Wave B.2 of the store migration.
Linked Issues check ✅ Passed All coding requirements from issue #247 are met: three repos (SettingsRepo, IdentityLinkRepo, LogsRepo) with specified methods are implemented, Repos struct is extended with new fields, handlers use repos instead of Queries for domain-specific operations, and Postgres implementations translate pgx.ErrNoRows to store.ErrNotFound.
Out of Scope Changes check ✅ Passed All changes are within scope: new repos and implementations for settings/identity_link/logs domains, handler updates to use repos, and wire-up in Repos struct—nothing addresses user/device repos, compliance-policy queries, or UnitOfWork as correctly deferred to other waves.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/247-settings-idlink-logs-repos

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/api/compliance_handler.go`:
- Around line 33-41: The DeviceResults error branch currently always returns an
internal error (apiErrorCtx with ErrInternal/connect.CodeInternal) even when the
repo indicates the device is missing; update the error handling for
h.store.Repos().Compliance.DeviceResults(ctx, deviceID) to detect the repo "not
found" case (same normalization used by DeviceSummary) and return a NotFound API
error (use the NotFound error/code variant used elsewhere) instead of
ErrInternal, so missing-device errors map to NotFound while other errors still
return internal errors via apiErrorCtx.

In `@internal/api/logs_handler.go`:
- Around line 101-104: The current error handling around
h.store.Repos().Logs.GetQueryResult treats every error as NotFound; change it to
call store.IsNotFound(err) and only return apiErrorCtx(ctx,
ErrQueryResultNotFound, connect.CodeNotFound, ...) for that case, otherwise
return an internal error via apiErrorCtx with connect.CodeInternal (e.g.,
apiErrorCtx(ctx, err, connect.CodeInternal, "failed to get log query result")).
Update the GetQueryResult error branch to discriminate store.IsNotFound(err) vs
other errors accordingly.

In `@internal/idp/linker.go`:
- Around line 142-143: The current conditional unconditionally calls
store.IsNotFound(err) and can misclassify a nil error from the stale-link
cleanup; change the guard so you only classify not-found when err is non-nil
(e.g. replace the condition with "if err != nil && !store.IsNotFound(err)" or
first check "if err != nil { if !store.IsNotFound(err) return ... }") so that a
nil err falls through into the Step 2/3 linking logic; update the branch
containing store.IsNotFound(err) accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9b968f12-370c-4a8a-a6f9-b65b36c73f9e

📥 Commits

Reviewing files that changed from the base of the PR and between 8d12458 and e246862.

📒 Files selected for processing (54)
  • cmd/control/main.go
  • cmd/indexer/main.go
  • internal/api/action_crud.go
  • internal/api/action_dispatch.go
  • internal/api/assignment_handler.go
  • internal/api/auth_handler.go
  • internal/api/certificate_handler.go
  • internal/api/compliance_handler.go
  • internal/api/compliance_policy_handler.go
  • internal/api/device_group_handler.go
  • internal/api/device_handler.go
  • internal/api/helpers.go
  • internal/api/helpers_test.go
  • internal/api/identity_link_handler.go
  • internal/api/idp_handler.go
  • internal/api/logs_handler.go
  • internal/api/registration_handler.go
  • internal/api/role_handler.go
  • internal/api/search_listener.go
  • internal/api/search_listener_e2e_test.go
  • internal/api/settings_handler.go
  • internal/api/sso_handler.go
  • internal/api/terminal_handler.go
  • internal/api/totp_handler.go
  • internal/api/user_group_handler.go
  • internal/api/user_handler.go
  • internal/control/inbox_worker.go
  • internal/idp/linker.go
  • internal/idp/linker_test.go
  • internal/projectors/assignment_listener.go
  • internal/projectors/assignment_test.go
  • internal/projectors/device_group_listener.go
  • internal/projectors/user_group_listener.go
  • internal/scim/auth.go
  • internal/scim/groups.go
  • internal/scim/groups_create.go
  • internal/scim/groups_helpers.go
  • internal/scim/groups_mutate.go
  • internal/scim/users.go
  • internal/scim/users_create.go
  • internal/scim/users_mutate.go
  • internal/store/compliance.go
  • internal/store/identity_link.go
  • internal/store/logs.go
  • internal/store/notfound.go
  • internal/store/postgres/compliance.go
  • internal/store/postgres/identity_link.go
  • internal/store/postgres/logs.go
  • internal/store/postgres/settings.go
  • internal/store/postgres/wire.go
  • internal/store/repos.go
  • internal/store/settings.go
  • internal/store/store.go
  • internal/testutil/postgres.go

Comment on lines +33 to 41
results, err := h.store.Repos().Compliance.DeviceResults(ctx, deviceID)
if err != nil {
return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal, "failed to query compliance results")
}

summary, err := h.store.Queries().GetDeviceComplianceSummary(ctx, deviceID)
summary, err := h.store.Repos().Compliance.DeviceSummary(ctx, deviceID)
if err != nil {
return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal, "failed to query compliance summary")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Map missing-device compliance to NotFound instead of Internal.

The summary lookup now goes through repo not-found normalization, but this branch still always returns Internal. This can return the wrong API code for a missing device.

Suggested fix
-	results, err := h.store.Repos().Compliance.DeviceResults(ctx, deviceID)
-	if err != nil {
-		return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal, "failed to query compliance results")
-	}
-
 	summary, err := h.store.Repos().Compliance.DeviceSummary(ctx, deviceID)
 	if err != nil {
-		return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal, "failed to query compliance summary")
+		return nil, handleGetError(ctx, err, ErrDeviceNotFound, "device not found")
 	}
+
+	results, err := h.store.Repos().Compliance.DeviceResults(ctx, deviceID)
+	if err != nil {
+		return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal, "failed to query compliance results")
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/api/compliance_handler.go` around lines 33 - 41, The DeviceResults
error branch currently always returns an internal error (apiErrorCtx with
ErrInternal/connect.CodeInternal) even when the repo indicates the device is
missing; update the error handling for
h.store.Repos().Compliance.DeviceResults(ctx, deviceID) to detect the repo "not
found" case (same normalization used by DeviceSummary) and return a NotFound API
error (use the NotFound error/code variant used elsewhere) instead of
ErrInternal, so missing-device errors map to NotFound while other errors still
return internal errors via apiErrorCtx.

Comment on lines +101 to 104
result, err := h.store.Repos().Logs.GetQueryResult(ctx, req.Msg.QueryId)
if err != nil {
return nil, apiErrorCtx(ctx, ErrQueryResultNotFound, connect.CodeNotFound, "log query result not found")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Differentiate not-found from backend failures in result lookup.

Line 102 currently maps every GetQueryResult failure to connect.CodeNotFound. That hides real backend failures as 404s; only store.IsNotFound(err) should return not-found, otherwise return internal.

As per coding guidelines, "Errors must use apiErrorCtx with proper Connect error codes."

💡 Suggested fix
 func (h *LogsHandler) GetDeviceLogResult(ctx context.Context, req *connect.Request[pm.GetDeviceLogResultRequest]) (*connect.Response[pm.GetDeviceLogResultResponse], error) {
 	result, err := h.store.Repos().Logs.GetQueryResult(ctx, req.Msg.QueryId)
 	if err != nil {
-		return nil, apiErrorCtx(ctx, ErrQueryResultNotFound, connect.CodeNotFound, "log query result not found")
+		if store.IsNotFound(err) {
+			return nil, apiErrorCtx(ctx, ErrQueryResultNotFound, connect.CodeNotFound, "log query result not found")
+		}
+		h.logger.Error("failed to get log query result", "query_id", req.Msg.QueryId, "error", err)
+		return nil, apiErrorCtx(ctx, ErrInternal, connect.CodeInternal, "failed to get log query result")
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/api/logs_handler.go` around lines 101 - 104, The current error
handling around h.store.Repos().Logs.GetQueryResult treats every error as
NotFound; change it to call store.IsNotFound(err) and only return
apiErrorCtx(ctx, ErrQueryResultNotFound, connect.CodeNotFound, ...) for that
case, otherwise return an internal error via apiErrorCtx with
connect.CodeInternal (e.g., apiErrorCtx(ctx, err, connect.CodeInternal, "failed
to get log query result")). Update the GetQueryResult error branch to
discriminate store.IsNotFound(err) vs other errors accordingly.

Comment thread internal/idp/linker.go
Comment on lines +142 to 143
if !store.IsNotFound(err) {
return nil, fmt.Errorf("lookup identity link: %w", err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard the fallback branch with err != nil before classifying not-found.

After stale-link cleanup (Line 102), the code intentionally falls through with a nil err; Line 142 can then return early and skip Step 2/3 linking logic.

Proposed fix
-	if !store.IsNotFound(err) {
+	if err != nil && !store.IsNotFound(err) {
 		return nil, fmt.Errorf("lookup identity link: %w", err)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if !store.IsNotFound(err) {
return nil, fmt.Errorf("lookup identity link: %w", err)
if err != nil && !store.IsNotFound(err) {
return nil, fmt.Errorf("lookup identity link: %w", err)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/idp/linker.go` around lines 142 - 143, The current conditional
unconditionally calls store.IsNotFound(err) and can misclassify a nil error from
the stale-link cleanup; change the guard so you only classify not-found when err
is non-nil (e.g. replace the condition with "if err != nil &&
!store.IsNotFound(err)" or first check "if err != nil { if
!store.IsNotFound(err) return ... }") so that a nil err falls through into the
Step 2/3 linking logic; update the branch containing store.IsNotFound(err)
accordingly.

Continues the per-domain migration started by Wave B.1 (#246). Three
small handlers move from Store.Queries() to domain-specific repos
under store.Repos(); cross-domain reads (GetUserByID, GetDeviceByID)
stay on Queries until those domains migrate in later sub-waves.

Domains migrated:

- SettingsRepo (GetServer) — settings_handler.go.
- IdentityLinkRepo (Get / ListForUser / CountForUser) — identity_link_handler.go
  + populateUserIdentityLinks in user_handler.go. ListForUser returns
  the join-shape IdentityLinkWithProvider so handlers can't confuse
  it with the basic Get shape.
- LogsRepo (CreateQueryResult / GetQueryResult / ExpirePendingQueryResult) —
  logs_handler.go. First write-side migration in Wave B; the :exec
  signatures stay opaque to handlers, matching the read-side pattern.

Each new repo's Postgres impl translates pgx.ErrNoRows to
store.ErrNotFound at the boundary, matching the contract established
in Wave A (#244) and applied in Wave B.1.

Non-goals:

- User / device repos — cross-domain reads in these handlers
  remain on Store.Queries() until those domains migrate.
- Compliance-policy queries — separate sub-wave.

Closes #247.
@PaulDotterer PaulDotterer force-pushed the feat/247-settings-idlink-logs-repos branch from e246862 to fc5711f Compare May 14, 2026 19:06

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/api/user_handler.go`:
- Around line 802-805: The call to h.store.Repos().IdentityLink.ListForUser is
ignoring errors with a bare return; change this to handle the error explicitly
by logging or returning it to the caller (e.g., wrap with context like "failed
to load identity links for user <user.Id>") and return an appropriate error/HTTP
response instead of a silent return so failures are visible and traceable;
ensure the error path in the surrounding handler function returns the error (or
sends a proper error response) rather than aborting silently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d78125b0-690a-47ad-8aa2-1b859bee0c73

📥 Commits

Reviewing files that changed from the base of the PR and between e246862 and fc5711f.

📒 Files selected for processing (12)
  • internal/api/identity_link_handler.go
  • internal/api/logs_handler.go
  • internal/api/settings_handler.go
  • internal/api/user_handler.go
  • internal/store/identity_link.go
  • internal/store/logs.go
  • internal/store/postgres/identity_link.go
  • internal/store/postgres/logs.go
  • internal/store/postgres/settings.go
  • internal/store/postgres/wire.go
  • internal/store/repos.go
  • internal/store/settings.go
✅ Files skipped from review due to trivial changes (1)
  • internal/store/identity_link.go
🚧 Files skipped from review as they are similar to previous changes (8)
  • internal/store/settings.go
  • internal/api/identity_link_handler.go
  • internal/store/postgres/settings.go
  • internal/store/logs.go
  • internal/store/repos.go
  • internal/api/logs_handler.go
  • internal/store/postgres/identity_link.go
  • internal/store/postgres/logs.go

Comment on lines +802 to 805
links, err := h.store.Repos().IdentityLink.ListForUser(ctx, user.Id)
if err != nil {
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t silently ignore identity-link load errors.

At Line 803, the repo error is dropped with a bare return, so failures are invisible and the response may be partially populated without traceability.

Suggested fix
func (h *UserHandler) populateUserIdentityLinks(ctx context.Context, user *pm.User) {
	links, err := h.store.Repos().IdentityLink.ListForUser(ctx, user.Id)
	if err != nil {
+		h.logger.Warn("failed to load identity links", "user_id", user.Id, "error", err)
		return
	}
	for _, link := range links {
		user.IdentityLinks = append(user.IdentityLinks, identityLinkRowToProto(link))
	}
}

As per coding guidelines: internal/api/*_handler.go: “Never silently ignore errors.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/api/user_handler.go` around lines 802 - 805, The call to
h.store.Repos().IdentityLink.ListForUser is ignoring errors with a bare return;
change this to handle the error explicitly by logging or returning it to the
caller (e.g., wrap with context like "failed to load identity links for user
<user.Id>") and return an appropriate error/HTTP response instead of a silent
return so failures are visible and traceable; ensure the error path in the
surrounding handler function returns the error (or sends a proper error
response) rather than aborting silently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

wave B.2: settings + identity_link + logs repos

1 participant