From 5b86728d7778d640ed59b09469323273934a9d77 Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 2 Dec 2024 15:03:42 +0100 Subject: [PATCH] Improve data hierarchy --- LeStorage/Codables/DataAccess.swift | 38 ++++++++++- LeStorage/Codables/FailedAPICall.swift | 61 ++++++++++++++--- LeStorage/Codables/GetSyncData.swift | 4 +- LeStorage/Codables/Log.swift | 26 +++++++- LeStorage/ModelObject.swift | 90 +++++++++++++++----------- LeStorage/Storable.swift | 11 ++++ LeStorage/Store.swift | 4 +- LeStorage/StoreCenter.swift | 24 ++++--- LeStorage/StoredCollection+Sync.swift | 47 ++++++-------- LeStorage/SyncedStorable.swift | 1 + 10 files changed, 216 insertions(+), 90 deletions(-) diff --git a/LeStorage/Codables/DataAccess.swift b/LeStorage/Codables/DataAccess.swift index f6fb2dc..64dee97 100644 --- a/LeStorage/Codables/DataAccess.swift +++ b/LeStorage/Codables/DataAccess.swift @@ -7,9 +7,8 @@ import Foundation -class DataAccess: ModelObject, SyncedStorable { - var lastUpdate: Date = Date() - +class DataAccess: SyncedModelObject, SyncedStorable { + static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func resourceName() -> String { return "data-access" } static func filterByStoreIdentifier() -> Bool { return false } @@ -27,6 +26,39 @@ class DataAccess: ModelObject, SyncedStorable { self.sharedWith = sharedWith self.modelName = modelName self.modelId = modelId + super.init() + } + + // Codable implementation + enum CodingKeys: String, CodingKey { + case id + case owner + case sharedWith + case modelName + case modelId + case grantedAt + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(String.self, forKey: .id) + owner = try container.decode(String.self, forKey: .owner) + sharedWith = try container.decode([String].self, forKey: .sharedWith) + modelName = try container.decode(String.self, forKey: .modelName) + modelId = try container.decode(String.self, forKey: .modelId) + grantedAt = try container.decode(Date.self, forKey: .grantedAt) + try super.init(from: decoder) + } + + override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(owner, forKey: .owner) + try container.encode(sharedWith, forKey: .sharedWith) + try container.encode(modelName, forKey: .modelName) + try container.encode(modelId, forKey: .modelId) + try container.encode(grantedAt, forKey: .grantedAt) + try super.encode(to: encoder) } func copy(from other: any Storable) { diff --git a/LeStorage/Codables/FailedAPICall.swift b/LeStorage/Codables/FailedAPICall.swift index 3ea8e99..7a8ca88 100644 --- a/LeStorage/Codables/FailedAPICall.swift +++ b/LeStorage/Codables/FailedAPICall.swift @@ -8,44 +8,85 @@ import Foundation class FailedAPICall: SyncedModelObject, SyncedStorable { - + static func resourceName() -> String { return "failed-api-calls" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return false } static func relationships() -> [Relationship] { return [] } var id: String = Store.randomId() - + /// The creation date of the call var date: Date = Date() - + /// The id of the API call var callId: String /// The type of the call var type: String - + /// The JSON representation of the API call var apiCall: String - + /// The server error var error: String - + /// The authentication header var authentication: String? - + init(callId: String, type: String, apiCall: String, error: String, authentication: String?) { self.callId = callId self.type = type self.apiCall = apiCall self.error = error self.authentication = authentication + super.init() } - - func copy(from other: any Storable) { + + // MARK: - Codable + + enum CodingKeys: String, CodingKey { + case id + case date + case callId + case type + case apiCall + case error + case authentication + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + id = try container.decode(String.self, forKey: .id) + date = try container.decode(Date.self, forKey: .date) + callId = try container.decode(String.self, forKey: .callId) + type = try container.decode(String.self, forKey: .type) + apiCall = try container.decode(String.self, forKey: .apiCall) + error = try container.decode(String.self, forKey: .error) + authentication = try container.decodeIfPresent(String.self, forKey: .authentication) - guard let fac = other as? FailedAPICall else { return } + try super.init(from: decoder) + } + + override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(id, forKey: .id) + try container.encode(date, forKey: .date) + try container.encode(callId, forKey: .callId) + try container.encode(type, forKey: .type) + try container.encode(apiCall, forKey: .apiCall) + try container.encode(error, forKey: .error) + try container.encodeIfPresent(authentication, forKey: .authentication) + try super.encode(to: encoder) + } + + func copy(from other: any Storable) { + + guard let fac = other as? FailedAPICall else { return } + self.date = fac.date self.callId = fac.callId self.type = fac.type diff --git a/LeStorage/Codables/GetSyncData.swift b/LeStorage/Codables/GetSyncData.swift index dce52ea..b8d2aa7 100644 --- a/LeStorage/Codables/GetSyncData.swift +++ b/LeStorage/Codables/GetSyncData.swift @@ -7,13 +7,11 @@ import Foundation -class GetSyncData: ModelObject, SyncedStorable, URLParameterConvertible { +class GetSyncData: SyncedModelObject, SyncedStorable, URLParameterConvertible { static func filterByStoreIdentifier() -> Bool { return false } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } - var lastUpdate: Date = Date.distantPast - static func resourceName() -> String { return "data" } diff --git a/LeStorage/Codables/Log.swift b/LeStorage/Codables/Log.swift index c86e29e..ab23860 100644 --- a/LeStorage/Codables/Log.swift +++ b/LeStorage/Codables/Log.swift @@ -19,9 +19,33 @@ class Log: SyncedModelObject, SyncedStorable { var date: Date = Date() var message: String - + init(message: String) { self.message = message + super.init() + } + + // MARK: - Codable + enum CodingKeys: String, CodingKey { + case id + case date + case message + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(String.self, forKey: .id) + date = try container.decode(Date.self, forKey: .date) + message = try container.decode(String.self, forKey: .message) + try super.init(from: decoder) + } + + override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(date, forKey: .date) + try container.encode(message, forKey: .message) + try super.encode(to: encoder) } func copy(from other: any Storable) { diff --git a/LeStorage/ModelObject.swift b/LeStorage/ModelObject.swift index 70555a1..24b0f22 100644 --- a/LeStorage/ModelObject.swift +++ b/LeStorage/ModelObject.swift @@ -13,8 +13,6 @@ open class ModelObject: NSObject { public var store: Store? = nil - var storeId: String? = nil - public override init() { } open func deleteDependencies() { @@ -26,47 +24,65 @@ open class ModelObject: NSObject { } static var relationshipNames: [String] = [] + +} + +open class BaseModelObject: ModelObject, Codable { + + public var storeId: String? = nil -// // MARK: - Codable -// -// enum CodingKeys: CodingKey { -// case storeId -// } -// -// public required init(from decoder: any Decoder) throws { -// let decoder = try decoder.container(keyedBy: CodingKeys.self) -// self.storeId = try decoder.decodeIfPresent(String.self, forKey: CodingKeys.storeId) -// } -// -// public func encode(to encoder: any Encoder) throws { -// var container = encoder.container(keyedBy: CodingKeys.self) -// try container.encodeIfPresent(self.storeId, forKey: .storeId) -// } + public override init() { } + + // Coding Keys to map properties during encoding/decoding + enum CodingKeys: String, CodingKey { + case storeId + } + + // Required initializer for Decodable + required public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.storeId = try container.decodeIfPresent(String.self, forKey: .storeId) + } + // Required method for Encodable + open func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.storeId, forKey: .storeId) + } } -open class SyncedModelObject: ModelObject { +open class SyncedModelObject: BaseModelObject { public var lastUpdate: Date = Date() + public var shared: Bool? + + public override init() { + super.init() + } + + enum CodingKeys: String, CodingKey { + case lastUpdate + case shared = "_shared" + } -// enum CodingKeys: CodingKey { -// case lastUpdate -// } -// -// public override init() { -// super.init() -// } -// -// public required init(from decoder: any Decoder) throws { -// try super.init(from: decoder) -// let decoder = try decoder.container(keyedBy: CodingKeys.self) -// self.lastUpdate = try decoder.decode(Date.self, forKey: CodingKeys.lastUpdate) -// } -// -// open override func encode(to encoder: any Encoder) throws { -// try super.encode(to: encoder) -// var container = encoder.container(keyedBy: CodingKeys.self) -// try container.encodeIfPresent(self.lastUpdate, forKey: .lastUpdate) -// } + // Required initializer for Decodable + required public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.lastUpdate = try container.decodeIfPresent(Date.self, forKey: .lastUpdate) ?? Date() + self.shared = try container.decodeIfPresent(Bool.self, forKey: .shared) + + try super.init(from: decoder) + } + + // Required method for Encodable + open override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(lastUpdate, forKey: .lastUpdate) + if self.shared == true { + try container.encodeIfPresent(shared, forKey: .shared) + } + + try super.encode(to: encoder) + } } diff --git a/LeStorage/Storable.swift b/LeStorage/Storable.swift index 5672723..21b7739 100644 --- a/LeStorage/Storable.swift +++ b/LeStorage/Storable.swift @@ -84,4 +84,15 @@ extension Storable { return storageDirectory } + static func buildRealId(id: String) -> ID { + switch ID.self { + case is String.Type: + return id as! ID + case is Int64.Type: + return Formatter.number.number(from: id)?.int64Value as! ID + default: + fatalError("ID \(type(of: ID.self)) is neither String nor Int, can't parse \(id)") + } + } + } diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index 8d86a45..57af2ec 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -215,9 +215,9 @@ final public class Store { // MARK: - Synchronization /// Calls addOrUpdateIfNewer from the collection corresponding to the instance - func addOrUpdateIfNewer(_ instance: T) { + func addOrUpdateIfNewer(_ instance: T, shared: Bool) { let collection: StoredCollection = self.registerOrGetSyncedCollection(T.self) - collection.addOrUpdateIfNewer(instance) + collection.addOrUpdateIfNewer(instance, shared: shared) } /// Calls deleteById from the collection corresponding to the instance diff --git a/LeStorage/StoreCenter.swift b/LeStorage/StoreCenter.swift index efc1fb9..14e67b6 100644 --- a/LeStorage/StoreCenter.swift +++ b/LeStorage/StoreCenter.swift @@ -435,7 +435,7 @@ public class StoreCenter { } if let updates = json["grants"] as? [String: Any] { - try self._parseSyncUpdates(updates) + try self._parseSyncUpdates(updates, shared: true) } if let revocations = json["revocations"] as? [String: Any] { @@ -456,7 +456,7 @@ public class StoreCenter { } } - fileprivate func _parseSyncUpdates(_ updates: [String: Any]) throws { + fileprivate func _parseSyncUpdates(_ updates: [String: Any], shared: Bool = false) throws { for (className, updateData) in updates { guard let updateArray = updateData as? [[String: Any]] else { Logger.w("Invalid update data for \(className)") @@ -473,7 +473,7 @@ public class StoreCenter { let decodedObject = try JSON.decoder.decode(type, from: jsonData) let storeId: String? = decodedObject.getStoreId() - StoreCenter.main.synchronizationAddOrUpdate(decodedObject, storeId: storeId) + StoreCenter.main.synchronizationAddOrUpdate(decodedObject, storeId: storeId, shared: shared) } catch { Logger.w("Issue with json decoding: \(updateItem)") Logger.error(error) @@ -569,11 +569,11 @@ public class StoreCenter { }) } - func synchronizationAddOrUpdate(_ instance: T, storeId: String?) { + func synchronizationAddOrUpdate(_ instance: T, storeId: String?, shared: Bool) { let hasAlreadyBeenDeleted: Bool = self._hasAlreadyBeenDeleted(instance) if !hasAlreadyBeenDeleted { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - self._store(id: storeId).addOrUpdateIfNewer(instance) + self._store(id: storeId).addOrUpdateIfNewer(instance, shared: shared) } } } @@ -596,15 +596,23 @@ public class StoreCenter { DispatchQueue.main.async { do { let type = try StoreCenter.classFromName(model) - let count = Store.main.referenceCount(type: type, id: id) - if count == 0 { - try self._store(id: storeId).deleteNoSync(type: type, id: id) + if self._instanceShared(id: id, type: type) { + let count = Store.main.referenceCount(type: type, id: id) + if count == 0 { + try self._store(id: storeId).deleteNoSync(type: type, id: id) + } } } catch { Logger.error(error) } } } + + fileprivate func _instanceShared(id: String, type: T.Type) -> Bool { + let realId: T.ID = T.buildRealId(id: id) + let instance: T? = Store.main.findById(realId) + return instance?.shared == true + } fileprivate func _cleanupDataLog(dataId: String) { let logs = self._dataLogs.filter { $0.dataId == dataId } diff --git a/LeStorage/StoredCollection+Sync.swift b/LeStorage/StoredCollection+Sync.swift index 0b3acb8..1bd1413 100644 --- a/LeStorage/StoredCollection+Sync.swift +++ b/LeStorage/StoredCollection+Sync.swift @@ -87,13 +87,9 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable { defer { self.setChanged() } - if let realId = self._buildRealId(id: id) { - if let instance = self.findById(realId) { - self.deleteItem(instance) - } - } else { - Logger.w("CRITICAL: collection \(T.resourceName()) could not build id from \(id)") - StoreCenter.main.log(message: "Could not build an id from \(id)") + let realId = T.buildRealId(id: id) + if let instance = self.findById(realId) { + self.deleteItem(instance) } } @@ -102,27 +98,23 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable { defer { self.setChanged() } - if let realId = self._buildRealId(id: id) { - if let instance = self.findById(realId) { - self.deleteItemIfUnused(instance) - } - } else { - Logger.w("CRITICAL: collection \(T.resourceName()) could not build id from \(id)") - StoreCenter.main.log(message: "Could not build an id from \(id)") + let realId = T.buildRealId(id: id) + if let instance = self.findById(realId) { + self.deleteItemIfUnused(instance) } } - fileprivate func _buildRealId(id: String) -> T.ID? { - switch T.ID.self { - case is String.Type: - return id as? T.ID - case is Int64.Type: - return Formatter.number.number(from: id)?.int64Value as? T.ID - default: - fatalError("ID \(type(of: T.ID.self)) is neither String nor Int, can't parse \(id)") -// return nil - } - } +// fileprivate func _buildRealId(id: String) -> T.ID? { +// switch T.ID.self { +// case is String.Type: +// return id as? T.ID +// case is Int64.Type: +// return Formatter.number.number(from: id)?.int64Value as? T.ID +// default: +// fatalError("ID \(type(of: T.ID.self)) is neither String nor Int, can't parse \(id)") +//// return nil +// } +// } public func addOrUpdate(instance: T) { defer { @@ -230,7 +222,7 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable { // MARK: - Synchronization - func addOrUpdateIfNewer(_ instance: T) { + func addOrUpdateIfNewer(_ instance: T, shared: Bool) { defer { self.setChanged() } @@ -241,6 +233,9 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable { self.updateItem(instance, index: index) } } else { // insert + if shared { + instance.shared = true + } self.addItem(instance: instance) } diff --git a/LeStorage/SyncedStorable.swift b/LeStorage/SyncedStorable.swift index 4275fac..6825382 100644 --- a/LeStorage/SyncedStorable.swift +++ b/LeStorage/SyncedStorable.swift @@ -10,6 +10,7 @@ import Foundation public protocol SyncedStorable: Storable { var lastUpdate: Date { get set } + var shared: Bool? { get set } /// Returns HTTP methods that do not need to pass the token to the request static func tokenExemptedMethods() -> [HTTPMethod]