Skip to content

refactor: branded OrganizationId type + remove RepositoryWithOrg#116

Merged
coji merged 1 commit intomainfrom
refactor/branded-organization-id
Feb 26, 2026
Merged

refactor: branded OrganizationId type + remove RepositoryWithOrg#116
coji merged 1 commit intomainfrom
refactor/branded-organization-id

Conversation

@coji
Copy link
Owner

@coji coji commented Feb 26, 2026

Summary

  • Introduce OrganizationId branded type (string & { __brand: 'OrganizationId' }) for compile-time safety
  • Remove RepositoryWithOrg type hack from Provider interface; pass organizationId as separate argument to fetch/analyze
  • Remove organizationId re-attach in batch/db/queries.ts (getTenantData)
  • Update all 40 files across routes, batch, services, tests, and scripts

Follows up on items documented in docs/database-per-tenant.md "フォローアップ TODO" section.

Design decisions

  • Cast at system boundaries only: requireOrgMember/requireOrgAdmin return, DB reads, CLI args
  • All downstream functions receive OrganizationId — no raw string flows through tenant-scoped code paths

Test plan

  • pnpm validate — lint, format, typecheck, build, test all pass (31 tests)
  • Branded type correctly catches string misuse at compile time

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor

    • Refactored organization identifier handling across the application by introducing a specialized type for organization references. Updates affect multiple system operations including organization management, data access operations, repository handling, and background processing workflows.
  • Tests

    • Updated tests to ensure compatibility with the refactored organization identifier type.

…WithOrg

- Add `OrganizationId` branded type to prevent mixing with arbitrary strings
- Update all function signatures across routes, batch, and services
- Remove `RepositoryWithOrg` type; pass `organizationId` as separate arg to Provider.fetch/analyze
- Remove organizationId re-attach hack from batch/db/queries getTenantData
- Cast at system boundaries (auth guards, DB reads, CLI args)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a branded OrganizationId type throughout the codebase to replace loose string types for organization identifiers. The type is defined as a branded type in tenant-db.server.ts and propagated across auth modules, route handlers, batch operations, and provider interfaces via targeted type updates and import additions.

Changes

Cohort / File(s) Summary
Type Definition
app/services/tenant-db.server.ts
Introduces branded OrganizationId type; updates public function signatures (getTenantDb, closeTenantDb, createTenantDb, deleteTenantDb, getTenantDbPath) to accept OrganizationId instead of string.
Auth & Core
app/libs/auth.server.ts
Updates OrgContext.organization.id field type from string to OrganizationId; casts result.orgId to OrganizationId in requireOrgMember.
Settings Route Queries
app/routes/$orgSlug/settings/_index/+functions/queries.server.ts
Updates five function signatures (getOrganization, getOrganizationSetting, createDefaultOrganizationSetting, getExportSetting, getIntegration) to accept organizationId: OrganizationId.
Settings Route Mutations
app/routes/$orgSlug/settings/_index/+functions/mutations.server.ts
Updates four mutation signatures (updateOrganizationSetting, deleteOrganization, upsertIntegration, upsertExportSetting) to accept organizationId: OrganizationId.
Repository Route Functions
app/routes/$orgSlug/settings/repositories.../*
Updates organizationId parameter type to OrganizationId across 10 repository-related query and mutation files (add, delete, settings, index, and pull request handlers).
Members & Users Route Functions
app/routes/$orgSlug/settings/members/*, app/routes/$orgSlug/settings/github-users._index/*
Updates organizationId parameter type to OrganizationId in members and github-users query and mutation handlers.
Main Route Functions
app/routes/$orgSlug/_index/+functions/queries.server.ts, app/routes/$orgSlug/ongoing/+functions/queries.server.ts
Updates getMergedPullRequestReport and getOngoingPullRequestReport signatures to accept organizationId: OrganizationId.
Data Management Handler
app/routes/$orgSlug/settings/data-management/index.tsx
Updates provider.analyze call to pass orgContext.id as the first argument.
Batch Database
batch/db/queries.ts, batch/db/mutations.ts
Updates public query/mutation signatures to accept organizationId: OrganizationId; refactors data shaping to remove embedded organizationId from returned objects.
Batch Commands
batch/commands/fetch.ts, batch/commands/report.ts, batch/commands/upsert.ts
Casts organizationId to OrganizationId locally and passes typed identifier to database and provider functions.
Batch Configuration
batch/config/index.ts, batch/helper/path-builder.ts
Updates Config.organizationId field and function parameters to use OrganizationId type.
Batch Jobs & Provider
batch/jobs/crawl.ts, batch/provider/github/provider.ts, batch/provider/github/pullrequest.ts, batch/provider/github/store.ts, batch/provider/index.ts
Updates fetch and analyze signatures to accept explicit organizationId: OrganizationId parameter; removes RepositoryWithOrg type alias; refactors provider to separate organization context from repository data.
Batch Use Cases
batch/usecases/analyze-and-upsert.ts
Updates OrganizationForAnalyze.id type to OrganizationId; removes embedded organizationId from repositories array; passes orgId to provider.analyze.
Batch Scripts & Seed
batch/scripts/golden-compare.ts, batch/scripts/golden-snapshot.ts, db/seed.ts, lab/fetch.ts
Casts organization IDs to OrganizationId at call sites.
Tests
app/services/tenant-db.server.test.ts, batch/helper/path-builder.test.ts
Introduces helper function toOrgId() to cast strings to OrganizationId in test calls; updates test assertions to use typed identifiers.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 A branded ID hops through the code,
Where strings once wandered, now type-safe they strode,
From auth to batch, the refactor's cascade,
Organization IDs, in safety arrays arrayed,
No logic changed, just types realigned—
Type safety and clarity, beautifully designed! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and specifically summarizes the main changes: introducing a branded OrganizationId type and removing RepositoryWithOrg, which are the core refactoring objectives.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/branded-organization-id

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

coji added a commit that referenced this pull request Feb 26, 2026
マルチテナント SaaS リファクタリング (PR #108) と
database-per-tenant 移行 (PR #112, #115, #116) の計画書を削除。
両方とも実装完了済み。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/routes/$orgSlug/settings/repositories.$repository.settings/+functions/mutations.server.ts (1)

18-21: ⚠️ Potential issue | 🟠 Major

Scope the repository UPDATE with organizationId as well.

Line 19 currently scopes only by repository ID. For org-scoped mutations, add WHERE organizationId = organizationId with the server-derived value.

Proposed fix
   return tenantDb
     .updateTable('repositories')
     .where('id', '=', repositoryId)
+    .where('organizationId', '=', organizationId)
     .set(data)
     .executeTakeFirst()

As per coding guidelines, "Every UPDATE/DELETE mutation on org-scoped tables must include WHERE organizationId = ? with a server-derived value to enforce multi-tenant data isolation."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/routes/`$orgSlug/settings/repositories.$repository.settings/+functions/mutations.server.ts
around lines 18 - 21, The UPDATE on the repositories table currently only
filters by repositoryId; add a second where clause to enforce org scoping by
including WHERE organizationId = organizationId using the server-derived
organization id variable (i.e., chain .where('organizationId', '=',
organizationId) alongside the existing .where('id', '=', repositoryId) before
.set(data). Update the block that calls updateTable('repositories') /
where('id', '=', repositoryId) / set(data) / executeTakeFirst() so the query
includes the organizationId check sourced from the server-derived value.
batch/jobs/crawl.ts (1)

58-62: ⚠️ Potential issue | 🟠 Major

Scope the refresh-flag update with a WHERE predicate.

Line 60-62 currently executes an unscoped update and can clear refreshRequestedAt for every row in organizationSettings in that tenant DB.

Suggested fix
     if (refresh) {
       const tenantDb = getTenantDb(orgId)
       await tenantDb
         .updateTable('organizationSettings')
         .set({ refreshRequestedAt: null })
+        .where('organizationId', '=', orgId)
         .execute()
       logger.info('refresh flag consumed.')
     }

Based on learnings: Every UPDATE/DELETE mutation on org-scoped tables must include WHERE organizationId = ? with a server-derived value to enforce multi-tenant data isolation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@batch/jobs/crawl.ts` around lines 58 - 62, The update on organizationSettings
uses tenantDb.updateTable('organizationSettings').set({ refreshRequestedAt: null
}).execute() with no WHERE clause, which risks clearing every row; scope the
mutation by adding a WHERE predicate that filters by organizationId (use the
server-derived orgId variable) so the query only updates rows where
organizationId = orgId (or the equivalent server-provided identifier) before
executing.
🧹 Nitpick comments (1)
batch/provider/github/provider.ts (1)

16-37: Add a tenant-consistency guard when organizationId is passed separately.

With organizationId decoupled from repository objects, Line 30+ and Line 192+ can silently process a repository belonging to a different org if upstream data is mixed. Add an invariant check before store/path usage.

Suggested guard
   const fetch: Provider['fetch'] = async (
     organizationId,
     repository,
     { refresh = false, halt = false },
   ) => {
+    invariant(
+      repository.organizationId === organizationId,
+      'repository does not belong to organization',
+    )
     invariant(repository.repo, 'private token not specified')
     invariant(repository.owner, 'private token not specified')
     invariant(integration.privateToken, 'private token not specified')
@@
     for (const repository of repositories) {
+      invariant(
+        repository.organizationId === organizationId,
+        'repository does not belong to organization',
+      )
       current++
       onProgress?.({ repo: repository.repo, current, total })

Also applies to: 188-211

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@batch/provider/github/provider.ts` around lines 16 - 37, Add a
tenant-consistency invariant that ensures the passed organizationId matches the
repository's owning org before creating storage/path helpers: check that
repository.organizationId (or repository.orgId if that is the field used) equals
organizationId and throw/invariant if not, then proceed to call createStore and
createPathBuilder; apply the same guard in the other code path where
createStore/createPathBuilder are used (the block around the later usage
referenced in the review) so you never process a repository belonging to a
different org.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@app/routes/`$orgSlug/settings/repositories.$repository.settings/+functions/mutations.server.ts:
- Around line 18-21: The UPDATE on the repositories table currently only filters
by repositoryId; add a second where clause to enforce org scoping by including
WHERE organizationId = organizationId using the server-derived organization id
variable (i.e., chain .where('organizationId', '=', organizationId) alongside
the existing .where('id', '=', repositoryId) before .set(data). Update the block
that calls updateTable('repositories') / where('id', '=', repositoryId) /
set(data) / executeTakeFirst() so the query includes the organizationId check
sourced from the server-derived value.

In `@batch/jobs/crawl.ts`:
- Around line 58-62: The update on organizationSettings uses
tenantDb.updateTable('organizationSettings').set({ refreshRequestedAt: null
}).execute() with no WHERE clause, which risks clearing every row; scope the
mutation by adding a WHERE predicate that filters by organizationId (use the
server-derived orgId variable) so the query only updates rows where
organizationId = orgId (or the equivalent server-provided identifier) before
executing.

---

Nitpick comments:
In `@batch/provider/github/provider.ts`:
- Around line 16-37: Add a tenant-consistency invariant that ensures the passed
organizationId matches the repository's owning org before creating storage/path
helpers: check that repository.organizationId (or repository.orgId if that is
the field used) equals organizationId and throw/invariant if not, then proceed
to call createStore and createPathBuilder; apply the same guard in the other
code path where createStore/createPathBuilder are used (the block around the
later usage referenced in the review) so you never process a repository
belonging to a different org.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 80b81f3 and 0663b44.

📒 Files selected for processing (40)
  • app/libs/auth.server.ts
  • app/routes/$orgSlug/_index/+functions/queries.server.ts
  • app/routes/$orgSlug/ongoing/+functions/queries.server.ts
  • app/routes/$orgSlug/settings/_index/+functions/mutations.server.ts
  • app/routes/$orgSlug/settings/_index/+functions/queries.server.ts
  • app/routes/$orgSlug/settings/data-management/index.tsx
  • app/routes/$orgSlug/settings/github-users._index/mutations.server.ts
  • app/routes/$orgSlug/settings/github-users._index/queries.server.ts
  • app/routes/$orgSlug/settings/members/mutations.server.ts
  • app/routes/$orgSlug/settings/members/queries.server.ts
  • app/routes/$orgSlug/settings/repositories.$repository.$pull/queries.server.ts
  • app/routes/$orgSlug/settings/repositories.$repository._index/queries.server.ts
  • app/routes/$orgSlug/settings/repositories.$repository.delete/+functions/mutations.server.ts
  • app/routes/$orgSlug/settings/repositories.$repository.delete/+functions/queries.server.ts
  • app/routes/$orgSlug/settings/repositories.$repository.settings/+functions/mutations.server.ts
  • app/routes/$orgSlug/settings/repositories.$repository.settings/+functions/queries.server.ts
  • app/routes/$orgSlug/settings/repositories._index/queries.server.ts
  • app/routes/$orgSlug/settings/repositories.add/+functions/mutations.server.ts
  • app/routes/$orgSlug/settings/repositories.add/+functions/queries.server.ts
  • app/routes/admin/+create/mutations.server.ts
  • app/services/tenant-db.server.test.ts
  • app/services/tenant-db.server.ts
  • batch/commands/fetch.ts
  • batch/commands/report.ts
  • batch/commands/upsert.ts
  • batch/config/index.ts
  • batch/db/mutations.ts
  • batch/db/queries.ts
  • batch/helper/path-builder.test.ts
  • batch/helper/path-builder.ts
  • batch/jobs/crawl.ts
  • batch/provider/github/provider.ts
  • batch/provider/github/pullrequest.ts
  • batch/provider/github/store.ts
  • batch/provider/index.ts
  • batch/scripts/golden-compare.ts
  • batch/scripts/golden-snapshot.ts
  • batch/usecases/analyze-and-upsert.ts
  • db/seed.ts
  • lab/fetch.ts

@coji coji merged commit 0663b44 into main Feb 26, 2026
6 checks passed
@coji coji deleted the refactor/branded-organization-id branch February 26, 2026 13:21
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