Skip to content

refactor(installation): layer the module and push tenant filtering into SQL - #62

Merged
mariuspruvot merged 1 commit into
mainfrom
refactor/installation-layering
Jul 31, 2026
Merged

refactor(installation): layer the module and push tenant filtering into SQL#62
mariuspruvot merged 1 commit into
mainfrom
refactor/installation-layering

Conversation

@mariuspruvot

Copy link
Copy Markdown
Owner

Second module through the layering pass, same shape as identity: thin router → service (use cases) → repository (SQL) → typed boundaries (github.py, anthropic.py).

Bugs this surfaced

  • Tenant isolation was done in Python. get_installations_for_user selected every installation row in the database and filtered with a list comprehension. The predicate now lives in the query, so the database never returns rows the caller may not see.
  • An N+1 sitting directly above a comment boasting about avoiding N+1: the dashboard called session.refresh(inst, ["byok_config"]) once per installation. Replaced with selectinload.
  • Label validation existed twice — in SuppressionLabelsRequest and again in the service — with two different exception types producing two different error envelopes for the same user mistake. The schema is now the single copy (it runs before any handler is entered).
  • Dead code: post_commit_status (no callers since the container pivot) and get_default_suppression_labels (referenced only by its own test, and never wired into installation creation, so "defaults" were never applied).

Structural changes

  • Five copy-pasted select(...).where(id == ..., deleted_at.is_(None)) blocks became repository functions; forgetting the soft-delete predicate is now impossible.
  • Three lifecycle mutators (soft-delete, suspend, unsuspend) that differed only in which field they set collapsed into one helper.
  • _build_installation_response — an HTTP response assembled as a dict inside the router — became InstallationResponse.from_model.
  • GitHub error translation (401 → Unauthorized, 403/404 → Forbidden, else ExternalService) was repeated in four functions; now one _translate_status_error.

Verification

  • 350 tests pass (was 317)
  • test_service.py, test_byok_service.py and test_router.py no longer import unittest.mock; new test_github.py covers the boundary through httpx.MockTransport and asserts on real requests (URL, headers, body)
  • ruff and mypy clean

…to SQL

- repository.py owns every query, including the deleted_at predicate that five
  copy-pasted lookups each had to remember
- github.py / anthropic.py are typed boundaries: InstallationToken and
  OrgMembership replace bare dicts, and one _translate_status_error maps GitHub
  statuses onto domain errors in a single place
- service.py is use cases only; three near-identical lifecycle mutators collapse
  into one _apply_lifecycle_change
- router.py is thin and builds responses through InstallationResponse.from_model
  instead of a hand-rolled dict

Fixes found while layering:
- tenant isolation was a Python list comprehension over every installation row
  in the database; the predicate now lives in the query
- the dashboard refreshed byok_config once per installation (an N+1 sitting two
  lines above a comment boasting about avoiding N+1) — now a selectinload
- label rules were implemented twice, in the schema and the service, with two
  different exception types for the same mistake; the schema is now the only
  copy
- dead code removed: post_commit_status (no callers since the pivot) and
  get_default_suppression_labels (only referenced by its own test)

Session queries moved to container/repository.py, which owns that table.

350 tests pass (was 317). test_service.py, test_byok_service.py and
test_router.py are now free of unittest.mock; new test_github.py covers the
boundary with httpx.MockTransport.
@helprs-prod

helprs-prod Bot commented Jul 31, 2026

Copy link
Copy Markdown

helPRs session created for this PR.

Skill: challenge-me | Open session

@mariuspruvot
mariuspruvot merged commit d85d300 into main Jul 31, 2026
10 checks passed
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.

1 participant