|
|
|
|
@ -9,17 +9,17 @@ import Foundation |
|
|
|
|
import UIKit |
|
|
|
|
|
|
|
|
|
public class StoreCenter { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// The main instance |
|
|
|
|
public static let main: StoreCenter = StoreCenter() |
|
|
|
|
|
|
|
|
|
fileprivate lazy var _storeLibrary: StoreLibrary = { StoreLibrary(storeCenter: self) }() |
|
|
|
|
|
|
|
|
|
/// The name of the directory to store the json files |
|
|
|
|
let directoryName: String |
|
|
|
|
|
|
|
|
|
/// Returns a default Store instance |
|
|
|
|
public lazy var mainStore: Store = { Store(storeCenter: self) }() |
|
|
|
|
/// A dictionary of Stores associated to their id |
|
|
|
|
fileprivate var _stores: [String: Store] = [:] |
|
|
|
|
|
|
|
|
|
lazy var mainStore: Store = { Store(storeCenter: self) }() |
|
|
|
|
|
|
|
|
|
/// A KeychainStore object used to store the user's token |
|
|
|
|
var tokenKeychain: KeychainService? = nil |
|
|
|
|
@ -44,10 +44,10 @@ public class StoreCenter { |
|
|
|
|
fileprivate var _apiCallCollections: [String: any SomeCallCollection] = [:] |
|
|
|
|
|
|
|
|
|
/// A collection of DataLog objects, used for the synchronization |
|
|
|
|
fileprivate var _deleteLogs: StoredCollection<DataLog>? = nil |
|
|
|
|
lazy fileprivate var _deleteLogs: StoredCollection<DataLog> = { self.mainStore.registerCollection() }() |
|
|
|
|
|
|
|
|
|
/// A synchronized collection of DataAccess |
|
|
|
|
fileprivate(set) var dataAccessCollection: SyncedCollection<DataAccess>? = nil |
|
|
|
|
fileprivate var _dataAccess: SyncedCollection<DataAccess>? = nil |
|
|
|
|
|
|
|
|
|
/// A collection storing FailedAPICall objects |
|
|
|
|
fileprivate var _failedAPICallsCollection: SyncedCollection<FailedAPICall>? = nil |
|
|
|
|
@ -61,30 +61,17 @@ public class StoreCenter { |
|
|
|
|
/// The URL manager |
|
|
|
|
fileprivate var _urlManager: URLManager? = nil |
|
|
|
|
|
|
|
|
|
/// Gives the project name to retrieve classes from names |
|
|
|
|
public var classProject: String? = nil |
|
|
|
|
|
|
|
|
|
var useWebsockets: Bool = false |
|
|
|
|
var useSynchronization: Bool = false |
|
|
|
|
|
|
|
|
|
var synchronizesData: Bool = false |
|
|
|
|
var wantsToSynchronize: Bool = false |
|
|
|
|
var classProject: String? = nil |
|
|
|
|
|
|
|
|
|
init(directoryName: String? = nil) { |
|
|
|
|
|
|
|
|
|
self.directoryName = directoryName ?? "storage" |
|
|
|
|
|
|
|
|
|
self._createDirectory() |
|
|
|
|
|
|
|
|
|
self._setupNotifications() |
|
|
|
|
|
|
|
|
|
self.loadApiCallCollection(type: GetSyncData.self) |
|
|
|
|
|
|
|
|
|
if let directoryName { |
|
|
|
|
self._settingsStorage = MicroStorage( |
|
|
|
|
fileName: "\(directoryName)/settings.json") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
NetworkMonitor.shared.onConnectionEstablished = { |
|
|
|
|
self._resumeApiCalls() |
|
|
|
|
// self._configureWebSocket() |
|
|
|
|
@ -92,34 +79,23 @@ public class StoreCenter { |
|
|
|
|
// Logger.log("device Id = \(self.deviceId())") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public func configureURLs(secureScheme: Bool, domain: String, webSockets: Bool = true, useSynchronization: Bool = false) { |
|
|
|
|
|
|
|
|
|
self.useWebsockets = webSockets |
|
|
|
|
self.useSynchronization = useSynchronization |
|
|
|
|
self._deleteLogs = self.mainStore.registerCollection() |
|
|
|
|
|
|
|
|
|
public func configureURLs(secureScheme: Bool, domain: String, webSockets: Bool = true) { |
|
|
|
|
let urlManager: URLManager = URLManager(secureScheme: secureScheme, domain: domain) |
|
|
|
|
self._urlManager = urlManager |
|
|
|
|
self._services = Services(storeCenter: self, url: urlManager.api) |
|
|
|
|
self.tokenKeychain = KeychainStore(serverId: urlManager.api) |
|
|
|
|
|
|
|
|
|
if self.useSynchronization { |
|
|
|
|
self.dataAccessCollection = self.mainStore.registerSynchronizedCollection() |
|
|
|
|
} |
|
|
|
|
self._dataAccess = self.mainStore.registerSynchronizedCollection() |
|
|
|
|
|
|
|
|
|
Logger.log("Sync URL: \(urlManager.api)") |
|
|
|
|
|
|
|
|
|
if self.userId != nil { |
|
|
|
|
if webSockets && self.userId != nil { |
|
|
|
|
self._configureWebSocket() |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fileprivate func _configureWebSocket() { |
|
|
|
|
|
|
|
|
|
guard self.useWebsockets else { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
self._webSocketManager?.disconnect() |
|
|
|
|
self._webSocketManager = nil |
|
|
|
|
|
|
|
|
|
@ -146,12 +122,6 @@ public class StoreCenter { |
|
|
|
|
public var websocketFailure: Bool { |
|
|
|
|
return self._webSocketManager?.failure ?? true |
|
|
|
|
} |
|
|
|
|
public var websocketError: Error? { |
|
|
|
|
return self._webSocketManager?.error |
|
|
|
|
} |
|
|
|
|
public var websocketReconnectAttempts: Int { |
|
|
|
|
return self._webSocketManager?.reconnectAttempts ?? 0 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public var apiURL: String? { |
|
|
|
|
return self._urlManager?.api |
|
|
|
|
@ -184,45 +154,39 @@ public class StoreCenter { |
|
|
|
|
|
|
|
|
|
@objc fileprivate func _launchSynchronization() { |
|
|
|
|
Task { |
|
|
|
|
await self.synchronizeLastUpdates() |
|
|
|
|
do { |
|
|
|
|
try await self.synchronizeLastUpdates() |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// MARK: - Store management |
|
|
|
|
|
|
|
|
|
func requestStore(identifier: String) -> Store { |
|
|
|
|
return self._storeLibrary.requestStore(identifier: identifier) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns the store corresponding to the provided id, and creates one if necessary, otherwise returns the main store |
|
|
|
|
fileprivate func _requestStore(id: String?) -> Store { |
|
|
|
|
if let storeId = id { |
|
|
|
|
return self._storeLibrary.requestStore(identifier: storeId) |
|
|
|
|
} else { |
|
|
|
|
return self.mainStore |
|
|
|
|
/// Registers a store into the list of stores |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - store: A store to save |
|
|
|
|
fileprivate func _registerStore(store: Store) { |
|
|
|
|
guard let identifier = store.identifier else { |
|
|
|
|
fatalError("The store has no identifier") |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fileprivate func _store(id: String?) -> Store? { |
|
|
|
|
if let storeId = id { |
|
|
|
|
return self._storeLibrary[storeId] |
|
|
|
|
} else { |
|
|
|
|
return self.mainStore |
|
|
|
|
if self._stores[identifier] != nil { |
|
|
|
|
fatalError("A store with this identifier has already been registered: \(identifier)") |
|
|
|
|
} |
|
|
|
|
self._stores[identifier] = store |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public func store(identifier: String) throws -> Store { |
|
|
|
|
if let store = self._storeLibrary[identifier] { |
|
|
|
|
/// Returns a store using its identifier, and registers it if it does not exists |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - identifier: The store identifer |
|
|
|
|
/// - parameter: The parameter name used to filter data on the server |
|
|
|
|
public func store(identifier: String) -> Store { |
|
|
|
|
if let store = self._stores[identifier] { |
|
|
|
|
return store |
|
|
|
|
} else { |
|
|
|
|
let store = Store(storeCenter: self, identifier: identifier) |
|
|
|
|
self._registerStore(store: store) |
|
|
|
|
return store |
|
|
|
|
} |
|
|
|
|
throw StoreError.storeNotRegistered(id: identifier) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Deletes the directory using its identifier |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - identifier: The name of the directory |
|
|
|
|
public func destroyStore(identifier: String) { |
|
|
|
|
self._storeLibrary.destroyStore(identifier: identifier) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// MARK: - Settings |
|
|
|
|
@ -264,9 +228,9 @@ public class StoreCenter { |
|
|
|
|
self.resetApiCalls() |
|
|
|
|
self._failedAPICallsCollection?.reset() |
|
|
|
|
|
|
|
|
|
self._storeLibrary.reset() |
|
|
|
|
self.dataAccessCollection?.reset() |
|
|
|
|
self._deleteLogs?.reset() |
|
|
|
|
self._stores.removeAll() |
|
|
|
|
self._dataAccess?.reset() |
|
|
|
|
self._deleteLogs.reset() |
|
|
|
|
|
|
|
|
|
self._settingsStorage.update { settings in |
|
|
|
|
settings.username = nil |
|
|
|
|
@ -478,7 +442,7 @@ public class StoreCenter { |
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
/// Executes an API call |
|
|
|
|
func executeGet<T: SyncedStorable>(apiCall: ApiCall<T>) async throws -> Data { |
|
|
|
|
func executeGet<T: SyncedStorable, V: Decodable>(apiCall: ApiCall<T>) async throws -> V { |
|
|
|
|
return try await self.service().runGetApiCall(apiCall) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -543,7 +507,7 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func itemsRetrieved<T: SyncedStorable>(_ results: [T], storeId: String?, clear: Bool) async { |
|
|
|
|
await self._requestStore(id: storeId).loadCollectionItems(results, clear: clear) |
|
|
|
|
await self._store(id: storeId).loadCollectionItems(results, clear: clear) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns the names of all collections |
|
|
|
|
@ -569,57 +533,37 @@ public class StoreCenter { |
|
|
|
|
self.mainStore.loadCollectionsFromServer(clear: clear) |
|
|
|
|
|
|
|
|
|
// request data that has been shared with the user |
|
|
|
|
if self.useSynchronization { |
|
|
|
|
Task { |
|
|
|
|
do { |
|
|
|
|
try await self.service().getUserDataAccessContent() |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
Task { |
|
|
|
|
do { |
|
|
|
|
try await self.service().getUserDataAccess() |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Basically asks the server for new content |
|
|
|
|
@discardableResult public func synchronizeLastUpdates() async -> Error? { |
|
|
|
|
public func synchronizeLastUpdates() async throws { |
|
|
|
|
|
|
|
|
|
Logger.log("synchronizeLastUpdates: self.synchronizesData: \(self.synchronizesData) / self.isAuthenticated = \(self.isAuthenticated) / self.useSynchronization = \(self.useSynchronization)") |
|
|
|
|
guard self.isAuthenticated, self.useSynchronization else { |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
guard !self.synchronizesData else { |
|
|
|
|
self.wantsToSynchronize = true |
|
|
|
|
return nil |
|
|
|
|
guard self.isAuthenticated else { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
Logger.log(">>> synchronizeLastUpdates started...") |
|
|
|
|
|
|
|
|
|
self.synchronizesData = true |
|
|
|
|
self.wantsToSynchronize = false |
|
|
|
|
|
|
|
|
|
let lastSync = self._settingsStorage.item.lastSynchronization |
|
|
|
|
|
|
|
|
|
do { |
|
|
|
|
let syncGetCollection: ApiCallCollection<GetSyncData> = try self.apiCallCollection() |
|
|
|
|
if await syncGetCollection.hasPendingCalls() == false { |
|
|
|
|
Logger.log("*** START sync: \(lastSync)") |
|
|
|
|
let getSyncData = GetSyncData() |
|
|
|
|
getSyncData.date = lastSync |
|
|
|
|
try await syncGetCollection.sendGetRequest(instance: getSyncData) |
|
|
|
|
} |
|
|
|
|
} catch { |
|
|
|
|
self.synchronizesData = false |
|
|
|
|
self.log(message: "sync failed: \(error)") |
|
|
|
|
Logger.error(error) |
|
|
|
|
return error |
|
|
|
|
|
|
|
|
|
let syncGetCollection: ApiCallCollection<GetSyncData> = try self.apiCallCollection() |
|
|
|
|
if await syncGetCollection.hasPendingCalls() { |
|
|
|
|
await syncGetCollection.rescheduleImmediately() |
|
|
|
|
} else { |
|
|
|
|
let getSyncData = GetSyncData() |
|
|
|
|
getSyncData.date = lastSync |
|
|
|
|
try await syncGetCollection.sendGetRequest(instance: getSyncData) |
|
|
|
|
} |
|
|
|
|
self.synchronizesData = false |
|
|
|
|
Logger.log(">>> synchronizeLastUpdates ended.") |
|
|
|
|
|
|
|
|
|
return nil |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@discardableResult func testSynchronizeOnceAsync() async throws -> Data { |
|
|
|
|
func testSynchronizeOnceAsync() async throws { |
|
|
|
|
guard self.isAuthenticated else { |
|
|
|
|
throw StoreError.missingToken |
|
|
|
|
} |
|
|
|
|
@ -628,7 +572,7 @@ public class StoreCenter { |
|
|
|
|
|
|
|
|
|
let getSyncData = GetSyncData() |
|
|
|
|
getSyncData.date = lastSync |
|
|
|
|
return try await syncGetCollection.executeSingleGet(instance: getSyncData) |
|
|
|
|
await syncGetCollection.executeSingleGet(instance: getSyncData) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func sendGetRequest<T: SyncedStorable>(_ type: T.Type, storeId: String?, clear: Bool) async throws { |
|
|
|
|
@ -655,43 +599,69 @@ public class StoreCenter { |
|
|
|
|
Logger.w("data unrecognized: \(string)") |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let array = try self.decodeDictionary(json) |
|
|
|
|
await self._syncAddOrUpdate(array, shared: .shared) |
|
|
|
|
try await self._parseSyncUpdates(json, shared: true) |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Processes the data coming from a sync request |
|
|
|
|
@MainActor func synchronizeContent(_ syncData: SyncData) async { |
|
|
|
|
|
|
|
|
|
await self._syncAddOrUpdate(syncData.updates) |
|
|
|
|
await self._syncDelete(syncData.deletions) |
|
|
|
|
await self._syncAddOrUpdate(syncData.shared, shared: .shared) |
|
|
|
|
await self._syncAddOrUpdate(syncData.grants, shared: .granted) |
|
|
|
|
await self.syncRevoke(syncData.revocations, parents: syncData.revocationParents) |
|
|
|
|
// self._syncAddOrUpdate(syncData.relationshipSets) |
|
|
|
|
// await self._syncDelete(syncData.relationshipRemovals) |
|
|
|
|
await self._syncAddOrUpdate(syncData.sharedRelationshipSets, shared: .granted) |
|
|
|
|
await self._syncRevoke(syncData.sharedRelationshipRemovals) |
|
|
|
|
|
|
|
|
|
// Logger.log("sync content: updates = \(syncData.updates.count) / deletions = \(syncData.deletions.count), grants = \(syncData.grants.count)") |
|
|
|
|
|
|
|
|
|
if let dateString = syncData.date { |
|
|
|
|
Logger.log("Sets sync date = \(dateString)") |
|
|
|
|
self._settingsStorage.update { settings in |
|
|
|
|
settings.lastSynchronization = dateString |
|
|
|
|
@MainActor func synchronizeContent(_ data: Data) { |
|
|
|
|
|
|
|
|
|
do { |
|
|
|
|
guard |
|
|
|
|
let json = try JSONSerialization.jsonObject(with: data, options: []) |
|
|
|
|
as? [String: Any] |
|
|
|
|
else { |
|
|
|
|
Logger.w("data unrecognized") |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if let updates = json["updates"] as? [String: Any] { |
|
|
|
|
try self._parseSyncUpdates(updates) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
self.synchronizesData = false |
|
|
|
|
if self.wantsToSynchronize { |
|
|
|
|
await self.synchronizeLastUpdates() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Logger.log(">>> SYNC ENDED") |
|
|
|
|
if let deletions = json["deletions"] as? [String: Any] { |
|
|
|
|
try self._parseSyncDeletions(deletions) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if let updates = json["grants"] as? [String: Any] { |
|
|
|
|
try self._parseSyncUpdates(updates, shared: true) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if let revocations = json["revocations"] as? [String: Any] { |
|
|
|
|
try self._parseSyncRevocations(revocations, parents: json["revocation_parents"] as? [[String: Any]]) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Data access events |
|
|
|
|
if let rs = json["relationship_sets"] as? [String: Any] { |
|
|
|
|
try self._parseSyncUpdates(rs) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if let rr = json["relationship_removals"] as? [String: Any] { |
|
|
|
|
try self._parseSyncDeletions(rr) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if let srs = json["shared_relationship_sets"] as? [String: Any] { |
|
|
|
|
try self._parseSyncUpdates(srs, shared: true) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if let srm = json["shared_relationship_removals"] as? [String: Any] { |
|
|
|
|
self._synchronizationRevoke(items: srm) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if let dateString = json["date"] as? String { |
|
|
|
|
Logger.log("Sets sync date = \(dateString)") |
|
|
|
|
self._settingsStorage.update { settings in |
|
|
|
|
settings.lastSynchronization = dateString |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} catch { |
|
|
|
|
self.log(message: error.localizedDescription) |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
NotificationCenter.default.post( |
|
|
|
|
name: NSNotification.Name.LeStorageDidSynchronize, object: self) |
|
|
|
|
|
|
|
|
|
@ -699,103 +669,101 @@ public class StoreCenter { |
|
|
|
|
|
|
|
|
|
/// Processes data that should be inserted or updated inside the app |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - updateArrays: the server updates |
|
|
|
|
/// - updates: the server updates |
|
|
|
|
/// - shared: indicates if the content should be flagged as shared |
|
|
|
|
@MainActor |
|
|
|
|
fileprivate func _syncAddOrUpdate(_ updateArrays: [SyncedStorableArray], shared: SharingStatus? = nil) async { |
|
|
|
|
|
|
|
|
|
for updateArray in updateArrays { |
|
|
|
|
await self._syncAddOrUpdate(updateArray, type: updateArray.type, shared: shared) |
|
|
|
|
// for item in updateArray.items { |
|
|
|
|
// let storeId: String? = item.getStoreId() |
|
|
|
|
// await self.synchronizationAddOrUpdate(item, storeId: storeId, shared: shared) |
|
|
|
|
// } |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
@MainActor func _parseSyncUpdates(_ updates: [String: Any], shared: Bool = false) throws { |
|
|
|
|
for (className, updateData) in updates { |
|
|
|
|
|
|
|
|
|
@MainActor |
|
|
|
|
fileprivate func _syncAddOrUpdate<T: SyncedStorable>(_ updateArray: SyncedStorableArray, type: T.Type, shared: SharingStatus? = nil) async { |
|
|
|
|
|
|
|
|
|
let itemsByStore = updateArray.items.group { $0.getStoreId() } |
|
|
|
|
for (storeId, items) in itemsByStore { |
|
|
|
|
let store = self._requestStore(id: storeId) |
|
|
|
|
store.synchronizationAddOrUpdate(items as! [T], shared: shared) |
|
|
|
|
guard let updateArray = updateData as? [[String: Any]] else { |
|
|
|
|
Logger.w("Invalid update data for \(className)") |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
Logger.log(">>> UPDATE \(updateArray.count) \(className)") |
|
|
|
|
|
|
|
|
|
let type = try self.classFromName(className) |
|
|
|
|
|
|
|
|
|
for updateItem in updateArray { |
|
|
|
|
|
|
|
|
|
do { |
|
|
|
|
let jsonData = try JSONSerialization.data( |
|
|
|
|
withJSONObject: updateItem, options: []) |
|
|
|
|
let decodedObject = try JSON.decoder.decode(type, from: jsonData) |
|
|
|
|
// Logger.log(">>> \(decodedObject.lastUpdate.timeIntervalSince1970) : \(decodedObject.id)") |
|
|
|
|
|
|
|
|
|
let storeId: String? = decodedObject.getStoreId() |
|
|
|
|
self.synchronizationAddOrUpdate(decodedObject, storeId: storeId, shared: shared) |
|
|
|
|
} catch { |
|
|
|
|
Logger.w("Issue with json decoding: \(updateItem)") |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Processes data that should be deleted inside the app |
|
|
|
|
fileprivate func _syncDelete(_ deletionArrays: [ObjectIdentifierArray]) async { |
|
|
|
|
for deletionArray in deletionArrays { |
|
|
|
|
await self._syncDelete(deletionArray, type: deletionArray.type) |
|
|
|
|
|
|
|
|
|
// for deletedObject in deletionArray.items { |
|
|
|
|
// await self.synchronizationDelete(id: deletedObject.modelId, type: deletionArray.type, storeId: deletedObject.storeId) |
|
|
|
|
// } |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
fileprivate func _parseSyncDeletions(_ deletions: [String: Any]) throws { |
|
|
|
|
for (className, deleteData) in deletions { |
|
|
|
|
guard let deletedItems = deleteData as? [Any] else { |
|
|
|
|
Logger.w("Invalid update data for \(className)") |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
for deleted in deletedItems { |
|
|
|
|
|
|
|
|
|
do { |
|
|
|
|
let data = try JSONSerialization.data(withJSONObject: deleted, options: []) |
|
|
|
|
let deletedObject = try JSON.decoder.decode(ObjectIdentifier.self, from: data) |
|
|
|
|
|
|
|
|
|
self.synchronizationDelete(id: deletedObject.modelId, model: className, storeId: deletedObject.storeId) |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fileprivate func _syncDelete<T : SyncedStorable>(_ deletionArray: ObjectIdentifierArray, type: T.Type) async { |
|
|
|
|
|
|
|
|
|
let itemsByStore = deletionArray.items.group { $0.storeId } |
|
|
|
|
for (storeId, items) in itemsByStore { |
|
|
|
|
if let store = self._store(id: storeId) { |
|
|
|
|
await store.synchronizationDelete(items, type: T.self) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// for deletedObject in deletionArray.items { |
|
|
|
|
// |
|
|
|
|
// let itemsByStore = deletionArray.items.group { $0.storeId } |
|
|
|
|
// for (storeId, items) in itemsByStore { |
|
|
|
|
// let store = self._requestStore(id: storeId) |
|
|
|
|
// await store.synchronizationDelete(items, type: T.self) |
|
|
|
|
// } |
|
|
|
|
//// await self.synchronizationDelete(id: deletedObject.modelId, type: deletionArray.type, storeId: deletedObject.storeId) |
|
|
|
|
// } |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Processes data that has been revoked |
|
|
|
|
fileprivate func syncRevoke(_ revokedArrays: [ObjectIdentifierArray], parents: [[ObjectIdentifierArray]]) async { |
|
|
|
|
|
|
|
|
|
await self._syncRevoke(revokedArrays) |
|
|
|
|
for revokedArray in revokedArrays { |
|
|
|
|
await self._syncDelete(revokedArray, type: revokedArray.type) |
|
|
|
|
|
|
|
|
|
// for revoked in revokedArray.items { |
|
|
|
|
// await self.synchronizationDelete(id: revoked.modelId, type: revokedArray.type, storeId: revoked.storeId) // or synchronizationRevoke ? |
|
|
|
|
// } |
|
|
|
|
fileprivate func _parseSyncRevocations(_ deletions: [String: Any], parents: [[String: Any]]?) throws { |
|
|
|
|
for (className, revocationData) in deletions { |
|
|
|
|
guard let revokedItems = revocationData as? [Any] else { |
|
|
|
|
Logger.w("Invalid update data for \(className)") |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
for revoked in revokedItems { |
|
|
|
|
do { |
|
|
|
|
let data = try JSONSerialization.data(withJSONObject: revoked, options: []) |
|
|
|
|
let revokedObject = try JSON.decoder.decode(ObjectIdentifier.self, from: data) |
|
|
|
|
self.synchronizationDelete(id: revokedObject.modelId, model: className, storeId: revokedObject.storeId) |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
for level in parents { |
|
|
|
|
await self._syncRevoke(level) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fileprivate func _syncRevoke(_ revokeArrays: [ObjectIdentifierArray]) async { |
|
|
|
|
for revokeArray in revokeArrays { |
|
|
|
|
await self._syncRevoke(revokeArray: revokeArray) |
|
|
|
|
// for revoked in revokeArray.items { |
|
|
|
|
// await self.synchronizationRevoke(id: revoked.modelId, type: revokeArray.type, storeId: revoked.storeId) |
|
|
|
|
// } |
|
|
|
|
if let parents { |
|
|
|
|
for level in parents { |
|
|
|
|
self._synchronizationRevoke(items: level) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@MainActor |
|
|
|
|
fileprivate func _syncRevoke(revokeArray: ObjectIdentifierArray) async { |
|
|
|
|
|
|
|
|
|
let itemsByStore = revokeArray.items.group { $0.storeId } |
|
|
|
|
for (storeId, items) in itemsByStore { |
|
|
|
|
let store = self._requestStore(id: storeId) |
|
|
|
|
store.synchronizationRevoke(items, type: revokeArray.type) |
|
|
|
|
fileprivate func _synchronizationRevoke(items: [String: Any]) { |
|
|
|
|
for (className, parentData) in items { |
|
|
|
|
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) |
|
|
|
|
self.synchronizationRevoke(id: revokedObject.modelId, model: className, storeId: revokedObject.storeId) |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// for revoked in revokeArray.items { |
|
|
|
|
// |
|
|
|
|
// |
|
|
|
|
// |
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -813,167 +781,95 @@ public class StoreCenter { |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns whether a data has already been deleted by, to avoid inserting it again |
|
|
|
|
func hasAlreadyBeenDeleted<T: Storable>(_ instance: T) -> Bool { |
|
|
|
|
guard let deleteLogs = self._deleteLogs else { |
|
|
|
|
fatalError("missing delete logs collection") |
|
|
|
|
/// Returns the store corresponding to the provided id, and creates one if necessary |
|
|
|
|
fileprivate func _store(id: String?) -> Store { |
|
|
|
|
if let storeId = id { |
|
|
|
|
if let store = self._stores[storeId] { |
|
|
|
|
return store |
|
|
|
|
} else { |
|
|
|
|
let store = Store(storeCenter: self, identifier: storeId) |
|
|
|
|
self._registerStore(store: store) |
|
|
|
|
return store |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
return self.mainStore |
|
|
|
|
} |
|
|
|
|
return deleteLogs.contains(where: { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns whether a data has already been deleted by, to avoid inserting it again |
|
|
|
|
fileprivate func _hasAlreadyBeenDeleted<T: Storable>(_ instance: T) -> Bool { |
|
|
|
|
return self._deleteLogs.contains(where: { |
|
|
|
|
$0.dataId == instance.stringId && $0.operation == .delete |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Adds or updates an instance into the store |
|
|
|
|
// func synchronizationAddOrUpdate<T: SyncedStorable>(_ instance: T, storeId: String?, shared: SharingStatus?) async { |
|
|
|
|
// let hasAlreadyBeenDeleted: Bool = self.hasAlreadyBeenDeleted(instance) |
|
|
|
|
// if !hasAlreadyBeenDeleted { |
|
|
|
|
// await self._requestStore(id: storeId).addOrUpdateIfNewer(instance, shared: shared) |
|
|
|
|
// } |
|
|
|
|
// } |
|
|
|
|
func synchronizationAddOrUpdate<T: SyncedStorable>(_ instance: T, storeId: String?, shared: Bool) { |
|
|
|
|
let hasAlreadyBeenDeleted: Bool = self._hasAlreadyBeenDeleted(instance) |
|
|
|
|
if !hasAlreadyBeenDeleted { |
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
self._store(id: storeId).addOrUpdateIfNewer(instance, shared: shared) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Deletes an instance with the given parameters |
|
|
|
|
// @MainActor |
|
|
|
|
// func synchronizationDelete<T: SyncedStorable>(id: String, type: T.Type, storeId: String?) { |
|
|
|
|
// do { |
|
|
|
|
// try self._store(id: storeId).deleteNoSyncNoCascade(type: type, id: id) |
|
|
|
|
// } catch { |
|
|
|
|
// Logger.error(error) |
|
|
|
|
// } |
|
|
|
|
// self.cleanupDataLog(dataId: id) |
|
|
|
|
// } |
|
|
|
|
func synchronizationDelete(id: String, model: String, storeId: String?) { |
|
|
|
|
|
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
do { |
|
|
|
|
let type = try self.classFromName(model) |
|
|
|
|
try self._store(id: storeId).deleteNoSync(type: type, id: id) |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
self._cleanupDataLog(dataId: id) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Revokes a data that has been shared with the user |
|
|
|
|
// @MainActor |
|
|
|
|
// func synchronizationRevoke<T: SyncedStorable>(id: String, type: T.Type, storeId: String?) { |
|
|
|
|
// |
|
|
|
|
// do { |
|
|
|
|
// if let instance = self._instance(id: id, type: type, storeId: storeId) { |
|
|
|
|
// if instance.sharing != nil && !self.isReferenced(instance: instance) { |
|
|
|
|
// try self._store(id: storeId).deleteNoSyncNoCascade(type: type, id: id) |
|
|
|
|
// } |
|
|
|
|
// } |
|
|
|
|
// } catch { |
|
|
|
|
// Logger.error(error) |
|
|
|
|
// } |
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
// fileprivate func _instance<T: SyncedStorable>(id: String, type: T.Type, storeId: String?) -> T? { |
|
|
|
|
// let realId: T.ID = T.buildRealId(id: id) |
|
|
|
|
// return self._store(id: storeId).findById(realId) |
|
|
|
|
// } |
|
|
|
|
func synchronizationRevoke(id: String, model: String, storeId: String?) { |
|
|
|
|
|
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
do { |
|
|
|
|
let type = try self.classFromName(model) |
|
|
|
|
if self._instanceShared(id: id, type: type) { |
|
|
|
|
let count = self.mainStore.referenceCount(type: type, id: id) |
|
|
|
|
if count == 0 { |
|
|
|
|
try self._store(id: storeId).deleteNoSync(type: type, id: id) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns whether an instance has been shared with the user |
|
|
|
|
// fileprivate func _instanceShared<T: SyncedStorable>(id: String, type: T.Type, storeId: String?) -> Bool { |
|
|
|
|
//// let realId: T.ID = T.buildRealId(id: id) |
|
|
|
|
// let instance: T? = self._instance(id: id, type: type, storeId: storeId) |
|
|
|
|
// return instance?.sharing != nil |
|
|
|
|
// } |
|
|
|
|
fileprivate func _instanceShared<T: SyncedStorable>(id: String, type: T.Type) -> Bool { |
|
|
|
|
let realId: T.ID = T.buildRealId(id: id) |
|
|
|
|
let instance: T? = self.mainStore.findById(realId) |
|
|
|
|
return instance?.shared == true |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Deletes a data log by data id |
|
|
|
|
func cleanupDataLog(dataId: String) { |
|
|
|
|
guard let deleteLogs = self._deleteLogs else { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
let logs = deleteLogs.filter { $0.dataId == dataId } |
|
|
|
|
deleteLogs.delete(contentOfs: logs) |
|
|
|
|
fileprivate func _cleanupDataLog(dataId: String) { |
|
|
|
|
let logs = self._deleteLogs.filter { $0.dataId == dataId } |
|
|
|
|
self._deleteLogs.delete(contentOfs: logs) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Creates a delete log for an instance |
|
|
|
|
func createDeleteLog<T: Storable>(_ instance: T) { |
|
|
|
|
let dataLog = DataLog(dataId: instance.stringId, |
|
|
|
|
modelName: String(describing: T.self), |
|
|
|
|
operation: .delete) |
|
|
|
|
self._deleteLogs?.addOrUpdate(instance: dataLog) |
|
|
|
|
self._addDataLog(instance, method: .delete) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns the appropriate store for a relationship |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - instance: some Storable instance |
|
|
|
|
/// - relationship: the relationship |
|
|
|
|
func relationshipStore<T: Storable>(instance: T, relationship: Relationship) -> Store? { |
|
|
|
|
switch relationship.storeLookup { |
|
|
|
|
case .main: return Store.main |
|
|
|
|
case .child: return self._storeLibrary[instance.stringId] |
|
|
|
|
case .same: return instance.store |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns if an instance has at least one valid parent relationship by checking if the id of the parent exists |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - instance: some Storable instance |
|
|
|
|
/// - relationship: the relationship |
|
|
|
|
func hasParentReferences<T: Storable, S: Storable>(instance: T, relationshipType: S.Type, relationship: Relationship) -> Bool { |
|
|
|
|
if let referenceId = instance[keyPath: relationship.keyPath] as? S.ID, |
|
|
|
|
let store = self.relationshipStore(instance: instance, relationship: relationship) { |
|
|
|
|
let instance: S? = store.findById(referenceId) |
|
|
|
|
return instance != nil |
|
|
|
|
} |
|
|
|
|
return false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func isReferenced<T: Storable>(instance: T) -> Bool { |
|
|
|
|
|
|
|
|
|
for relationship in T.parentRelationships() { |
|
|
|
|
if self.hasParentReferences(instance: instance, relationshipType: relationship.type, relationship: relationship) { |
|
|
|
|
return true |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
for relationship in T.childrenRelationships() { |
|
|
|
|
if let store = self.relationshipStore(instance: instance, relationship: relationship) { |
|
|
|
|
if store.isReferenced(collectionType: relationship.type, type: T.self, id: instance.stringId) { |
|
|
|
|
return true |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
Logger.w("missing store for instance \(instance)") |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return false |
|
|
|
|
/// Adds a datalog for an instance with the associated method |
|
|
|
|
fileprivate func _addDataLog<T: Storable>(_ instance: T, method: HTTPMethod) { |
|
|
|
|
let dataLog = DataLog( |
|
|
|
|
dataId: instance.stringId, modelName: String(describing: T.self), operation: method) |
|
|
|
|
self._deleteLogs.addOrUpdate(instance: dataLog) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// MARK: - Sync data conversion |
|
|
|
|
|
|
|
|
|
func decodeObjectIdentifierDictionary(_ dictionary: [String: Any]) throws -> [ObjectIdentifierArray] { |
|
|
|
|
|
|
|
|
|
var objectIdentifierArray: [ObjectIdentifierArray] = [] |
|
|
|
|
|
|
|
|
|
for (className, dataArray) in dictionary { |
|
|
|
|
|
|
|
|
|
guard let array = dataArray as? [[String: Any]] else { |
|
|
|
|
Logger.w("Invalid update data for \(className)") |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
let type = try self.classFromName(className) |
|
|
|
|
let decodedArray = try self._decodeArray(type: ObjectIdentifier.self, array: array) |
|
|
|
|
objectIdentifierArray.append(ObjectIdentifierArray(type: type, items: decodedArray)) |
|
|
|
|
} |
|
|
|
|
return objectIdentifierArray |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func decodeDictionary(_ dictionary: [String: Any]) throws -> [SyncedStorableArray] { |
|
|
|
|
|
|
|
|
|
var syncedStorableArray: [SyncedStorableArray] = [] |
|
|
|
|
|
|
|
|
|
for (className, dataArray) in dictionary { |
|
|
|
|
|
|
|
|
|
guard let array = dataArray as? [[String: Any]] else { |
|
|
|
|
Logger.w("Invalid update data for \(className)") |
|
|
|
|
continue |
|
|
|
|
} |
|
|
|
|
// Logger.log(">>> UPDATE \(array.count) \(className)") |
|
|
|
|
|
|
|
|
|
let type = try self.classFromName(className) |
|
|
|
|
let decodedArray = try self._decodeArray(type: type, array: array) |
|
|
|
|
syncedStorableArray.append(SyncedStorableArray(type: type, items: decodedArray)) |
|
|
|
|
} |
|
|
|
|
return syncedStorableArray |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fileprivate func _decodeArray<T: Decodable>(type: T.Type, array: [[String : Any]]) throws -> [T] { |
|
|
|
|
let jsonData = try JSONSerialization.data(withJSONObject: array, options: []) |
|
|
|
|
return try JSON.decoder.decode([T].self, from: jsonData) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// MARK: - Miscellanous |
|
|
|
|
|
|
|
|
|
/// Returns the count of api calls for a Type |
|
|
|
|
@ -1093,6 +989,15 @@ public class StoreCenter { |
|
|
|
|
return !self._blackListedUserName.contains(where: { $0 == userName }) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Deletes the directory using its identifier |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - identifier: The name of the directory |
|
|
|
|
public func destroyStore(identifier: String) { |
|
|
|
|
let directory = "\(self.directoryName)/\(identifier)" |
|
|
|
|
FileManager.default.deleteDirectoryInDocuments(directoryName: directory) |
|
|
|
|
self._stores.removeValue(forKey: identifier) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// MARK: - Instant update |
|
|
|
|
|
|
|
|
|
/// Updates a local object with a server instance |
|
|
|
|
@ -1107,37 +1012,35 @@ public class StoreCenter { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns the collection hosting an instance |
|
|
|
|
func collectionOfInstance<T: Storable>(_ instance: T) -> (any SomeCollection)? { |
|
|
|
|
func collectionOfInstance<T: Storable>(_ instance: T) -> BaseCollection<T>? { |
|
|
|
|
do { |
|
|
|
|
if let storeId = instance.getStoreId() { |
|
|
|
|
let store = try self.store(identifier: storeId) |
|
|
|
|
return try store.someCollection(type: T.self) |
|
|
|
|
let collection: BaseCollection<T> = try self.mainStore.collection() |
|
|
|
|
if collection.findById(instance.id) != nil { |
|
|
|
|
return collection |
|
|
|
|
} else { |
|
|
|
|
return try Store.main.someCollection(type: T.self) |
|
|
|
|
return self.collectionOfInstanceInSubStores(instance) |
|
|
|
|
} |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
return self.collectionOfInstanceInSubStores(instance) |
|
|
|
|
} |
|
|
|
|
return nil |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Search inside the additional stores to find the collection hosting the instance |
|
|
|
|
// func collectionOfInstanceInSubStores<T: Storable>(_ instance: T) -> BaseCollection<T>? { |
|
|
|
|
// for store in self._stores.values { |
|
|
|
|
// let collection: BaseCollection<T>? = try? store.collection() |
|
|
|
|
// if collection?.findById(instance.id) != nil { |
|
|
|
|
// return collection |
|
|
|
|
// } |
|
|
|
|
// } |
|
|
|
|
// return nil |
|
|
|
|
// } |
|
|
|
|
func collectionOfInstanceInSubStores<T: Storable>(_ instance: T) -> BaseCollection<T>? { |
|
|
|
|
for store in self._stores.values { |
|
|
|
|
let collection: BaseCollection<T>? = try? store.collection() |
|
|
|
|
if collection?.findById(instance.id) != nil { |
|
|
|
|
return collection |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// MARK: - Data Access |
|
|
|
|
|
|
|
|
|
/// Returns the list of users have access to a data given its id |
|
|
|
|
public func authorizedUsers(for modelId: String) -> [String] { |
|
|
|
|
guard let dataAccessCollection = self.dataAccessCollection else { |
|
|
|
|
guard let dataAccessCollection = self._dataAccess else { |
|
|
|
|
return [] |
|
|
|
|
} |
|
|
|
|
if let dataAccess = dataAccessCollection.first(where: { $0.modelId == modelId }) { |
|
|
|
|
@ -1146,10 +1049,10 @@ public class StoreCenter { |
|
|
|
|
return [] |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public func setAuthorizedUsersAsync<T: SyncedStorable>(for instance: T, users: [String]) async throws { |
|
|
|
|
|
|
|
|
|
guard let dataAccessCollection = self.dataAccessCollection else { |
|
|
|
|
throw StoreError.synchronizationInactive |
|
|
|
|
/// Sets the the list of authorized users for an instance |
|
|
|
|
public func setAuthorizedUsers<T: SyncedStorable>(for instance: T, users: [String]) throws { |
|
|
|
|
guard let dataAccessCollection = self._dataAccess else { |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
guard let userId = self.userId else { |
|
|
|
|
throw LeStorageError.cantCreateDataAccessBecauseUserIdIsNil |
|
|
|
|
@ -1157,27 +1060,15 @@ public class StoreCenter { |
|
|
|
|
|
|
|
|
|
if let dataAccess = dataAccessCollection.first(where: { $0.modelId == instance.stringId }) { |
|
|
|
|
if users.isEmpty { |
|
|
|
|
try await dataAccessCollection.deleteAsync(instance: dataAccess) |
|
|
|
|
dataAccessCollection.delete(instance: dataAccess) |
|
|
|
|
} else { |
|
|
|
|
dataAccess.sharedWith.removeAll() |
|
|
|
|
dataAccess.sharedWith = users |
|
|
|
|
try await dataAccessCollection.addOrUpdateAsync(instance: dataAccess) |
|
|
|
|
dataAccessCollection.addOrUpdate(instance: dataAccess) |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
let dataAccess = DataAccess(owner: userId, sharedWith: users, modelName: String(describing: type(of: instance)), modelId: instance.stringId, storeId: instance.getStoreId()) |
|
|
|
|
try await dataAccessCollection.addOrUpdateAsync(instance: dataAccess) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Sets the the list of authorized users for an instance |
|
|
|
|
public func setAuthorizedUsers<T: SyncedStorable>(for instance: T, users: [String]) throws { |
|
|
|
|
Task { |
|
|
|
|
do { |
|
|
|
|
try await self.setAuthorizedUsersAsync(for: instance, users: users) |
|
|
|
|
} catch { |
|
|
|
|
Logger.error(error) |
|
|
|
|
} |
|
|
|
|
let dataAccess = DataAccess(owner: userId, sharedWith: users, modelName: String(describing: type(of: instance)), modelId: instance.stringId) |
|
|
|
|
dataAccessCollection.addOrUpdate(instance: dataAccess) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -1197,11 +1088,21 @@ public class StoreCenter { |
|
|
|
|
/// Logs a message in the logs collection |
|
|
|
|
public func log(message: String) { |
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
let log = Log(message: message, user: self.userId) |
|
|
|
|
let log = Log(message: message) |
|
|
|
|
self._logsCollection().addOrUpdate(instance: log) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// MARK: - Migration |
|
|
|
|
|
|
|
|
|
/// Migrates the token from the provided service to the main Services instance |
|
|
|
|
// public func migrateToken(_ services: Services) throws { |
|
|
|
|
// guard let userName = self.userName() else { |
|
|
|
|
// return |
|
|
|
|
// } |
|
|
|
|
// try self.service().migrateToken(services, userName: userName) |
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
deinit { |
|
|
|
|
NotificationCenter.default.removeObserver(self) |
|
|
|
|
} |
|
|
|
|
|