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

@@ -41,6 +41,7 @@ import {
type FolderNode,
type EntityType,
} from '@/lib/services/document-folders.service';
import { assertEntityInPort, collectRelatedEntities } from '@/lib/services/files';
import type {
CreateDocumentInput,
UpdateDocumentInput,
@@ -1822,8 +1823,6 @@ export async function createFromUpload(
// ─── Aggregated Workflow Projection ───────────────────────────────────────────
import { collectRelatedEntities } from '@/lib/services/files';
export interface AggregatedWorkflowGroup {
label: string;
source: 'direct' | 'client' | 'company' | 'yacht';
@@ -1844,6 +1843,9 @@ export async function listInflightWorkflowsAggregatedByEntity(
entityType: 'client' | 'company' | 'yacht',
entityId: string,
): Promise<{ groups: AggregatedWorkflowGroup[] }> {
const entityExists = await assertEntityInPort(portId, entityType, entityId);
if (!entityExists) return { groups: [] };
const related = await collectRelatedEntities(portId, entityType, entityId);
const groups: AggregatedWorkflowGroup[] = [];
@@ -1910,11 +1912,7 @@ async function fetchWorkflowGroupRows(
.select()
.from(documents)
.where(
and(
eq(documents.portId, portId),
inArray(documents.status, inflightStatuses),
predicate,
),
and(eq(documents.portId, portId), inArray(documents.status, inflightStatuses), predicate),
)
.orderBy(desc(documents.updatedAt))
.limit(WORKFLOW_GROUP_LIMIT);
@@ -1923,11 +1921,7 @@ async function fetchWorkflowGroupRows(
.select({ count: sql<number>`count(*)::int` })
.from(documents)
.where(
and(
eq(documents.portId, portId),
inArray(documents.status, inflightStatuses),
predicate,
),
and(eq(documents.portId, portId), inArray(documents.status, inflightStatuses), predicate),
);
return { rows, total: Number(countRow?.count ?? 0) };