Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: core data [8] #96

Draft
wants to merge 3 commits into
base: refactor/json
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright © 2023 Optimove. All rights reserved.

import CoreData
import Foundation

final class OptistreamPersistentContainerConfigurator: PersistentContainerConfigurator {
enum Constants {
static let modelName = "OptistreamQueue"
static let folderName = "com.optimove.sdk.no-backup"
}

let version: CoreDataMigrationVersion

init(version: CoreDataMigrationVersion = .current) {
self.version = version
}

let folderName: String? = Constants.folderName
let modelName: String = Constants.modelName
var managedObjectModel: ManagedObjectModel {
CoreDataModelDescription.makeOptistreamEventModel(version: version)
}

var location: FileManagerLocation = .libraryDirectory
}
90 changes: 74 additions & 16 deletions OptimoveSDK/Sources/Classes/CoreData/PersistentContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,31 @@ final class PersistentContainer: NSPersistentContainer {
}
}

private enum Constants {
static let modelName = "OptistreamQueue"
static let folderName = "com.optimove.sdk.no-backup"
}

private let migrator: CoreDataMigratorProtocol
private let persistentContainerConfigurator: PersistentContainerConfigurator
private let storeType: PersistentStoreType

init(
modelName: String = Constants.modelName,
version: CoreDataMigrationVersion = .current,
persistentContainerConfigurator: PersistentContainerConfigurator,
migrator: CoreDataMigratorProtocol = CoreDataMigrator(),
storeType: PersistentStoreType = .sql
) {
let mom = CoreDataModelDescription.makeOptistreamEventModel(version: version)
self.migrator = migrator
self.storeType = storeType
super.init(name: modelName, managedObjectModel: mom)
self.persistentContainerConfigurator = persistentContainerConfigurator
super.init(
name: persistentContainerConfigurator.modelName,
managedObjectModel: persistentContainerConfigurator.managedObjectModel
)
}

func loadPersistentStores(storeName: String) throws {
guard !isThisStoreAlreadyLoaded(storeName: storeName) else { return }
let persistentStoreDescription = NSPersistentStoreDescription()
persistentStoreDescription.type = storeType.coreDataValue
persistentStoreDescription.url = try FileManager.default.defineStoreURL(
folderName: Constants.folderName,
location: persistentContainerConfigurator.location,
folderName: persistentContainerConfigurator.folderName,
storeName: storeName
)
persistentStoreDescription.shouldMigrateStoreAutomatically = false
Expand Down Expand Up @@ -90,14 +89,32 @@ final class PersistentContainer: NSPersistentContainer {
}

extension FileManager {
func defineStoreURL(folderName: String, storeName: String) throws -> URL {
let libraryDirectory = try unwrap(urls(for: .libraryDirectory, in: .userDomainMask).first)
let libraryStoreDirectoryURL = try unwrap(libraryDirectory.appendingPathComponent(folderName))
let storeURL = try unwrap(libraryStoreDirectoryURL.appendingPathComponent("\(storeName).sqlite"))
guard !directoryExists(atUrl: libraryStoreDirectoryURL, isDirectory: true) else {
func defineLocation(_ location: FileManagerLocation) throws -> URL {
switch location {
case let .appGroupDirectory(url):
return url
case .libraryDirectory:
return try unwrap(urls(for: .libraryDirectory, in: .userDomainMask).first)
}
}

func defineStoreURL(
location: FileManagerLocation,
folderName: String?,
storeName: String
) throws -> URL {
let storeFolderURL = try {
let locationURL = try defineLocation(location)
if let folderName = folderName {
return try unwrap(locationURL.appendingPathComponent(folderName))
}
return locationURL
}()
let storeURL = try unwrap(storeFolderURL.appendingPathComponent("\(storeName).sqlite"))
guard !directoryExists(atUrl: storeFolderURL, isDirectory: true) else {
return try addSkipBackupAttributeToItemAtURL(url: storeURL)
}
try createDirectory(at: libraryStoreDirectoryURL, withIntermediateDirectories: true)
try createDirectory(at: storeFolderURL, withIntermediateDirectories: true)
return try addSkipBackupAttributeToItemAtURL(url: storeURL)
}

Expand Down Expand Up @@ -129,6 +146,10 @@ extension CoreDataModelDescription {
}
}

static func makeAnalyticsEventModel() -> NSManagedObjectModel {
return makeAnalyticsEventModelv1()
}

private static func makeOptistreamEventModelv1() -> NSManagedObjectModel {
let modelDescription = CoreDataModelDescription(
entities: [
Expand Down Expand Up @@ -170,4 +191,41 @@ extension CoreDataModelDescription {
)
return modelDescription.makeModel()
}

private static func makeAnalyticsEventModelv1() -> NSManagedObjectModel {
let modelDescription = CoreDataModelDescription(
entities: [
.entity(
name: KSEventModel.entityName,
managedObjectClass: KSEventModel.self,
attributes: [
.attribute(
name: #keyPath(KSEventModel.eventType),
type: .stringAttributeType
),
.attribute(
name: #keyPath(KSEventModel.happenedAt),
type: .integer64AttributeType,
defaultValue: 0
),
.attribute(
name: #keyPath(KSEventModel.properties),
type: .binaryDataAttributeType,
isOptional: true
),
.attribute(
name: #keyPath(KSEventModel.uuid),
type: .stringAttributeType
),
.attribute(
name: #keyPath(KSEventModel.userIdentifier),
type: .stringAttributeType,
isOptional: true
),
]
),
]
)
return modelDescription.makeModel()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright © 2023 Optimove. All rights reserved.

import CoreData
import Foundation
import OptimoveCore

typealias ManagedObjectModel = NSManagedObjectModel

enum FileManagerLocation {
case libraryDirectory
case appGroupDirectory(URL)
}

protocol PersistentContainerConfigurator {
var folderName: String? { get }
var modelName: String { get }
var managedObjectModel: ManagedObjectModel { get }
var location: FileManagerLocation { get }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ import CoreData
import OptimoveCore

extension NSManagedObjectContext {
enum Error: LocalizedError {
case unableToSaveEvent

var errorDescription: String? {
switch self {
case .unableToSaveEvent:
return "Unable to save event"
}
}
}

/**
Safe is determined by checking if the context has any persistent stores.
- Returns: `False` if no persistent stores found.
Expand All @@ -20,21 +31,52 @@ extension NSManagedObjectContext {
- Returns: A value with generic type.
*/
func safeTryPerformAndWait<T>(_ block: (Bool) throws -> T) throws -> T {
var result: Result<T, Error>?
var result: Result<T, Swift.Error>?
performAndWait {
result = Result { try block(isSafe) }
}
return try result!.get()
}

// Async perform with throws error
func safeTryPerform<T>(_ block: @escaping (NSManagedObjectContext) throws -> T, completion: @escaping (Result<T, Swift.Error>) -> Void) {
perform {
guard self.isSafe else {
completion(.failure(NSManagedObjectContext.Error.unableToSaveEvent))
return
}
let result = Result { try block(self) }
completion(result)
}
}

// Async/await perform with throws error
func safeTryPerform<T>(
_ block: @escaping (NSManagedObjectContext) throws -> T) async throws -> T
{
guard isSafe else {
throw NSManagedObjectContext.Error.unableToSaveEvent
}
return try await withCheckedThrowingContinuation { continuation in
self.perform {
do {
let result = try block(self)
continuation.resume(returning: result)
} catch {
continuation.resume(throwing: error)
}
}
}
}

/**
Performs a synchronous block with the passed in boolean indicating if it's safe to perform
operations. Safe is determined by checking if the context has any persistent stores. Throws an error if occurs.
- Parameters:
- block: A block to perform.
*/
func safeTryPerformAndWait(_ block: (Bool) throws -> Void) throws {
var result: Result<Void, Error>?
var result: Result<Void, Swift.Error>?
performAndWait {
result = Result { try block(isSafe) }
}
Expand Down
4 changes: 3 additions & 1 deletion OptimoveSDK/Sources/Classes/Factories/ComponentFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ final class ComponentFactory {
{
self.serviceLocator = serviceLocator
self.coreEventFactory = coreEventFactory
persistentContainer = PersistentContainer()
persistentContainer = PersistentContainer(
persistentContainerConfigurator: OptistreamPersistentContainerConfigurator()
)
}

func createRealtimeComponent(configuration: Configuration) throws -> RealTime {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright © 2023 Optimove. All rights reserved.

import CoreData
import Foundation

final class KSEventModel: NSManagedObject {
@discardableResult
static func insert(
into context: NSManagedObjectContext,
uuid: UUID = UUID(),
atTime: Date,
eventType: String,
userIdentifier: String,
properties: [String: Any]? = nil
) throws -> KSEventModel {
let eventCD: KSEventModel = try context.insertObject()
eventCD.uuid = uuid.uuidString.lowercased()
eventCD.happenedAt = NSNumber(value: Int64(atTime.timeIntervalSince1970 * 1000))
eventCD.eventType = eventType
eventCD.userIdentifier = userIdentifier
if let properties = properties {
eventCD.properties = try JSONSerialization.data(withJSONObject: properties)
}
return eventCD
}
}

extension KSEventModel {
@NSManaged var uuid: String
@NSManaged var userIdentifier: String
@NSManaged var happenedAt: NSNumber
@NSManaged var eventType: String
@NSManaged var properties: Data?
}

extension KSEventModel: Managed {
static var entityName: String {
return "Event"
}

static var defaultSortDescriptors: [NSSortDescriptor] {
return [NSSortDescriptor(keyPath: \KSEventModel.happenedAt, ascending: true)]
}
}

extension KSEventModel {}
Loading