import { and, eq, inArray, or } from "drizzle-orm"; import { projectRoots, projects, timelineEvents } from "../db/schema"; import type { CapabilityContext } from "./context"; /** * Build the org-scoping filter for timeline_events queries, matching the * behavior the hub chat route used inline: * * - no organizationId → undefined (local mode, no filter) * - org + empty devices → events belonging to the org through their project * - org + devices → events matching a device or an org-owned project * - org + missing devices → throws (fail closed: an org context with no * resolved device list must never fall through to an unscoped query, which * would leak every org's events across tenants) */ export function buildTimelineScope(ctx: CapabilityContext) { if (!ctx.organizationId) return undefined; if (ctx.deviceIds) { const orgProjectRootIds = ctx.db .select({ id: projectRoots.id }) .from(projectRoots) .innerJoin(projects, eq(projects.id, projectRoots.projectId)) .where(and(eq(projects.organizationId, ctx.organizationId))); return or( inArray(timelineEvents.deviceId, ctx.deviceIds), inArray(timelineEvents.projectRootId, orgProjectRootIds), ); } throw new Error( "buildTimelineScope: organizationId set but deviceIds missing — refusing to run unscoped", ); }