Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CalenTask/App/AppShellView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ struct AppShellView: View {
reportFailure("Seed failed: \(error)")
}
SmokeSeeder.seedIfRequested(in: modelContext)
TrashService.purgeExpired(in: modelContext)
WidgetBridge.refresh(in: modelContext)
WeatherService.shared.refreshIfNeeded()
if !didShowWelcome {
Expand Down
30 changes: 30 additions & 0 deletions CalenTask/Services/TrashService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation
import SwiftData

/// #22 — Cestino: elimina in modo definitivo le attività cancellate (soft
/// delete) da più di `retentionDays`, così DB e CloudKit non crescono per sempre.
enum TrashService {
static let retentionDays = 30

/// Hard delete delle task con `deletedAt` oltre la soglia. Salta quelle che
/// hanno ancora sotto-attività vive (non si orfanano figli). Ritorna quante
/// ne ha eliminate.
@MainActor
@discardableResult
static func purgeExpired(in context: ModelContext, now: Date = .now) -> Int {
guard let cutoff = Calendar.app.date(byAdding: .day, value: -retentionDays, to: now) else { return 0 }
let predicate = #Predicate<TodoTask> { task in
task.deletedAt != nil
}
// Il cestino è piccolo: il filtro sulla soglia avviene in memoria.
let expired = context.fetchOrLog(FetchDescriptor<TodoTask>(predicate: predicate))
let purgeable = expired.filter { task in
guard let deletedAt = task.deletedAt else { return false }
return deletedAt < cutoff && task.liveSubtasks.isEmpty
}
guard !purgeable.isEmpty else { return 0 }
purgeable.forEach { context.delete($0) }
context.saveOrLog()
return purgeable.count
}
}
38 changes: 38 additions & 0 deletions CalenTaskTests/TrashServiceTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Foundation
import SwiftData
import Testing
@testable import CalenTask

/// #22 — `TrashService.purgeExpired`: elimina solo ciò che è nel cestino da oltre 30 giorni.
@MainActor
struct TrashServiceTests {

private func makeContext() throws -> ModelContext {
let schema = Schema([
Workspace.self, Membership.self, UserProfile.self,
Project.self, TodoTask.self, TaskDependency.self, Tag.self,
CustomFieldDefinition.self, CustomFieldValue.self, SavedView.self,
WorkflowStage.self, AutomationRule.self, Attachment.self,
Contact.self, CrewAssignment.self, ProductionScene.self,
])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
return ModelContext(try ModelContainer(for: schema, configurations: [config]))
}

@Test func purgesOnlyExpiredDeletedTasks() throws {
let context = try makeContext()
let now = Date(timeIntervalSince1970: 10_000_000)
let author = UUID(), ws = UUID()
let old = TodoTask(workspaceID: ws, title: "Vecchia", createdByID: author)
old.deletedAt = now.addingTimeInterval(-31 * 86_400)
let recent = TodoTask(workspaceID: ws, title: "Recente", createdByID: author)
recent.deletedAt = now.addingTimeInterval(-5 * 86_400)
let live = TodoTask(workspaceID: ws, title: "Viva", createdByID: author)
[old, recent, live].forEach { context.insert($0) }
try context.save()

#expect(TrashService.purgeExpired(in: context, now: now) == 1)
let titles = try context.fetch(FetchDescriptor<TodoTask>()).map(\.title).sorted()
#expect(titles == ["Recente", "Viva"])
}
}
Loading