From 30afabffa85b886d164004afcb100bdaef915f69 Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 28 Nov 2024 15:42:20 +0100 Subject: [PATCH] work on revocation --- LeStorage.xcodeproj/project.pbxproj | 8 +- .../xcshareddata/xcschemes/LeStorage.xcscheme | 2 +- LeStorage/Codables/ApiCall.swift | 1 + LeStorage/Codables/DataAccess.swift | 23 +++- LeStorage/Codables/DataLog.swift | 3 +- LeStorage/Codables/FailedAPICall.swift | 1 + LeStorage/Codables/GetSyncData.swift | 1 + LeStorage/Codables/Log.swift | 3 +- LeStorage/Relationship.swift | 17 +++ LeStorage/Storable.swift | 2 + LeStorage/Store.swift | 11 +- LeStorage/StoreCenter.swift | 124 +++++++++++------- LeStorage/StoredCollection.swift | 14 ++ LeStorage/SyncedStorable.swift | 2 +- LeStorage/Utils/Errors.swift | 18 +++ 15 files changed, 166 insertions(+), 64 deletions(-) create mode 100644 LeStorage/Relationship.swift diff --git a/LeStorage.xcodeproj/project.pbxproj b/LeStorage.xcodeproj/project.pbxproj index 39bbc24..00fb0eb 100644 --- a/LeStorage.xcodeproj/project.pbxproj +++ b/LeStorage.xcodeproj/project.pbxproj @@ -34,6 +34,7 @@ C4A47DAF2B85FD3800ADC637 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A47DAE2B85FD3800ADC637 /* Errors.swift */; }; C4AC9CE52CEFB12100CC13DF /* DataAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AC9CE42CEFB12100CC13DF /* DataAccess.swift */; }; C4AC9CE82CF0A13B00CC13DF /* ClassLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AC9CE72CF0A13B00CC13DF /* ClassLoader.swift */; }; + C4AC9CEA2CF754D200CC13DF /* Relationship.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AC9CE92CF754CC00CC13DF /* Relationship.swift */; }; C4C33F6F2C9B06B7006316DE /* LeStorage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C425D4342B6D24E1002A7B48 /* LeStorage.framework */; }; C4D477972CB66EEA0077713D /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D477962CB66EEA0077713D /* Date+Extensions.swift */; }; C4D4779D2CB923720077713D /* DataLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D4779C2CB923720077713D /* DataLog.swift */; }; @@ -84,6 +85,7 @@ C4A47DAE2B85FD3800ADC637 /* Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; C4AC9CE42CEFB12100CC13DF /* DataAccess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataAccess.swift; sourceTree = ""; }; C4AC9CE72CF0A13B00CC13DF /* ClassLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassLoader.swift; sourceTree = ""; }; + C4AC9CE92CF754CC00CC13DF /* Relationship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Relationship.swift; sourceTree = ""; }; C4C33F6B2C9B06B7006316DE /* LeStorageTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LeStorageTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C4D477962CB66EEA0077713D /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = ""; }; C4D4779C2CB923720077713D /* DataLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataLog.swift; sourceTree = ""; }; @@ -150,6 +152,7 @@ C425D4572B6D2519002A7B48 /* Store.swift */, C4FC2E282C2B2EC30021F3BF /* StoreCenter.swift */, C4A47D642B6E92FE00ADC637 /* Storable.swift */, + C4AC9CE92CF754CC00CC13DF /* Relationship.swift */, C4D4779E2CB92FD80077713D /* SyncedStorable.swift */, C4A47D4E2B6D280200ADC637 /* StoredCollection.swift */, C4D477A02CB9586A0077713D /* StoredCollection+Sync.swift */, @@ -265,7 +268,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1600; - LastUpgradeCheck = 1520; + LastUpgradeCheck = 1600; TargetAttributes = { C425D4332B6D24E1002A7B48 = { CreatedOnToolsVersion = 15.2; @@ -328,6 +331,7 @@ C425D4392B6D24E1002A7B48 /* LeStorage.docc in Sources */, C4AC9CE82CF0A13B00CC13DF /* ClassLoader.swift in Sources */, C4A47DAF2B85FD3800ADC637 /* Errors.swift in Sources */, + C4AC9CEA2CF754D200CC13DF /* Relationship.swift in Sources */, C4A47D612B6D3C1300ADC637 /* Services.swift in Sources */, C4A47D552B6D2DBF00ADC637 /* FileUtils.swift in Sources */, C456EFE22BE52379007388E2 /* StoredSingleton.swift in Sources */, @@ -499,6 +503,7 @@ C425D4492B6D24E1002A7B48 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; @@ -531,6 +536,7 @@ C425D44A2B6D24E1002A7B48 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; diff --git a/LeStorage.xcodeproj/xcshareddata/xcschemes/LeStorage.xcscheme b/LeStorage.xcodeproj/xcshareddata/xcschemes/LeStorage.xcscheme index b566f6c..a1060f6 100644 --- a/LeStorage.xcodeproj/xcshareddata/xcschemes/LeStorage.xcscheme +++ b/LeStorage.xcodeproj/xcshareddata/xcschemes/LeStorage.xcscheme @@ -1,6 +1,6 @@ : ModelObject, Storable, SomeCall { } } + static func relationships() -> [Relationship] { return [] } } fileprivate extension Dictionary where Key == String, Value == String { diff --git a/LeStorage/Codables/DataAccess.swift b/LeStorage/Codables/DataAccess.swift index 8808b4a..f6fb2dc 100644 --- a/LeStorage/Codables/DataAccess.swift +++ b/LeStorage/Codables/DataAccess.swift @@ -8,11 +8,26 @@ import Foundation class DataAccess: ModelObject, SyncedStorable { - var lastUpdate: Date + var lastUpdate: Date = Date() static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func resourceName() -> String { return "data-access" } static func filterByStoreIdentifier() -> Bool { return false } + static func relationships() -> [Relationship] { return [] } + + var id: String = Store.randomId() + var owner: String + var sharedWith: [String] + var modelName: String + var modelId: String + var grantedAt: Date = Date() + + init(owner: String, sharedWith: [String], modelName: String, modelId: String) { + self.owner = owner + self.sharedWith = sharedWith + self.modelName = modelName + self.modelId = modelId + } func copy(from other: any Storable) { guard let dataAccess = other as? DataAccess else { return } @@ -25,12 +40,6 @@ class DataAccess: ModelObject, SyncedStorable { // self.lastHierarchyUpdate = dataAccess.lastHierarchyUpdate } - var id: String - var owner: String - var sharedWith: [String] - var modelName: String - var modelId: String - var grantedAt: Date // var lastHierarchyUpdate: Date } diff --git a/LeStorage/Codables/DataLog.swift b/LeStorage/Codables/DataLog.swift index d064252..c9d46f0 100644 --- a/LeStorage/Codables/DataLog.swift +++ b/LeStorage/Codables/DataLog.swift @@ -12,7 +12,8 @@ class DataLog: ModelObject, Storable { static func resourceName() -> String { return "data-logs" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return false } - + static func relationships() -> [Relationship] { return [] } + var id: String = Store.randomId() /// The id of the underlying data diff --git a/LeStorage/Codables/FailedAPICall.swift b/LeStorage/Codables/FailedAPICall.swift index b5fdd5d..3ea8e99 100644 --- a/LeStorage/Codables/FailedAPICall.swift +++ b/LeStorage/Codables/FailedAPICall.swift @@ -12,6 +12,7 @@ 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() diff --git a/LeStorage/Codables/GetSyncData.swift b/LeStorage/Codables/GetSyncData.swift index 9e9300f..dce52ea 100644 --- a/LeStorage/Codables/GetSyncData.swift +++ b/LeStorage/Codables/GetSyncData.swift @@ -34,4 +34,5 @@ class GetSyncData: ModelObject, SyncedStorable, URLParameterConvertible { return encodedDate.replacingOccurrences(of: "+", with: "%2B") } + static func relationships() -> [Relationship] { return [] } } diff --git a/LeStorage/Codables/Log.swift b/LeStorage/Codables/Log.swift index c8f7ebd..c86e29e 100644 --- a/LeStorage/Codables/Log.swift +++ b/LeStorage/Codables/Log.swift @@ -12,7 +12,8 @@ class Log: SyncedModelObject, SyncedStorable { static func resourceName() -> String { return "logs" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return false } - + static func relationships() -> [Relationship] { return [] } + var id: String = Store.randomId() var date: Date = Date() diff --git a/LeStorage/Relationship.swift b/LeStorage/Relationship.swift new file mode 100644 index 0000000..3074981 --- /dev/null +++ b/LeStorage/Relationship.swift @@ -0,0 +1,17 @@ +// +// Relationship.swift +// LeStorage +// +// Created by Laurent Morvillier on 27/11/2024. +// + +public struct Relationship { + + public init(type: any Storable.Type, keyPath: AnyKeyPath) { + self.type = type + self.keyPath = keyPath + } + + var type: any Storable.Type + var keyPath: AnyKeyPath +} diff --git a/LeStorage/Storable.swift b/LeStorage/Storable.swift index 1b30afc..5672723 100644 --- a/LeStorage/Storable.swift +++ b/LeStorage/Storable.swift @@ -30,6 +30,8 @@ public protocol Storable: Codable, Identifiable, NSObjectProtocol { func copy(from other: any Storable) + static func relationships() -> [Relationship] + } extension Storable { diff --git a/LeStorage/Store.swift b/LeStorage/Store.swift index b3adc65..8d86a45 100644 --- a/LeStorage/Store.swift +++ b/LeStorage/Store.swift @@ -237,9 +237,14 @@ final public class Store { } /// Calls deleteById from the collection corresponding to the instance - func revokeNoSync(type: T.Type, id: String) throws { - let collection: StoredCollection = try self.collection() - collection.revokeByStringIdNoSync(id) + func referenceCount(type: T.Type, id: String) -> Int { + var count: Int = 0 + for collection in self._collections.values { + count += collection.referenceCount(type: type, id: id) + } + return count +// let collection: StoredCollection = try self.collection() +// collection.revokeByStringIdNoSync(id) } // MARK: - Write diff --git a/LeStorage/StoreCenter.swift b/LeStorage/StoreCenter.swift index 4095559..efc1fb9 100644 --- a/LeStorage/StoreCenter.swift +++ b/LeStorage/StoreCenter.swift @@ -427,52 +427,31 @@ public class StoreCenter { } if let updates = json["updates"] as? [String: Any] { - do { - try self._parseSyncUpdates(updates) - } catch { - StoreCenter.main.log(message: error.localizedDescription) - Logger.error(error) - } + try self._parseSyncUpdates(updates) } if let deletions = json["deletions"] as? [String: Any] { - do { - try self._parseSyncDeletions(deletions) - } catch { - StoreCenter.main.log(message: error.localizedDescription) - Logger.error(error) - } + try self._parseSyncDeletions(deletions) } if let updates = json["grants"] as? [String: Any] { - do { - try self._parseSyncUpdates(updates) - } catch { - StoreCenter.main.log(message: error.localizedDescription) - Logger.error(error) - } + try self._parseSyncUpdates(updates) } - if let deletions = json["revocations"] as? [String: Any] { - do { - try self._parseSyncRevocations(deletions) - } catch { - StoreCenter.main.log(message: error.localizedDescription) - Logger.error(error) - } + if let revocations = json["revocations"] as? [String: Any] { + try self._parseSyncRevocations(revocations, parents: json["revocation_parents"] as? [String: Any]) } if let dateString = json["date"] as? String, let date = Date.iso8601FractionalFormatter.date(from: dateString) { - Logger.log("date = \(date)") + Logger.log("Sets sync date = \(date)") self._settingsStorage.update { settings in settings.lastSynchronization = date } - } else { - Logger.w("no date set for the last sync!!!") } } catch { + StoreCenter.main.log(message: error.localizedDescription) Logger.error(error) } } @@ -514,7 +493,7 @@ public class StoreCenter { do { let data = try JSONSerialization.data(withJSONObject: deleted, options: []) - let deletedObject = try JSON.decoder.decode(DeletedObject.self, from: data) + let deletedObject = try JSON.decoder.decode(ObjectIdentifier.self, from: data) StoreCenter.main.synchronizationDelete(id: deletedObject.modelId, model: className, storeId: deletedObject.storeId) } catch { @@ -525,26 +504,41 @@ public class StoreCenter { } } - fileprivate func _parseSyncRevocations(_ deletions: [String: Any]) throws { + fileprivate func _parseSyncRevocations(_ deletions: [String: Any], parents: [String: Any]?) throws { for (className, revocationData) in deletions { - guard let rovokedItems = revocationData as? [Any] else { + guard let revokedItems = revocationData as? [Any] else { Logger.w("Invalid update data for \(className)") continue } - - for revoked in rovokedItems { - + for revoked in revokedItems { do { let data = try JSONSerialization.data(withJSONObject: revoked, options: []) - let deletedObject = try JSON.decoder.decode(DeletedObject.self, from: data) - - StoreCenter.main.synchronizationDelete(id: deletedObject.modelId, model: className, storeId: deletedObject.storeId) + let revokedObject = try JSON.decoder.decode(ObjectIdentifier.self, from: data) + StoreCenter.main.synchronizationDelete(id: revokedObject.modelId, model: className, storeId: revokedObject.storeId) } catch { Logger.error(error) } - } } + + if let parents { + for (className, parentData) in parents { + guard let parentItems = parentData as? [Any] else { + Logger.w("Invalid update data for \(className): \(parentData)") + continue + } + for parentItem in parentItems { + do { + let data = try JSONSerialization.data(withJSONObject: parentItem, options: []) + let revokedObject = try JSON.decoder.decode(ObjectIdentifier.self, from: data) + StoreCenter.main.synchronizationRevoke(id: revokedObject.modelId, model: className, storeId: revokedObject.storeId) + } catch { + Logger.error(error) + } + } + } + + } } static func classFromName(_ className: String) throws -> any SyncedStorable.Type { @@ -602,22 +596,16 @@ public class StoreCenter { DispatchQueue.main.async { do { let type = try StoreCenter.classFromName(model) - try self._store(id: storeId).revokeNoSync(type: type, id: id) + 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) } - self._cleanupDataLog(dataId: id) } } - -// func synchronizationDelete(instance: T, storeId: String?) { -// DispatchQueue.main.async { -// self._store(id: storeId)?.deleteNoSync(instance: instance) -// self._cleanupDataLog(dataId: instance.stringId) -// } -// } - fileprivate func _cleanupDataLog(dataId: String) { let logs = self._dataLogs.filter { $0.dataId == dataId } self._dataLogs.delete(contentOfs: logs) @@ -795,6 +783,44 @@ public class StoreCenter { return nil } + // MARK: - Data Access + + public func giveUserAccess(_ user: String, data: T) throws { + guard let dataAccessCollection = self._dataAccess else { + throw LeStorageError.dataAccessCollectionNotDefined + } + guard let userId = self.userId else { + throw LeStorageError.cantCreateDataAccessBecauseUserIdIsNil + } + let collection: StoredCollection = try Store.main.collection() + guard collection.findById(data.id) != nil else { + throw LeStorageError.cantCreateDataAccessBecauseNotInMainStore + } + + if let dataAccess = dataAccessCollection.first(where: { $0.modelId == data.stringId }) { + dataAccess.sharedWith.append(user) + dataAccessCollection.addOrUpdate(instance: dataAccess) + } else { + let dataAccess = DataAccess(owner: userId, sharedWith: [user], modelName: T.resourceName(), modelId: data.stringId) + dataAccessCollection.addOrUpdate(instance: dataAccess) + } + } + + public func removeUserAccess(_ user: String, data: T) { + guard let dataAccessCollection = self._dataAccess else { + return + } + if let dataAccess = dataAccessCollection.first(where: { $0.modelId == data.stringId }) { + dataAccess.sharedWith.removeAll(where: { $0 == user }) + + if dataAccess.sharedWith.isEmpty { + dataAccessCollection.delete(instance: dataAccess) + } else { + dataAccessCollection.addOrUpdate(instance: dataAccess) + } + } + } + // MARK: - Logs /// Returns the logs collection and instantiates it if necessary @@ -830,7 +856,7 @@ public class StoreCenter { } -class DeletedObject: Codable { +class ObjectIdentifier: Codable { var modelId: String var storeId: String? } diff --git a/LeStorage/StoredCollection.swift b/LeStorage/StoredCollection.swift index 21f7908..2b6aed7 100644 --- a/LeStorage/StoredCollection.swift +++ b/LeStorage/StoredCollection.swift @@ -19,6 +19,7 @@ protocol SomeCollection: CollectionHolder, Identifiable { var hasLoaded: Bool { get } func allItems() -> [any Storable] + func referenceCount(type: S.Type, id: String) -> Int } protocol SomeSyncedCollection: SomeCollection { @@ -357,6 +358,19 @@ public class StoredCollection: RandomAccessCollection, SomeCollecti self.items.removeAll() self.store.removeFile(type: T.self) } + + // MARK: - Reference count + + func referenceCount(type: S.Type, id: String) -> Int { + let relationships = T.relationships().filter { $0.type == type } + guard relationships.count > 0 else { return 0 } + + return self.items.reduce(0) { count, item in + count + relationships.filter { relationship in + (item[keyPath: relationship.keyPath] as? String) == id + }.count + } + } // MARK: - RandomAccessCollection diff --git a/LeStorage/SyncedStorable.swift b/LeStorage/SyncedStorable.swift index e3e7140..4275fac 100644 --- a/LeStorage/SyncedStorable.swift +++ b/LeStorage/SyncedStorable.swift @@ -19,7 +19,7 @@ public protocol SyncedStorable: Storable { /// and will provide a copy of what's on the server /// Should return true to trigger a write on the collection, or false if nothing changed func copyFromServerInstance(_ instance: any Storable) -> Bool - + } protocol URLParameterConvertible { diff --git a/LeStorage/Utils/Errors.swift b/LeStorage/Utils/Errors.swift index 01947c0..0781a49 100644 --- a/LeStorage/Utils/Errors.swift +++ b/LeStorage/Utils/Errors.swift @@ -58,4 +58,22 @@ public enum UUIDError: Error, LocalizedError { public enum LeStorageError: Error { case cantFindClassFromName(name: String) case cantAccessCFBundleName + case cantCreateDataAccessBecauseNotInMainStore + case cantCreateDataAccessBecauseUserIdIsNil + case dataAccessCollectionNotDefined + + public var errorDescription: String? { + switch self { + case .cantFindClassFromName(let string): + return "can't find class for class name: \(string)" + case .cantAccessCFBundleName: + return "can't access CFBundleName for some reason" + case .cantCreateDataAccessBecauseNotInMainStore: + return "Can't create data access because the data is not in the main Store" + case .cantCreateDataAccessBecauseUserIdIsNil: + return "Can't create data access because the there is no logged user" + case .dataAccessCollectionNotDefined: + return "Can't create data access because the collection is not defined" + } + } }