Improve data hierarchy

sync2
Laurent 11 months ago
parent 30afabffa8
commit 5b86728d77
  1. 38
      LeStorage/Codables/DataAccess.swift
  2. 61
      LeStorage/Codables/FailedAPICall.swift
  3. 4
      LeStorage/Codables/GetSyncData.swift
  4. 26
      LeStorage/Codables/Log.swift
  5. 90
      LeStorage/ModelObject.swift
  6. 11
      LeStorage/Storable.swift
  7. 4
      LeStorage/Store.swift
  8. 24
      LeStorage/StoreCenter.swift
  9. 47
      LeStorage/StoredCollection+Sync.swift
  10. 1
      LeStorage/SyncedStorable.swift

@ -7,9 +7,8 @@
import Foundation import Foundation
class DataAccess: ModelObject, SyncedStorable { class DataAccess: SyncedModelObject, SyncedStorable {
var lastUpdate: Date = Date()
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func resourceName() -> String { return "data-access" } static func resourceName() -> String { return "data-access" }
static func filterByStoreIdentifier() -> Bool { return false } static func filterByStoreIdentifier() -> Bool { return false }
@ -27,6 +26,39 @@ class DataAccess: ModelObject, SyncedStorable {
self.sharedWith = sharedWith self.sharedWith = sharedWith
self.modelName = modelName self.modelName = modelName
self.modelId = modelId 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) { func copy(from other: any Storable) {

@ -8,44 +8,85 @@
import Foundation import Foundation
class FailedAPICall: SyncedModelObject, SyncedStorable { class FailedAPICall: SyncedModelObject, SyncedStorable {
static func resourceName() -> String { return "failed-api-calls" } static func resourceName() -> String { return "failed-api-calls" }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
static func filterByStoreIdentifier() -> Bool { return false } static func filterByStoreIdentifier() -> Bool { return false }
static func relationships() -> [Relationship] { return [] } static func relationships() -> [Relationship] { return [] }
var id: String = Store.randomId() var id: String = Store.randomId()
/// The creation date of the call /// The creation date of the call
var date: Date = Date() var date: Date = Date()
/// The id of the API call /// The id of the API call
var callId: String var callId: String
/// The type of the call /// The type of the call
var type: String var type: String
/// The JSON representation of the API call /// The JSON representation of the API call
var apiCall: String var apiCall: String
/// The server error /// The server error
var error: String var error: String
/// The authentication header /// The authentication header
var authentication: String? var authentication: String?
init(callId: String, type: String, apiCall: String, error: String, authentication: String?) { init(callId: String, type: String, apiCall: String, error: String, authentication: String?) {
self.callId = callId self.callId = callId
self.type = type self.type = type
self.apiCall = apiCall self.apiCall = apiCall
self.error = error self.error = error
self.authentication = authentication 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.date = fac.date
self.callId = fac.callId self.callId = fac.callId
self.type = fac.type self.type = fac.type

@ -7,13 +7,11 @@
import Foundation import Foundation
class GetSyncData: ModelObject, SyncedStorable, URLParameterConvertible { class GetSyncData: SyncedModelObject, SyncedStorable, URLParameterConvertible {
static func filterByStoreIdentifier() -> Bool { return false } static func filterByStoreIdentifier() -> Bool { return false }
static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
var lastUpdate: Date = Date.distantPast
static func resourceName() -> String { static func resourceName() -> String {
return "data" return "data"
} }

@ -19,9 +19,33 @@ class Log: SyncedModelObject, SyncedStorable {
var date: Date = Date() var date: Date = Date()
var message: String var message: String
init(message: String) { init(message: String) {
self.message = message 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) { func copy(from other: any Storable) {

@ -13,8 +13,6 @@ open class ModelObject: NSObject {
public var store: Store? = nil public var store: Store? = nil
var storeId: String? = nil
public override init() { } public override init() { }
open func deleteDependencies() { open func deleteDependencies() {
@ -26,47 +24,65 @@ open class ModelObject: NSObject {
} }
static var relationshipNames: [String] = [] static var relationshipNames: [String] = []
}
open class BaseModelObject: ModelObject, Codable {
public var storeId: String? = nil
// // MARK: - Codable public override init() { }
//
// enum CodingKeys: CodingKey { // Coding Keys to map properties during encoding/decoding
// case storeId enum CodingKeys: String, CodingKey {
// } case storeId
// }
// public required init(from decoder: any Decoder) throws {
// let decoder = try decoder.container(keyedBy: CodingKeys.self) // Required initializer for Decodable
// self.storeId = try decoder.decodeIfPresent(String.self, forKey: CodingKeys.storeId) required public init(from decoder: Decoder) throws {
// } let container = try decoder.container(keyedBy: CodingKeys.self)
// self.storeId = try container.decodeIfPresent(String.self, forKey: .storeId)
// public func encode(to encoder: any Encoder) throws { }
// var container = encoder.container(keyedBy: CodingKeys.self)
// try container.encodeIfPresent(self.storeId, 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 lastUpdate: Date = Date()
public var shared: Bool?
public override init() {
super.init()
}
enum CodingKeys: String, CodingKey {
case lastUpdate
case shared = "_shared"
}
// enum CodingKeys: CodingKey { // Required initializer for Decodable
// case lastUpdate 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()
// public override init() { self.shared = try container.decodeIfPresent(Bool.self, forKey: .shared)
// super.init()
// } try super.init(from: decoder)
// }
// public required init(from decoder: any Decoder) throws {
// try super.init(from: decoder) // Required method for Encodable
// let decoder = try decoder.container(keyedBy: CodingKeys.self) open override func encode(to encoder: Encoder) throws {
// self.lastUpdate = try decoder.decode(Date.self, forKey: CodingKeys.lastUpdate) var container = encoder.container(keyedBy: CodingKeys.self)
// } try container.encode(lastUpdate, forKey: .lastUpdate)
// if self.shared == true {
// open override func encode(to encoder: any Encoder) throws { try container.encodeIfPresent(shared, forKey: .shared)
// try super.encode(to: encoder) }
// var container = encoder.container(keyedBy: CodingKeys.self)
// try container.encodeIfPresent(self.lastUpdate, forKey: .lastUpdate) try super.encode(to: encoder)
// } }
} }

@ -84,4 +84,15 @@ extension Storable {
return storageDirectory 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)")
}
}
} }

@ -215,9 +215,9 @@ final public class Store {
// MARK: - Synchronization // MARK: - Synchronization
/// Calls addOrUpdateIfNewer from the collection corresponding to the instance /// Calls addOrUpdateIfNewer from the collection corresponding to the instance
func addOrUpdateIfNewer<T: SyncedStorable>(_ instance: T) { func addOrUpdateIfNewer<T: SyncedStorable>(_ instance: T, shared: Bool) {
let collection: StoredCollection<T> = self.registerOrGetSyncedCollection(T.self) let collection: StoredCollection<T> = self.registerOrGetSyncedCollection(T.self)
collection.addOrUpdateIfNewer(instance) collection.addOrUpdateIfNewer(instance, shared: shared)
} }
/// Calls deleteById from the collection corresponding to the instance /// Calls deleteById from the collection corresponding to the instance

@ -435,7 +435,7 @@ public class StoreCenter {
} }
if let updates = json["grants"] as? [String: Any] { 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] { 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 { for (className, updateData) in updates {
guard let updateArray = updateData as? [[String: Any]] else { guard let updateArray = updateData as? [[String: Any]] else {
Logger.w("Invalid update data for \(className)") Logger.w("Invalid update data for \(className)")
@ -473,7 +473,7 @@ public class StoreCenter {
let decodedObject = try JSON.decoder.decode(type, from: jsonData) let decodedObject = try JSON.decoder.decode(type, from: jsonData)
let storeId: String? = decodedObject.getStoreId() let storeId: String? = decodedObject.getStoreId()
StoreCenter.main.synchronizationAddOrUpdate(decodedObject, storeId: storeId) StoreCenter.main.synchronizationAddOrUpdate(decodedObject, storeId: storeId, shared: shared)
} catch { } catch {
Logger.w("Issue with json decoding: \(updateItem)") Logger.w("Issue with json decoding: \(updateItem)")
Logger.error(error) Logger.error(error)
@ -569,11 +569,11 @@ public class StoreCenter {
}) })
} }
func synchronizationAddOrUpdate<T: SyncedStorable>(_ instance: T, storeId: String?) { func synchronizationAddOrUpdate<T: SyncedStorable>(_ instance: T, storeId: String?, shared: Bool) {
let hasAlreadyBeenDeleted: Bool = self._hasAlreadyBeenDeleted(instance) let hasAlreadyBeenDeleted: Bool = self._hasAlreadyBeenDeleted(instance)
if !hasAlreadyBeenDeleted { if !hasAlreadyBeenDeleted {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 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 { DispatchQueue.main.async {
do { do {
let type = try StoreCenter.classFromName(model) let type = try StoreCenter.classFromName(model)
let count = Store.main.referenceCount(type: type, id: id) if self._instanceShared(id: id, type: type) {
if count == 0 { let count = Store.main.referenceCount(type: type, id: id)
try self._store(id: storeId).deleteNoSync(type: type, id: id) if count == 0 {
try self._store(id: storeId).deleteNoSync(type: type, id: id)
}
} }
} catch { } catch {
Logger.error(error) Logger.error(error)
} }
} }
} }
fileprivate func _instanceShared<T: SyncedStorable>(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) { fileprivate func _cleanupDataLog(dataId: String) {
let logs = self._dataLogs.filter { $0.dataId == dataId } let logs = self._dataLogs.filter { $0.dataId == dataId }

@ -87,13 +87,9 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable {
defer { defer {
self.setChanged() self.setChanged()
} }
if let realId = self._buildRealId(id: id) { let realId = T.buildRealId(id: id)
if let instance = self.findById(realId) { if let instance = self.findById(realId) {
self.deleteItem(instance) 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)")
} }
} }
@ -102,27 +98,23 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable {
defer { defer {
self.setChanged() self.setChanged()
} }
if let realId = self._buildRealId(id: id) { let realId = T.buildRealId(id: id)
if let instance = self.findById(realId) { if let instance = self.findById(realId) {
self.deleteItemIfUnused(instance) 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)")
} }
} }
fileprivate func _buildRealId(id: String) -> T.ID? { // fileprivate func _buildRealId(id: String) -> T.ID? {
switch T.ID.self { // switch T.ID.self {
case is String.Type: // case is String.Type:
return id as? T.ID // return id as? T.ID
case is Int64.Type: // case is Int64.Type:
return Formatter.number.number(from: id)?.int64Value as? T.ID // return Formatter.number.number(from: id)?.int64Value as? T.ID
default: // default:
fatalError("ID \(type(of: T.ID.self)) is neither String nor Int, can't parse \(id)") // fatalError("ID \(type(of: T.ID.self)) is neither String nor Int, can't parse \(id)")
// return nil //// return nil
} // }
} // }
public func addOrUpdate(instance: T) { public func addOrUpdate(instance: T) {
defer { defer {
@ -230,7 +222,7 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable {
// MARK: - Synchronization // MARK: - Synchronization
func addOrUpdateIfNewer(_ instance: T) { func addOrUpdateIfNewer(_ instance: T, shared: Bool) {
defer { defer {
self.setChanged() self.setChanged()
} }
@ -241,6 +233,9 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable {
self.updateItem(instance, index: index) self.updateItem(instance, index: index)
} }
} else { // insert } else { // insert
if shared {
instance.shared = true
}
self.addItem(instance: instance) self.addItem(instance: instance)
} }

@ -10,6 +10,7 @@ import Foundation
public protocol SyncedStorable: Storable { public protocol SyncedStorable: Storable {
var lastUpdate: Date { get set } var lastUpdate: Date { get set }
var shared: Bool? { get set }
/// Returns HTTP methods that do not need to pass the token to the request /// Returns HTTP methods that do not need to pass the token to the request
static func tokenExemptedMethods() -> [HTTPMethod] static func tokenExemptedMethods() -> [HTTPMethod]

Loading…
Cancel
Save