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:
@@ -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) };
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user