Skip to content

Commit 43fa5ea

Browse files
feat(data-retention): workspace-level overrides for retention and PII (#5186)
* feat(data-retention): workspace-level overrides for retention and PII * fix(data-retention): hide unmanageable PII rows when flag off, scope override workspace IDs to org, dedupe key type * improvement(data-retention): unify org default and workspace overrides into one policy list * fix(data-retention): clean up overrides for workspaces deselected during edit
1 parent c20d5fc commit 43fa5ea

9 files changed

Lines changed: 736 additions & 294 deletions

File tree

apps/sim/app/api/organizations/[id]/data-retention/route.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
22
import { db } from '@sim/db'
33
import type { DataRetentionSettings } from '@sim/db/schema'
4-
import { member, organization } from '@sim/db/schema'
4+
import { member, organization, workspace } from '@sim/db/schema'
55
import { createLogger } from '@sim/logger'
6-
import { and, eq } from 'drizzle-orm'
6+
import { and, eq, inArray } from 'drizzle-orm'
77
import { type NextRequest, NextResponse } from 'next/server'
88
import {
99
type OrganizationRetentionValues,
@@ -26,6 +26,7 @@ function enterpriseDefaults(): OrganizationRetentionValues {
2626
softDeleteRetentionHours: CLEANUP_CONFIG['cleanup-soft-deletes'].defaults.enterprise,
2727
taskCleanupHours: CLEANUP_CONFIG['cleanup-tasks'].defaults.enterprise,
2828
piiRedaction: null,
29+
retentionOverrides: null,
2930
}
3031
}
3132

@@ -44,6 +45,7 @@ function normalizeConfigured(
4445
})),
4546
}
4647
: null,
48+
retentionOverrides: settings?.retentionOverrides ?? null,
4749
}
4850
}
4951

@@ -187,6 +189,32 @@ export const PUT = withRouteHandler(
187189
}
188190
merged.piiRedaction = body.piiRedaction
189191
}
192+
if (body.retentionOverrides !== undefined) {
193+
merged.retentionOverrides = body.retentionOverrides
194+
}
195+
196+
const targetedWorkspaceIds = new Set<string>()
197+
for (const override of body.retentionOverrides ?? []) {
198+
targetedWorkspaceIds.add(override.workspaceId)
199+
}
200+
for (const rule of body.piiRedaction?.rules ?? []) {
201+
if (rule.workspaceId) targetedWorkspaceIds.add(rule.workspaceId)
202+
}
203+
if (targetedWorkspaceIds.size > 0) {
204+
const ids = [...targetedWorkspaceIds]
205+
const orgWorkspaces = await db
206+
.select({ id: workspace.id })
207+
.from(workspace)
208+
.where(and(eq(workspace.organizationId, organizationId), inArray(workspace.id, ids)))
209+
const known = new Set(orgWorkspaces.map((row) => row.id))
210+
const unknown = ids.filter((id) => !known.has(id))
211+
if (unknown.length > 0) {
212+
return NextResponse.json(
213+
{ error: `Override targets workspaces outside this organization: ${unknown.join(', ')}` },
214+
{ status: 400 }
215+
)
216+
}
217+
}
190218

191219
const [updated] = await db
192220
.update(organization)

0 commit comments

Comments
 (0)