Skip to content

fix: inject scope on joins when root is unscoped#16

Merged
benvinegar merged 1 commit into
mainfrom
fix/unscoped-root-scoped-join
Jun 26, 2026
Merged

fix: inject scope on joins when root is unscoped#16
benvinegar merged 1 commit into
mainfrom
fix/unscoped-root-scoped-join

Conversation

@benvinegar

Copy link
Copy Markdown
Member

Problem

Currently, scoped join protection is only applied when the root table in from(table) is itself scoped. If a query starts from an unscoped table (e.g. users which has no scoping rules) and joins a protected/scoped table (e.g. projects), the query builder returned by .from(users) is the raw, unwrapped Drizzle query builder. This means that subsequent calls to .leftJoin or .innerJoin bypass the scoped join wrapper entirely, and scope predicates are never injected for the joined table.

// projects is scoped, users is unscoped
await scopedDb
  .select()
  .from(users)
  .leftJoin(projects, eq(projects.ownerId, users.id));
// ↑ projects from ALL scopes/tenants are leaked!

Fix

  1. Always wrap query builders returned by from(table) in the createScopedFromBuilder wrapper, even when the root table has no scoping rules (rootRule === undefined).
  2. Update assertWhereAllowed and scopeCondition to support an optional/undefined rule parameter. When undefined, assertWhereAllowed is a no-op and scopeCondition passes the condition through unmodified.
  3. This ensures that even for unscoped root tables, any subsequent .leftJoin() or .innerJoin() is intercepted, and table-specific scope rules are safely injected into the join condition using scopeJoinCondition.
  4. In then(), if rootRule is undefined and no scope predicate needs to be injected, we pass the raw builder through directly rather than calling .where(undefined).

Validation

  • Added full unit and regression tests for unscoped root tables with scoped joins (verifying both the non-awaited and awaited builder pathways).
  • Achieved 100% Statements, 100% Branches, 100% Functions, and 100% Lines coverage.
  • Formatted, linted, typechecked, and built cleanly.
pnpm format:check  ✓
pnpm lint          ✓  (0 warnings, 0 errors)
pnpm typecheck     ✓
pnpm test          ✓  (33/33, +2 new regression tests)
pnpm coverage      ✓  (100% across statement/branch/function/line)
pnpm build         ✓

@benvinegar benvinegar merged commit 0eaa4c6 into main Jun 26, 2026
4 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