Skip to content

Commit a3c1bab

Browse files
fix migration
1 parent ffe4de8 commit a3c1bab

File tree

21 files changed

+381
-15476
lines changed

21 files changed

+381
-15476
lines changed
Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { verifyCronAuth } from '@/lib/auth/internal'
4-
import { getJobQueue } from '@/lib/core/async-jobs'
4+
import { dispatchCleanupJobs } from '@/lib/billing/cleanup-dispatcher'
55

66
export const dynamic = 'force-dynamic'
77

@@ -12,14 +12,13 @@ export async function GET(request: NextRequest) {
1212
const authError = verifyCronAuth(request, 'soft-delete cleanup')
1313
if (authError) return authError
1414

15-
const jobQueue = await getJobQueue()
16-
const jobId = await jobQueue.enqueue('cleanup-soft-deletes', {})
15+
const result = await dispatchCleanupJobs('cleanup-soft-deletes', 'softDeleteRetentionHours')
1716

18-
logger.info('Soft-delete cleanup job dispatched', { jobId })
17+
logger.info('Soft-delete cleanup jobs dispatched', result)
1918

20-
return NextResponse.json({ triggered: true, jobId })
19+
return NextResponse.json({ triggered: true, ...result })
2120
} catch (error) {
22-
logger.error('Failed to dispatch soft-delete cleanup job:', { error })
21+
logger.error('Failed to dispatch soft-delete cleanup jobs:', { error })
2322
return NextResponse.json({ error: 'Failed to dispatch soft-delete cleanup' }, { status: 500 })
2423
}
2524
}
Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { verifyCronAuth } from '@/lib/auth/internal'
4-
import { getJobQueue } from '@/lib/core/async-jobs'
4+
import { dispatchCleanupJobs } from '@/lib/billing/cleanup-dispatcher'
55

66
export const dynamic = 'force-dynamic'
77

@@ -12,14 +12,13 @@ export async function GET(request: NextRequest) {
1212
const authError = verifyCronAuth(request, 'task cleanup')
1313
if (authError) return authError
1414

15-
const jobQueue = await getJobQueue()
16-
const jobId = await jobQueue.enqueue('cleanup-tasks', {})
15+
const result = await dispatchCleanupJobs('cleanup-tasks', 'taskCleanupHours')
1716

18-
logger.info('Task cleanup job dispatched', { jobId })
17+
logger.info('Task cleanup jobs dispatched', result)
1918

20-
return NextResponse.json({ triggered: true, jobId })
19+
return NextResponse.json({ triggered: true, ...result })
2120
} catch (error) {
22-
logger.error('Failed to dispatch task cleanup job:', { error })
21+
logger.error('Failed to dispatch task cleanup jobs:', { error })
2322
return NextResponse.json({ error: 'Failed to dispatch task cleanup' }, { status: 500 })
2423
}
2524
}

apps/sim/app/api/cron/redact-task-context/route.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.
Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { verifyCronAuth } from '@/lib/auth/internal'
4-
import { getJobQueue } from '@/lib/core/async-jobs'
4+
import { dispatchCleanupJobs } from '@/lib/billing/cleanup-dispatcher'
55

66
export const dynamic = 'force-dynamic'
77

@@ -12,14 +12,13 @@ export async function GET(request: NextRequest) {
1212
const authError = verifyCronAuth(request, 'logs cleanup')
1313
if (authError) return authError
1414

15-
const jobQueue = await getJobQueue()
16-
const jobId = await jobQueue.enqueue('cleanup-logs', {})
15+
const result = await dispatchCleanupJobs('cleanup-logs', 'logRetentionHours')
1716

18-
logger.info('Log cleanup job dispatched', { jobId })
17+
logger.info('Log cleanup jobs dispatched', result)
1918

20-
return NextResponse.json({ triggered: true, jobId })
19+
return NextResponse.json({ triggered: true, ...result })
2120
} catch (error) {
22-
logger.error('Failed to dispatch log cleanup job:', { error })
21+
logger.error('Failed to dispatch log cleanup jobs:', { error })
2322
return NextResponse.json({ error: 'Failed to dispatch log cleanup' }, { status: 500 })
2423
}
2524
}

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

Lines changed: 10 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
77
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
88
import { getSession } from '@/lib/auth'
9+
import { getRetentionDefaultHours } from '@/lib/billing/cleanup-dispatcher'
910
import { getHighestPrioritySubscription } from '@/lib/billing/core/plan'
1011
import { isEnterprisePlan } from '@/lib/billing/core/subscription'
1112
import {
@@ -21,40 +22,19 @@ const logger = createLogger('DataRetentionAPI')
2122
const MIN_HOURS = 24
2223
const MAX_HOURS = 43800 // 5 years
2324

24-
const FREE_LOG_RETENTION_HOURS = 7 * 24
25-
const FREE_SOFT_DELETE_RETENTION_HOURS = 7 * 24
26-
const FREE_TASK_REDACTION_HOURS = null // never
27-
const FREE_TASK_CLEANUP_HOURS = null // never
28-
29-
const PRO_LOG_RETENTION_HOURS = 30 * 24
30-
const PRO_SOFT_DELETE_RETENTION_HOURS = 30 * 24
31-
const PRO_TASK_REDACTION_HOURS = 30 * 24
32-
const PRO_TASK_CLEANUP_HOURS = null // never
33-
3425
interface PlanDefaults {
35-
logRetentionHours: number
36-
softDeleteRetentionHours: number
37-
taskRedactionHours: number | null
26+
logRetentionHours: number | null
27+
softDeleteRetentionHours: number | null
3828
taskCleanupHours: number | null
3929
}
4030

4131
function getPlanDefaults(plan: 'free' | 'pro' | 'enterprise'): PlanDefaults {
42-
switch (plan) {
43-
case 'enterprise':
44-
case 'pro':
45-
return {
46-
logRetentionHours: PRO_LOG_RETENTION_HOURS,
47-
softDeleteRetentionHours: PRO_SOFT_DELETE_RETENTION_HOURS,
48-
taskRedactionHours: PRO_TASK_REDACTION_HOURS,
49-
taskCleanupHours: PRO_TASK_CLEANUP_HOURS,
50-
}
51-
default:
52-
return {
53-
logRetentionHours: FREE_LOG_RETENTION_HOURS,
54-
softDeleteRetentionHours: FREE_SOFT_DELETE_RETENTION_HOURS,
55-
taskRedactionHours: FREE_TASK_REDACTION_HOURS,
56-
taskCleanupHours: FREE_TASK_CLEANUP_HOURS,
57-
}
32+
const tier: 'free' | 'paid' | 'enterprise' =
33+
plan === 'free' ? 'free' : plan === 'enterprise' ? 'enterprise' : 'paid'
34+
return {
35+
logRetentionHours: getRetentionDefaultHours(tier, 'logRetentionHours'),
36+
softDeleteRetentionHours: getRetentionDefaultHours(tier, 'softDeleteRetentionHours'),
37+
taskCleanupHours: getRetentionDefaultHours(tier, 'taskCleanupHours'),
5838
}
5939
}
6040

@@ -71,7 +51,6 @@ async function resolveWorkspacePlan(
7151
const updateRetentionSchema = z.object({
7252
logRetentionHours: z.number().int().min(MIN_HOURS).max(MAX_HOURS).nullable().optional(),
7353
softDeleteRetentionHours: z.number().int().min(MIN_HOURS).max(MAX_HOURS).nullable().optional(),
74-
taskRedactionHours: z.number().int().min(MIN_HOURS).max(MAX_HOURS).nullable().optional(),
7554
taskCleanupHours: z.number().int().min(MIN_HOURS).max(MAX_HOURS).nullable().optional(),
7655
})
7756

@@ -98,7 +77,6 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{
9877
.select({
9978
logRetentionHours: workspace.logRetentionHours,
10079
softDeleteRetentionHours: workspace.softDeleteRetentionHours,
101-
taskRedactionHours: workspace.taskRedactionHours,
10280
taskCleanupHours: workspace.taskCleanupHours,
10381
billedAccountUserId: workspace.billedAccountUserId,
10482
})
@@ -123,20 +101,17 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{
123101
configured: {
124102
logRetentionHours: ws.logRetentionHours,
125103
softDeleteRetentionHours: ws.softDeleteRetentionHours,
126-
taskRedactionHours: ws.taskRedactionHours,
127104
taskCleanupHours: ws.taskCleanupHours,
128105
},
129106
effective: isEnterpriseWorkspace
130107
? {
131108
logRetentionHours: ws.logRetentionHours,
132109
softDeleteRetentionHours: ws.softDeleteRetentionHours,
133-
taskRedactionHours: ws.taskRedactionHours,
134110
taskCleanupHours: ws.taskCleanupHours,
135111
}
136112
: {
137113
logRetentionHours: defaults.logRetentionHours,
138114
softDeleteRetentionHours: defaults.softDeleteRetentionHours,
139-
taskRedactionHours: defaults.taskRedactionHours,
140115
taskCleanupHours: defaults.taskCleanupHours,
141116
},
142117
},
@@ -196,10 +171,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
196171
if (parsed.data.softDeleteRetentionHours !== undefined) {
197172
updateData.softDeleteRetentionHours = parsed.data.softDeleteRetentionHours
198173
}
199-
if (parsed.data.taskRedactionHours !== undefined) {
200-
updateData.taskRedactionHours = parsed.data.taskRedactionHours
201-
}
202-
if (parsed.data.taskCleanupHours !== undefined) {
174+
if (parsed.data.taskCleanupHours !== undefined) {
203175
updateData.taskCleanupHours = parsed.data.taskCleanupHours
204176
}
205177

@@ -210,7 +182,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
210182
.returning({
211183
logRetentionHours: workspace.logRetentionHours,
212184
softDeleteRetentionHours: workspace.softDeleteRetentionHours,
213-
taskRedactionHours: workspace.taskRedactionHours,
214185
taskCleanupHours: workspace.taskCleanupHours,
215186
})
216187

@@ -242,13 +213,11 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
242213
configured: {
243214
logRetentionHours: updated.logRetentionHours,
244215
softDeleteRetentionHours: updated.softDeleteRetentionHours,
245-
taskRedactionHours: updated.taskRedactionHours,
246216
taskCleanupHours: updated.taskCleanupHours,
247217
},
248218
effective: {
249219
logRetentionHours: updated.logRetentionHours,
250220
softDeleteRetentionHours: updated.softDeleteRetentionHours,
251-
taskRedactionHours: updated.taskRedactionHours,
252221
taskCleanupHours: updated.taskCleanupHours,
253222
},
254223
},

apps/sim/background/cleanup-logs.ts

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { db } from '@sim/db'
2-
import { workflowExecutionLogs } from '@sim/db/schema'
2+
import { workflowExecutionLogs, workspace } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { task } from '@trigger.dev/sdk'
5-
import { and, inArray, lt } from 'drizzle-orm'
5+
import { and, eq, inArray, lt } from 'drizzle-orm'
66
import { snapshotService } from '@/lib/logs/execution/snapshot/service'
7-
import { resolveRetentionGroups } from '@/lib/retention/workspace-tiers'
7+
import {
8+
type CleanupJobPayload,
9+
getRetentionDefaultHours,
10+
resolveTierWorkspaceIds,
11+
} from '@/lib/billing/cleanup-dispatcher'
812
import { isUsingCloudStorage, StorageService } from '@/lib/uploads'
913

1014
const logger = createLogger('CleanupLogs')
@@ -102,36 +106,79 @@ async function cleanupTier(
102106
return results
103107
}
104108

105-
export const cleanupLogsTask = task({
106-
id: 'cleanup-logs',
107-
run: async () => {
108-
const startTime = Date.now()
109+
async function resolvePayload(payload: CleanupJobPayload): Promise<{
110+
workspaceIds: string[]
111+
retentionHours: number
112+
tierLabel: string
113+
} | null> {
114+
if (payload.tier === 'free' || payload.tier === 'paid') {
115+
const retentionHours = getRetentionDefaultHours(payload.tier, 'logRetentionHours')
116+
if (retentionHours === null) {
117+
logger.info(`[${payload.tier}] No default retention, skipping`)
118+
return null
119+
}
120+
const workspaceIds = await resolveTierWorkspaceIds(payload.tier)
121+
return { workspaceIds, retentionHours, tierLabel: payload.tier }
122+
}
123+
124+
// enterprise
125+
const [ws] = await db
126+
.select({ logRetentionHours: workspace.logRetentionHours })
127+
.from(workspace)
128+
.where(eq(workspace.id, payload.workspaceId))
129+
.limit(1)
109130

110-
logger.info('Starting log cleanup task')
131+
if (!ws?.logRetentionHours) {
132+
logger.info(`[enterprise/${payload.workspaceId}] No retention configured, skipping`)
133+
return null
134+
}
135+
136+
return {
137+
workspaceIds: [payload.workspaceId],
138+
retentionHours: ws.logRetentionHours,
139+
tierLabel: `enterprise/${payload.workspaceId}`,
140+
}
141+
}
111142

112-
const groups = await resolveRetentionGroups('logRetentionHours')
143+
export async function runCleanupLogs(payload: CleanupJobPayload): Promise<void> {
144+
const startTime = Date.now()
113145

114-
for (const group of groups) {
115-
const results = await cleanupTier(group.workspaceIds, group.retentionDate, group.tierLabel)
116-
logger.info(`[${group.tierLabel}] Result: ${results.deleted} deleted, ${results.deleteFailed} failed out of ${results.total} candidates`)
117-
}
146+
const resolved = await resolvePayload(payload)
147+
if (!resolved) return
148+
149+
const { workspaceIds, retentionHours, tierLabel } = resolved
118150

119-
// Snapshot cleanup — use shortest retention + 1 day
151+
if (workspaceIds.length === 0) {
152+
logger.info(`[${tierLabel}] No workspaces to process`)
153+
return
154+
}
155+
156+
const retentionDate = new Date(Date.now() - retentionHours * 60 * 60 * 1000)
157+
logger.info(
158+
`[${tierLabel}] Cleaning ${workspaceIds.length} workspaces, cutoff: ${retentionDate.toISOString()}`
159+
)
160+
161+
const results = await cleanupTier(workspaceIds, retentionDate, tierLabel)
162+
logger.info(
163+
`[${tierLabel}] Result: ${results.deleted} deleted, ${results.deleteFailed} failed out of ${results.total} candidates`
164+
)
165+
166+
// Snapshot cleanup runs only on the free job to avoid running it N times for N enterprise workspaces.
167+
if (payload.tier === 'free') {
120168
try {
121-
const shortestDays = Math.min(
122-
...groups.map((g) => (Date.now() - g.retentionDate.getTime()) / (24 * 60 * 60 * 1000))
123-
)
124-
if (Number.isFinite(shortestDays)) {
125-
const snapshotsCleaned = await snapshotService.cleanupOrphanedSnapshots(
126-
Math.floor(shortestDays) + 1
127-
)
128-
logger.info(`Cleaned up ${snapshotsCleaned} orphaned snapshots`)
129-
}
169+
const retentionDays = Math.floor(retentionHours / 24)
170+
const snapshotsCleaned = await snapshotService.cleanupOrphanedSnapshots(retentionDays + 1)
171+
logger.info(`Cleaned up ${snapshotsCleaned} orphaned snapshots`)
130172
} catch (snapshotError) {
131173
logger.error('Error cleaning up orphaned snapshots:', { snapshotError })
132174
}
175+
}
176+
177+
const timeElapsed = (Date.now() - startTime) / 1000
178+
logger.info(`[${tierLabel}] Job completed in ${timeElapsed.toFixed(2)}s`)
179+
}
133180

134-
const timeElapsed = (Date.now() - startTime) / 1000
135-
logger.info(`Log cleanup task completed in ${timeElapsed.toFixed(2)}s`)
136-
},
181+
export const cleanupLogsTask = task({
182+
id: 'cleanup-logs',
183+
run: runCleanupLogs,
137184
})

0 commit comments

Comments
 (0)