feat(ios): add Core Data DAO layer and unit tests
Implement comprehensive data access layer for Core Data entities: - Add NotificationContentDAO, NotificationDeliveryDAO, and NotificationConfigDAO with full CRUD operations and query helpers - Add DailyNotificationDataConversions utility for type conversions (Date ↔ Int64, Int ↔ Int32, JSON, optional strings) - Update PersistenceController with entity verification and migration policies - Add comprehensive unit tests for all DAO classes and data conversions - Update Core Data model with NotificationContent, NotificationDelivery, and NotificationConfig entities (relationships and indexes) - Integrate ReactivationManager into DailyNotificationPlugin.load() DAO Features: - Create/Insert methods with dictionary support - Read/Query methods with predicates (by timesafariDid, notificationType, scheduledTime range, deliveryStatus, etc.) - Update methods (touch, updateDeliveryStatus, recordUserInteraction) - Delete methods (by ID, by key, delete all) - Relationship management (NotificationContent ↔ NotificationDelivery) - Cascade delete support Test Coverage: - 328 lines: DailyNotificationDataConversionsTests (time, numeric, string, JSON) - 490 lines: NotificationContentDAOTests (CRUD, queries, updates) - 415 lines: NotificationDeliveryDAOTests (CRUD, relationships, cascade delete) - 412 lines: NotificationConfigDAOTests (CRUD, queries, active filtering) All tests use in-memory Core Data stack for isolation and speed. Completes sections 4.4, 4.5, and 6.0 of iOS implementation checklist.
This commit is contained in:
@@ -116,9 +116,10 @@ extension History: Identifiable {
|
||||
|
||||
// MARK: - Persistence Controller
|
||||
// Phase 2: CoreData integration for advanced features
|
||||
// Phase 1: Stubbed out - CoreData model not yet created
|
||||
// All entities now available: ContentCache, Schedule, Callback, History,
|
||||
// NotificationContent, NotificationDelivery, NotificationConfig
|
||||
class PersistenceController {
|
||||
// Lazy initialization to prevent Phase 1 errors
|
||||
// Lazy initialization
|
||||
private static var _shared: PersistenceController?
|
||||
static var shared: PersistenceController {
|
||||
if _shared == nil {
|
||||
@@ -131,8 +132,6 @@ class PersistenceController {
|
||||
private var initializationError: Error?
|
||||
|
||||
init(inMemory: Bool = false) {
|
||||
// Phase 1: CoreData model doesn't exist yet, so we'll handle gracefully
|
||||
// Phase 2: Will create DailyNotificationModel.xcdatamodeld
|
||||
var tempContainer: NSPersistentContainer? = nil
|
||||
|
||||
do {
|
||||
@@ -142,12 +141,23 @@ class PersistenceController {
|
||||
tempContainer?.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
|
||||
}
|
||||
|
||||
// Configure persistent store options
|
||||
let description = tempContainer?.persistentStoreDescriptions.first
|
||||
description?.shouldMigrateStoreAutomatically = true
|
||||
description?.shouldInferMappingModelAutomatically = true
|
||||
|
||||
var loadError: Error? = nil
|
||||
tempContainer?.loadPersistentStores { _, error in
|
||||
tempContainer?.loadPersistentStores { description, error in
|
||||
if let error = error as NSError? {
|
||||
loadError = error
|
||||
print("DNP-PLUGIN: CoreData model not found (Phase 1 - expected). Error: \(error.localizedDescription)")
|
||||
print("DNP-PLUGIN: CoreData features will be available in Phase 2")
|
||||
print("DNP-PLUGIN: CoreData store load error: \(error.localizedDescription)")
|
||||
print("DNP-PLUGIN: Error domain: \(error.domain), code: \(error.code)")
|
||||
if let failureReason = error.userInfo[NSLocalizedFailureReasonErrorKey] as? String {
|
||||
print("DNP-PLUGIN: Failure reason: \(failureReason)")
|
||||
}
|
||||
} else {
|
||||
print("DNP-PLUGIN: CoreData store loaded successfully")
|
||||
print("DNP-PLUGIN: Store URL: \(description.url?.absoluteString ?? "unknown")")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +165,14 @@ class PersistenceController {
|
||||
self.initializationError = error
|
||||
self.container = nil
|
||||
} else {
|
||||
tempContainer?.viewContext.automaticallyMergesChangesFromParent = true
|
||||
// Configure view context
|
||||
if let context = tempContainer?.viewContext {
|
||||
context.automaticallyMergesChangesFromParent = true
|
||||
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
||||
|
||||
// Verify all entities are available
|
||||
verifyEntities(in: context)
|
||||
}
|
||||
self.container = tempContainer
|
||||
}
|
||||
} catch {
|
||||
@@ -166,10 +183,88 @@ class PersistenceController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if CoreData is available (Phase 2+)
|
||||
* Check if CoreData is available
|
||||
*/
|
||||
var isAvailable: Bool {
|
||||
return container != nil && initializationError == nil
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the main view context
|
||||
*
|
||||
* @return NSManagedObjectContext or nil if not available
|
||||
*/
|
||||
var viewContext: NSManagedObjectContext? {
|
||||
return container?.viewContext
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new background context for async operations
|
||||
*
|
||||
* @return NSManagedObjectContext or nil if not available
|
||||
*/
|
||||
func newBackgroundContext() -> NSManagedObjectContext? {
|
||||
return container?.newBackgroundContext()
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the view context
|
||||
*
|
||||
* @return true if saved successfully, false otherwise
|
||||
*/
|
||||
func save() -> Bool {
|
||||
guard let context = viewContext else {
|
||||
print("DNP-PLUGIN: Cannot save - CoreData not available")
|
||||
return false
|
||||
}
|
||||
|
||||
if context.hasChanges {
|
||||
do {
|
||||
try context.save()
|
||||
print("DNP-PLUGIN: CoreData context saved successfully")
|
||||
return true
|
||||
} catch {
|
||||
print("DNP-PLUGIN: Error saving CoreData context: \(error.localizedDescription)")
|
||||
context.rollback()
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify all entities are available in the model
|
||||
*
|
||||
* @param context Managed object context
|
||||
*/
|
||||
private func verifyEntities(in context: NSManagedObjectContext) {
|
||||
guard let model = context.persistentStoreCoordinator?.managedObjectModel else {
|
||||
print("DNP-PLUGIN: Cannot verify entities - no managed object model")
|
||||
return
|
||||
}
|
||||
|
||||
let entityNames = [
|
||||
"ContentCache",
|
||||
"Schedule",
|
||||
"Callback",
|
||||
"History",
|
||||
"NotificationContent",
|
||||
"NotificationDelivery",
|
||||
"NotificationConfig"
|
||||
]
|
||||
|
||||
var missingEntities: [String] = []
|
||||
for entityName in entityNames {
|
||||
if model.entitiesByName[entityName] == nil {
|
||||
missingEntities.append(entityName)
|
||||
}
|
||||
}
|
||||
|
||||
if missingEntities.isEmpty {
|
||||
print("DNP-PLUGIN: All \(entityNames.count) entities verified in CoreData model")
|
||||
} else {
|
||||
print("DNP-PLUGIN: WARNING - Missing entities: \(missingEntities.joined(separator: ", "))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user