fix(documents): tighten aggregation — filter ended memberships + symmetry

Four follow-ups from Task 8 code review:
1. Aggregation now filters companyMemberships to active rows only
   (isNull(endDate)) on both client→companies and company→clients
   joins. Previously a rep who left a company 2y ago would still
   see that company's files in their aggregated view. Brings this
   service in line with the 8 other call sites in the codebase that
   already filter on endDate.
2. Move collectRelatedEntities import to the top of
   documents.service.ts — was wedged mid-file.
3. listInflightWorkflowsAggregatedByEntity now calls
   assertEntityInPort for symmetry with the files version. Cross-
   port reads short-circuit early instead of executing N empty
   port-scoped queries.
4. Add a cross-port leakage regression test for the workflow
   projection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 12:02:33 +02:00
parent 3037d832c6
commit d2b0d42e84
3 changed files with 29 additions and 31 deletions

View File

@@ -1,4 +1,4 @@
import { and, arrayContains, desc, eq, inArray, or, sql } from 'drizzle-orm';
import { and, arrayContains, desc, eq, inArray, isNull, or, sql } from 'drizzle-orm';
import { db } from '@/lib/db';
import { files, documents } from '@/lib/db/schema/documents';
@@ -364,7 +364,7 @@ export async function listFilesAggregatedByEntity(
return { groups };
}
async function assertEntityInPort(
export async function assertEntityInPort(
portId: string,
entityType: EntityType,
entityId: string,
@@ -415,7 +415,9 @@ export async function collectRelatedEntities(
companies,
and(eq(companies.id, companyMemberships.companyId), eq(companies.portId, portId)),
)
.where(eq(companyMemberships.clientId, entityId));
.where(
and(eq(companyMemberships.clientId, entityId), isNull(companyMemberships.endDate)),
);
const directYachts = await db
.select({ id: yachts.id, name: yachts.name })
@@ -461,7 +463,9 @@ export async function collectRelatedEntities(
clients,
and(eq(clients.id, companyMemberships.clientId), eq(clients.portId, portId)),
)
.where(eq(companyMemberships.companyId, entityId));
.where(
and(eq(companyMemberships.companyId, entityId), isNull(companyMemberships.endDate)),
);
const ownedYachts = await db
.select({ id: yachts.id, name: yachts.name })
@@ -561,10 +565,7 @@ export async function applyEntityFkFromFolder<
if (!payload.folderId) return payload;
const folder = await db.query.documentFolders.findFirst({
where: and(
eq(documentFolders.id, payload.folderId),
eq(documentFolders.portId, portId),
),
where: and(eq(documentFolders.id, payload.folderId), eq(documentFolders.portId, portId)),
columns: { systemManaged: true, entityType: true, entityId: true },
});