Skip to content

Make TeamResourceService sole owner of google_resources (#491)#225

Merged
peterdrier merged 2 commits intomainfrom
fix/491-teamresource-owns-google-resources
Apr 14, 2026
Merged

Make TeamResourceService sole owner of google_resources (#491)#225
peterdrier merged 2 commits intomainfrom
fix/491-teamresource-owns-google-resources

Conversation

@peterdrier
Copy link
Copy Markdown
Owner

Summary

Phase 1 of consolidating google_resources ownership under TeamResourceService, per .claude/DESIGN_RULES.md. Consumers stop touching DbSet<GoogleResource> directly and go through ITeamResourceService instead.

  • Add 7 read methods to ITeamResourceService: GetResourcesByTeamIdsAsync, GetTeamResourceSummariesAsync, GetActiveResourceCountsByTeamAsync, GetUserTeamResourcesAsync, GetActiveDriveFoldersAsync, GetResourceCountAsync, plus existing GetTeamResourcesAsync / GetResourceByIdAsync.
  • Migrate callers: HumansMetricsService, DriveActivityMonitorService, GoogleAdminService, and TeamService (email-send read, admin-list .Include, CreateAdminTeamSummary counts, and the relocated GetUserTeamGoogleResourcesAsync — now on ITeamResourceService and consumed directly by MyGoogleResourcesViewComponent).
  • Delete TeamResourcePersistence (static helper is gone; each impl does its own DbContext access).
  • Remove Team.GoogleResources EF navigation property and configure the relationship from the GoogleResource side only. Model snapshot updated; dotnet ef migrations has-pending-model-changes confirms no schema diff (this PR is db:no).
  • TeamServiceTeamResourceService now have a mutual read-path dependency (GetUserTeamResourcesAsync needs GetUserTeamsAsync); resolved lazily through IServiceProvider on both sides to avoid the DI cycle.
  • Guardrail: scripts/check-google-resource-ownership.sh (wired into the code-quality CI job) fails if anything outside TeamResourceService/StubTeamResourceService/TeamConfiguration/HumansDbContext touches DbSet<GoogleResource>. GoogleWorkspaceSyncService, SystemTeamSyncJob, ProcessGoogleSyncOutboxJob, and GoogleController stay on the Phase 2 exception list.

Phase 2 — decomposing GoogleWorkspaceSyncService's own writes/reads to the table, the return-tuple pattern for Google state, and removing the remaining exception-list entries — is out of scope for this PR and should get its own issue.

Test plan

  • dotnet build Humans.slnx — clean
  • dotnet test Humans.slnx (non-integration) — 1055 passing, 0 failing
  • dotnet format Humans.slnx --verify-no-changes — clean
  • dotnet ef migrations has-pending-model-changes — no changes
  • bash scripts/check-google-resource-ownership.sh — passes locally
  • CI green on the PR
  • QA smoke: dashboard "My Google Resources" widget still renders; /Admin/Teams admin list still shows correct HasMailGroup / DriveResourceCount per team; added-to-team email still lists the team's drive resources.

Closes nobodies-collective#491 (tracked upstream on nobodies-collective/Humans; auto-close will fire when this batch promotes to upstream main).

🤖 Generated with Claude Code

…lective#491)

Phase 1 of the google_resources ownership cleanup: consumers stop reaching
into DbSet<GoogleResource> directly and go through ITeamResourceService
instead. Adds seven read methods (GetResourcesByTeamIdsAsync,
GetTeamResourceSummariesAsync, GetActiveResourceCountsByTeamAsync,
GetUserTeamResourcesAsync, GetActiveDriveFoldersAsync, GetResourceCountAsync,
plus existing GetTeamResourcesAsync / GetResourceByIdAsync), migrates
HumansMetricsService, DriveActivityMonitorService, GoogleAdminService, and
TeamService (4 call sites + GetUserTeamGoogleResourcesAsync moved out),
deletes TeamResourcePersistence, and removes the Team.GoogleResources EF
navigation property. A CI guardrail
(scripts/check-google-resource-ownership.sh) enforces that future changes
keep DbSet access confined to TeamResourceService, its stub, the DbContext,
and TeamConfiguration — GoogleWorkspaceSyncService and three known callers
remain as Phase 2 exceptions pending a separate decomposition issue.

TeamResourceService <-> TeamService is now mutually recursive for the
GetUserTeamResourcesAsync read path, so both sides resolve each other
lazily through IServiceProvider to avoid the DI cycle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions bot added the db PR includes EF Core migration label Apr 14, 2026
@peterdrier
Copy link
Copy Markdown
Owner Author

PR Review — 2026-04-14

PR: #225 — Make TeamResourceService sole owner of google_resources (nobodies-collective#491)
Branch: fix/491-teamresource-owns-google-resourcesmain
Scope: 21 files, 510 insertions / 145 deletions, 1 commit


Spec Compliance — Issue nobodies-collective#491

Verdict: SUBSTANTIALLY ADDRESSED (7 of 8 criteria met)

# Criterion Verdict
1 All 7 new read methods on ITeamResourceService FULLY ADDRESSED (with naming substitution)
2 TeamResourcePersistence deleted FULLY ADDRESSED
3 Consumer services no longer touch DbSet<GoogleResource> FULLY ADDRESSED
4 Team.GoogleResources nav removed; no .Include remains FULLY ADDRESSED
5 Grep returns only expected hits FULLY ADDRESSED
6 Guardrail in place FULLY ADDRESSED (regex limitation noted)
7 Tests pass, no behavior changes LIKELY ADDRESSED
8 Phase 2 tracked in separate issue with link back NOT ADDRESSED

Notes:

  • Issue named GetActiveResourcesWithTeamAsync; PR shipped GetActiveResourceCountsByTeamAsync (IReadOnlyDictionary<Guid,int>). The narrower count-only shape matches the actual consumer (GoogleAdminService.cs:447) — acceptable substitution.
  • The issue body claimed GoogleController was "clean"; it is not (GoogleController.cs:739). The PR correctly added it to the Phase 2 exception list rather than silently leaving it broken.
  • The DI cycle resolution (mutual lazy IServiceProvider.GetRequiredService<T>() on both sides) is correct and safe for scoped services.
  • Gap: No GitHub issue exists yet for Phase 2 (decomposing GoogleWorkspaceSyncService writes). Should be filed before/at merge so the work is tracked.

Code Quality

No CRITICAL issues.

IMPORTANT:

  1. OnDelete(DeleteBehavior.SetNull) on non-nullable Guid TeamIdTeamConfiguration.cs:116 / GoogleResource.cs:39. Pre-existing latent bug, but this PR touches the exact configuration line. SetNull on a non-nullable FK column will throw a Postgres FK violation if a Team is ever hard-deleted. Fix to DeleteBehavior.Restrict (or Cascade) while here.

  2. GetUserTeamResourcesAsync ordering changeTeamResourceService.cs:143-185. Old single-JOIN query ordered by Postgres collation; new two-step impl re-sorts in memory with StringComparer.OrdinalIgnoreCase. Behavior change for accented names (e.g. "Équipo"). Edge case at 500-user scale, but undocumented.

  3. GetResourceCountAsync missing AsNoTrackingTeamResourceService.cs:196 and StubTeamResourceService.cs:178. Inconsistent with every other new read method.

  4. GetUserTeamResourcesAsync silently drops null TeamTeamResourceService.cs:154-158. The .Where(t => t is not null) masks data integrity issues. Either remove (fail loudly) or log a warning before filtering.

  5. Guardrail regex is variable-name-boundscripts/check-google-resource-ownership.sh:51. Catches _dbContext|dbContext|db|_db only. A future ctx/context/humansDb would slip through. Also doesn't catch context.Set<GoogleResource>(). Acceptable as a first line, but document the limitation.

MINOR:

  1. Lazy IServiceProvider pattern resolves on every property access — minor smell. Lazy<T> initialized in constructor would be slightly more idiomatic. Not required.
  2. Zero direct test coverage for the 6 new ITeamResourceService read methods. Tests only stub them with empty dicts; no behavior tests.
  3. EnsureTeamGroupAsync in GoogleWorkspaceSyncService now does 2 queries instead of 1 (Phase 2 file, intentional, irrelevant at scale).
  4. GetActiveDriveFoldersAsync lacks OrderBy — fine for anomaly detection, inconsistent with sibling methods.
  5. UserTeamGoogleResource record moved between interface files in the same namespace — harmless.

What looks good:

  • Lazy DI cycle resolution is correct for scoped services.
  • All new read methods (except GetResourceCountAsync) use AsNoTracking() and efficient projections (GetTeamResourceSummariesAsync projects to anonymous type).
  • StubTeamResourceService faithfully mirrors the real service, including the UnlinkResourceAsync SaveChangesAsync fix vs. the old persistence helper.
  • TeamConfiguration change to HasMany<GoogleResource>() is the correct pattern for a relationship without a principal-side nav. Snapshot confirms zero schema diff.
  • AdminTeamList behavior is preserved exactly (HasMailGroup / DriveResourceCount semantics match).
  • Guardrail script is wired into build.yml CI and currently passes against HEAD.
  • TeamResourcePersistence deletion removes a layer of indirection cleanly.

Bottom Line

Well-executed Phase 1. Architecture intent is correct, EF patterns are sound, and the CI guardrail is a meaningful regression gate. The actionable items before merge are: (a) file the Phase 2 tracking issue (criterion #8); (b) decide whether to fix the pre-existing OnDelete(SetNull) latent bug while you're touching that configuration line; (c) add AsNoTracking to GetResourceCountAsync. Items (b) and (c) are quick. Nothing blocks merging.


Review by Claude Code /pr-review

- Change google_resources FK from SET NULL to RESTRICT. TeamId is non-nullable,
  so the previous SET NULL would have thrown on team delete. Adds migration
  RestrictGoogleResourceTeamDelete (db:yes, was previously db:no).
- Rewrite GetUserTeamResourcesAsync in both TeamResourceService and
  StubTeamResourceService as a single DB-sorted JOIN. Restores pre-PR ordering
  (DB collation, not OrdinalIgnoreCase), removes the silent null-Team filter,
  and eliminates the service-locator hop through ITeamService for this path.
- Add AsNoTracking to GetResourceCountAsync for consistency with sibling reads.

Phase 2 decomposition of GoogleWorkspaceSyncService writes is tracked as
nobodies-collective#492.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@peterdrier
Copy link
Copy Markdown
Owner Author

Fixes Applied — 2026-04-14

Commit: 8ecf6b1

Addressed review findings 1–4:

  1. OnDelete(SetNull) on non-nullable FK → changed to DeleteBehavior.Restrict. GoogleResource.TeamId is Guid (non-nullable), so the previous SetNull would have thrown a Postgres NOT NULL violation if a team were ever hard-deleted. Restrict is the correct behavior: if resources exist, deletion is blocked and the caller must unlink first. Added migration 20260414215135_RestrictGoogleResourceTeamDelete — this PR is now db:yes (was db:no). EF migration reviewer gate ran clean.

  2. GetUserTeamResourcesAsync ordering + behavior change → rewritten as a single DB-sorted JOIN in both TeamResourceService and StubTeamResourceService. Restores the pre-PR query shape exactly (orderby t.Name, r.Name in SQL, using Postgres collation). No more in-memory OrdinalIgnoreCase sort — matches old behavior for accented names.

  3. GetResourceCountAsync missing AsNoTracking → added in both services, consistent with every other read method.

  4. Silent null-Team filter in GetUserTeamResourcesAsync → removed entirely. The JOIN guarantees Team is present, so no guard is needed. As a bonus, this eliminates the service-locator hop through ITeamService for this code path (the DI cycle is still required for CanManageTeamResourcesAsync elsewhere).

Also filed: Phase 2 tracking issue at nobodies-collective#492, covering the remaining GoogleWorkspaceSyncService / SystemTeamSyncJob / ProcessGoogleSyncOutboxJob / GoogleController direct writes — closes the last open acceptance criterion from nobodies-collective#491.

Verification:

  • dotnet build Humans.slnx — clean, 0 warnings
  • dotnet test Humans.slnx (non-integration) — 1055 passing, 0 failing
  • dotnet format Humans.slnx --verify-no-changes — clean
  • dotnet ef migrations has-pending-model-changes — no changes
  • bash scripts/check-google-resource-ownership.sh — passes
  • EF migration reviewer gate — no CRITICAL issues

Not addressed (minor, deferred):


Fix & re-review by Claude Code /pr-review

@peterdrier peterdrier merged commit ee8fc5a into main Apr 14, 2026
3 checks passed
@peterdrier peterdrier deleted the fix/491-teamresource-owns-google-resources branch April 14, 2026 22:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

db PR includes EF Core migration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make TeamResourceService sole owner of google_resources

1 participant