/** * @factory/shared — the org-scoped AUDIT-LOG query builders (TASK-0133). * * Single source of truth for "which AuditLog rows belong to an org". TWO * consumers must agree on the scope by construction (a divergence would mean * the viewer shows rows the retention engine purges, or vice versa): * * 1. the API's org audit viewer/export (`apps/api/src/modules/org/audit.service.ts`) * 2. the worker's retention purge (`apps/worker/src/processors/retention.ts`) * + async CSV export (`audit.export` maintenance job) * * The builders return PLAIN Prisma-`where`-shaped objects (no Prisma import — * `@factory/shared` must not depend on the API): * * Org rows = * - rows in ANY of the org's workspaces (`workspaceId IN wsIds`), PLUS * - ORG-LEVEL rows: `workspaceId IS NULL` and (metadata.orgId = orgId — * the SSO/SCIM admin + SSO-login emitters) OR (resourceType * "organization"|"org" and resourceId = orgId — org.created/org.updated * + org-admin policy/member/retention emitters). * * Recorded decision: NO new `AuditLog` index — EXPLAIN on the live 12k-row * table uses the existing `audit_logs_workspaceId_action_createdAt_idx` * (index scan, no seq scan at the filter combinations the viewer allows). */ /** * The viewer's filter set. `| undefined` on every optional (the worker * compiles with `exactOptionalPropertyTypes: true` and must be able to * build these objects from parsed, possibly-absent values). */ export interface AuditFilters { actorId?: string | undefined; actorType?: string | undefined; /** Exact action match. */ action?: string | undefined; /** `action LIKE 'prefix%'` (e.g. `page.`). */ actionPrefix?: string | undefined; resourceType?: string | undefined; workspaceId?: string | undefined; from?: Date | undefined; to?: Date | undefined; } /** The documented OR-branch org scope (see the header). */ export function auditOrgScopeWhere(orgId: string, wsIds: string[]): Record { return { OR: [ { workspaceId: { in: wsIds } }, { workspaceId: null, OR: [ // SSO/SCIM org-level rows carry the org only in `metadata.orgId` // (resourceType is "user" there). Prisma's `JsonFilter` needs // `{ path, equals }` — a bare `{ orgId }` object is rejected at // runtime ("Invalid database query"). { metadata: { path: ["orgId"], equals: orgId } }, { resourceType: { in: ["organization", "org"] }, resourceId: orgId } ] } ] }; } /** The viewer's user-facing filters (empty input → empty clause). */ export function auditFilterWhere(f: AuditFilters): Record { return { ...(f.actorId ? { actorId: f.actorId } : {}), ...(f.actorType ? { actorType: f.actorType } : {}), ...(f.action ? { action: f.action } : {}), ...(f.actionPrefix ? { action: { startsWith: f.actionPrefix } } : {}), ...(f.resourceType ? { resourceType: f.resourceType } : {}), ...(f.workspaceId ? { workspaceId: f.workspaceId } : {}), ...(f.from || f.to ? { createdAt: { ...(f.from ? { gte: f.from } : {}), ...(f.to ? { lte: f.to } : {}) } } : {}) }; } /** Org scope ∧ user filters (the shape both consumers pass to Prisma). */ export function auditScopedWhere( orgId: string, wsIds: string[], f: AuditFilters ): Record { return { AND: [auditOrgScopeWhere(orgId, wsIds), auditFilterWhere(f)] }; }