From 1ce8a65faf2eb18a49816f98e81922534c86c4df Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 21 Jul 2026 16:12:26 +0000 Subject: [PATCH] refactor(projects): move authz persistence into a repositories layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorganises the authz lib along DDD lines by separating the persistence boundary from the domain: src/lib/projects/authz.ts domain — pure decisions src/app/repositories/projects/authzRepository.ts persistence — Drizzle The repository sits alongside src/app/db/ and mirrors the existing db/schema/projects/ layout, so schema and repository stay symmetrical as Ara Projects grows more of both. Dependencies point one way: the repository imports the domain's fact types and returns them, so the domain never learns a table shape and continues to import no database client. The 34 guard tests still run with no connection. No behaviour change — the queries and guards are untouched. Note this is a new pattern for the codebase: the prevailing convention is a colocated queries.ts/server.ts per lib folder, which 88 files still follow. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../repositories/projects/authzRepository.ts} | 17 +++++++++++++---- src/lib/projects/authz.ts | 16 +++++++--------- 2 files changed, 20 insertions(+), 13 deletions(-) rename src/{lib/projects/authzQueries.ts => app/repositories/projects/authzRepository.ts} (87%) diff --git a/src/lib/projects/authzQueries.ts b/src/app/repositories/projects/authzRepository.ts similarity index 87% rename from src/lib/projects/authzQueries.ts rename to src/app/repositories/projects/authzRepository.ts index fea89557..418f6e55 100644 --- a/src/lib/projects/authzQueries.ts +++ b/src/app/repositories/projects/authzRepository.ts @@ -1,8 +1,17 @@ /** - * DB-backed fact loaders feeding the pure guards in `./authz` (issue #408). + * Ara Projects authorization repository (issue #408). * - * Kept separate from the guards so those stay unit-testable without a database. - * Route handlers load facts here, then decide with `authz`. + * The persistence boundary for authorization: it owns the Drizzle queries and + * returns domain-shaped facts (`ProjectAuthzFacts`, `WorkOrderAuthzFacts`) + * rather than raw rows, so the domain layer never sees a table shape. The + * decisions themselves live in `@/lib/projects/authz`, which imports no + * database client and unit-tests without a connection. + * + * const [user, project] = await Promise.all([ + * loadAuthzUser(userId), + * loadProjectAuthzFacts(projectId), + * ]); + * if (!canViewProject(user, project)) return forbidden(); * * Every loader is a single round trip; none probes per-row inside a list. */ @@ -20,7 +29,7 @@ import type { AuthzUser, ProjectAuthzFacts, WorkOrderAuthzFacts, -} from "./authz"; +} from "@/lib/projects/authz"; /** * The organisations a user belongs to, via `team_members → team → org_id`. diff --git a/src/lib/projects/authz.ts b/src/lib/projects/authz.ts index a65b0588..7fbd15c4 100644 --- a/src/lib/projects/authz.ts +++ b/src/lib/projects/authz.ts @@ -3,16 +3,14 @@ * Ara Projects route handler (issue #408). No route may re-implement any of * this inline. * - * This module is deliberately **pure**: it imports no database client, so the - * guards unit-test without mocking a connection pool. The DB-backed resolvers - * that produce its inputs live next door in `./authzQueries` — a route handler - * loads facts there, then decides here. + * This is the domain layer: deliberately **pure**, importing no database + * client, so the guards unit-test without mocking a connection pool. It owns + * the vocabulary (`ProjectRole`, `ProjectAuthzFacts`) and the decisions. * - * const [user, project] = await Promise.all([ - * loadAuthzUser(userId), - * loadProjectAuthzFacts(projectId), - * ]); - * if (!canViewProject(user, project)) return forbidden(); + * Persistence lives behind the repository at + * `@/app/repositories/projects/authzRepository`, which loads those facts. A + * route handler loads there, then decides here — and the dependency only ever + * points that way, so the domain never learns a table shape. * * Scoping note: Ara Projects are **organisation**-scoped. The existing * `team_portfolio_permissions` machinery is portfolio-scoped and plays no part