work on revocation

sync2
Laurent 12 months ago
parent c3c9718cb2
commit 30afabffa8
  1. 8
      LeStorage.xcodeproj/project.pbxproj
  2. 2
      LeStorage.xcodeproj/xcshareddata/xcschemes/LeStorage.xcscheme
  3. 1
      LeStorage/Codables/ApiCall.swift
  4. 23
      LeStorage/Codables/DataAccess.swift
  5. 3
      LeStorage/Codables/DataLog.swift
  6. 1
      LeStorage/Codables/FailedAPICall.swift
  7. 1
      LeStorage/Codables/GetSyncData.swift
  8. 3
      LeStorage/Codables/Log.swift
  9. 17
      LeStorage/Relationship.swift
  10. 2
      LeStorage/Storable.swift
  11. 11
      LeStorage/Store.swift
  12. 124
      LeStorage/StoreCenter.swift
  13. 14
      LeStorage/StoredCollection.swift
  14. 2
      LeStorage/SyncedStorable.swift
  15. 18
      LeStorage/Utils/Errors.swift

@ -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 = "<group>"; };
C4AC9CE42CEFB12100CC13DF /* DataAccess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataAccess.swift; sourceTree = "<group>"; };
C4AC9CE72CF0A13B00CC13DF /* ClassLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassLoader.swift; sourceTree = "<group>"; };
C4AC9CE92CF754CC00CC13DF /* Relationship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Relationship.swift; sourceTree = "<group>"; };
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 = "<group>"; };
C4D4779C2CB923720077713D /* DataLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataLog.swift; sourceTree = "<group>"; };
@ -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;

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1530"
LastUpgradeVersion = "1600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"

@ -71,6 +71,7 @@ class ApiCall<T: Storable>: ModelObject, Storable, SomeCall {
}
}
static func relationships() -> [Relationship] { return [] }
}
fileprivate extension Dictionary where Key == String, Value == String {

@ -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
}

@ -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

@ -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()

@ -34,4 +34,5 @@ class GetSyncData: ModelObject, SyncedStorable, URLParameterConvertible {
return encodedDate.replacingOccurrences(of: "+", with: "%2B")
}
static func relationships() -> [Relationship] { return [] }
}

@ -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()

@ -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
}

@ -30,6 +30,8 @@ public protocol Storable: Codable, Identifiable, NSObjectProtocol {
func copy(from other: any Storable)
static func relationships() -> [Relationship]
}
extension Storable {

@ -237,9 +237,14 @@ final public class Store {
}
/// Calls deleteById from the collection corresponding to the instance
func revokeNoSync<T: SyncedStorable>(type: T.Type, id: String) throws {
let collection: StoredCollection<T> = try self.collection()
collection.revokeByStringIdNoSync(id)
func referenceCount<T: SyncedStorable>(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<T> = try self.collection()
// collection.revokeByStringIdNoSync(id)
}
// MARK: - Write

@ -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<T: Storable>(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<T: SyncedStorable>(_ 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<T> = 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<T: SyncedStorable>(_ 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?
}

@ -19,6 +19,7 @@ protocol SomeCollection: CollectionHolder, Identifiable {
var hasLoaded: Bool { get }
func allItems() -> [any Storable]
func referenceCount<S: Storable>(type: S.Type, id: String) -> Int
}
protocol SomeSyncedCollection: SomeCollection {
@ -357,6 +358,19 @@ public class StoredCollection<T: Storable>: RandomAccessCollection, SomeCollecti
self.items.removeAll()
self.store.removeFile(type: T.self)
}
// MARK: - Reference count
func referenceCount<S: Storable>(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

@ -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 {

@ -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"
}
}
}

Loading…
Cancel
Save