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

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

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

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

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

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

@ -215,9 +215,9 @@ final public class Store {
// MARK: - Synchronization
/// 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)
collection.addOrUpdateIfNewer(instance)
collection.addOrUpdateIfNewer(instance, shared: shared)
}
/// Calls deleteById from the collection corresponding to the instance

@ -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<T: SyncedStorable>(_ instance: T, storeId: String?) {
func synchronizationAddOrUpdate<T: SyncedStorable>(_ 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<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) {
let logs = self._dataLogs.filter { $0.dataId == dataId }

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

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

Loading…
Cancel
Save