You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
LeStorage/LeStorage/StoreCenter.swift

994 lines
35 KiB

//
// StoreCenter.swift
// LeStorage
//
// Created by Laurent Morvillier on 25/06/2024.
//
import Foundation
import UIKit
public class StoreCenter {
/// The main instance
public static let main: StoreCenter = StoreCenter()
/// A dictionary of Stores associated to their id
fileprivate var _stores: [String: Store] = [:]
/// Indicates to Stored Collection if they can synchronize
public var collectionsCanSynchronize: Bool = true
/// Force the absence of synchronization
public var forceNoSynchronization: Bool = false
/// A store for the Settings object
fileprivate var _settingsStorage: MicroStorage<Settings> = MicroStorage(
fileName: "settings.json")
/// The services performing the API calls
fileprivate var _services: Services?
/// The WebSocketManager that manages realtime synchronization
fileprivate var _webSocketManager: WebSocketManager?
/// The dictionary of registered StoredCollections
fileprivate var _apiCallCollections: [String: any SomeCallCollection] = [:]
/// A collection of DataLog objects, used for the synchronization
fileprivate var _dataLogs: StoredCollection<DataLog>
/// A synchronized collection of DataAccess
fileprivate var _dataAccess: StoredCollection<DataAccess>? = nil
/// A collection storing FailedAPICall objects
fileprivate var _failedAPICallsCollection: StoredCollection<FailedAPICall>? = nil
/// A collection of Log objects
fileprivate var _logs: StoredCollection<Log>? = nil
/// A list of username that cannot synchronize with the server
fileprivate var _blackListedUserName: [String] = []
/// The URL manager
fileprivate var _urlManager: URLManager? = nil
init() {
// self._syncGetRequests = ApiCallCollection()
self._dataLogs = Store.main.registerCollection()
self._setupNotifications()
self.loadApiCallCollection(type: GetSyncData.self)
NetworkMonitor.shared.onConnectionEstablished = {
self._resumeApiCalls()
// self._configureWebSocket()
}
// Logger.log("device Id = \(self.deviceId())")
}
public func configureURLs(secureScheme: Bool, domain: String) {
let urlManager: URLManager = URLManager(secureScheme: secureScheme, domain: domain)
self._urlManager = urlManager
self._services = Services(url: urlManager.api)
self._dataAccess = Store.main.registerSynchronizedCollection()
Logger.log("Sync URL: \(urlManager.api)")
if self.userId != nil {
self._configureWebSocket()
}
}
fileprivate func _configureWebSocket() {
guard let userId = self.userId else {
Logger.w("Tried to configure websocket but userId is nil")
return
}
guard let urlManager = self._urlManager else {
Logger.w("Tried to configure websocket no URL has been defined")
return
}
let url = urlManager.websocket(userId: userId)
self._webSocketManager = WebSocketManager(urlString: url)
Logger.log("websocket configured: \(url)")
}
public var hasWebSocketManager: Bool {
return self._webSocketManager != nil
}
public var websocketPingStatus: Bool {
return self._webSocketManager?.pingStatus ?? false
}
public var websocketFailure: Bool {
return self._webSocketManager?.failure ?? true
}
public var apiURL: String? {
return self._urlManager?.api
}
public var lastSyncDate: String {
return self._settingsStorage.item.lastSynchronization
}
/// Returns the service instance
public func service() throws -> Services {
if let service = self._services {
return service
} else {
throw StoreError.missingService
}
}
private func _setupNotifications() {
NotificationCenter.default.addObserver(
self,
selector: #selector(_willEnterForegroundNotification),
name: UIScene.willEnterForegroundNotification,
object: nil)
}
@objc fileprivate func _willEnterForegroundNotification() {
// Logger.log("_willEnterForegroundNotification")
self._launchSynchronization()
}
@objc fileprivate func _launchSynchronization() {
Task {
do {
try await self.synchronizeLastUpdates()
} catch {
Logger.error(error)
}
}
}
/// 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")
}
if self._stores[identifier] != nil {
fatalError("A store with this identifier has already been registered: \(identifier)")
}
self._stores[identifier] = store
}
/// 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(identifier: identifier)
self._registerStore(store: store)
return store
}
}
// MARK: - Settings
/// Sets the user info given a user
func userDidLogIn(user: UserBase, at date: Date) {
self._settingsStorage.update { settings in
settings.userId = user.id
settings.username = user.username
let date = Date.microSecondFormatter.string(from: date)
Logger.log("LOG date = \(date)")
settings.lastSynchronization = Date.microSecondFormatter.string(from: Date.distantPast)
self._configureWebSocket()
}
}
/// Returns the stored user Id
public var userId: String? {
return self._settingsStorage.item.userId
}
/// Returns the username
public func userName() -> String? {
return self._settingsStorage.item.username
}
/// Returns the stored token
public func token() -> String? {
return try? self.service().keychainStore.getValue()
}
/// Disconnect the user from the storage and resets collection
public func disconnect() {
try? self.service().deleteToken()
self.resetApiCalls()
self._failedAPICallsCollection?.reset()
self._stores.removeAll()
self._dataAccess?.reset()
self._settingsStorage.update { settings in
settings.username = nil
settings.userId = nil
settings.lastSynchronization = Date.microSecondFormatter.string(from: Date())
self._webSocketManager = nil
}
}
/// Returns whether the system has a user token
public func hasToken() -> Bool {
do {
_ = try self.service().keychainStore.getValue()
return true
} catch {
return false
}
}
/// Returns a generated device id
/// If created, stores it inside the keychain to get a consistent value even if the app is deleted
/// as UIDevice.current.identifierForVendor value changes when the app is deleted and installed again
func deviceId() -> String {
let keychainStore = KeychainStore(serverId: "lestorage.main")
do {
return try keychainStore.getValue()
} catch {
let deviceId: String =
UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
do {
try keychainStore.add(value: deviceId)
} catch {
Logger.error(error)
}
return deviceId
}
}
// MARK: - Api Calls management
/// Instantiates and loads an ApiCallCollection with the provided type
public func loadApiCallCollection<T: SyncedStorable>(type: T.Type) {
if self._apiCallCollections[T.resourceName()] == nil {
let apiCallCollection = ApiCallCollection<T>()
self._apiCallCollections[T.resourceName()] = apiCallCollection
Task {
do {
try await apiCallCollection.loadFromFile()
let count = await apiCallCollection.items.count
Logger.log("collection \(T.resourceName()) loaded with \(count)")
await apiCallCollection.rescheduleApiCallsIfNecessary()
} catch {
Logger.error(error)
}
}
}
}
/// Returns the ApiCall collection using the resource name of the provided T type
func apiCallCollection<T: Storable>() throws -> ApiCallCollection<T> {
if let collection = self._apiCallCollections[T.resourceName()] as? ApiCallCollection<T> {
return collection
}
throw StoreError.apiCallCollectionNotRegistered(type: T.resourceName())
}
/// Deletes an ApiCall, identifying it by dataId
/// - Parameters:
/// - type: the subsequent type of the ApiCall
/// - id: the id of the data stored inside the ApiCall
func deleteApiCallByDataId<T: SyncedStorable>(type: T.Type, id: String) async throws {
let apiCallCollection: ApiCallCollection<T> = try self.apiCallCollection()
await apiCallCollection.deleteByDataId(id)
}
/// Deletes an ApiCall by its id
/// - Parameters:
/// - type: the subsequent type of the ApiCall
/// - id: the id of the ApiCall
func deleteApiCallById<T: SyncedStorable>(type: T.Type, id: String) async throws {
let apiCallCollection: ApiCallCollection<T> = try self.apiCallCollection()
await apiCallCollection.deleteById(id)
}
/// Deletes an ApiCall by its id
/// - Parameters:
/// - id: the id of the ApiCall
/// - collectionName: the name of the collection of ApiCall
func deleteApiCallById(_ id: String, collectionName: String) async throws {
if let apiCallCollection = self._apiCallCollections[collectionName] {
await apiCallCollection.deleteById(id)
} else {
throw StoreError.collectionNotRegistered(type: collectionName)
}
}
/// Resets all the api call collections
public func resetApiCalls() {
Task {
for collection in self._apiCallCollections.values {
await collection.reset()
}
}
}
public func resetLoggingCollections() {
Task {
do {
try FileManager.default.removeItem(at: Log.urlForJSONFile())
try FileManager.default.removeItem(at: FailedAPICall.urlForJSONFile())
let facApiCallCollection: ApiCallCollection<FailedAPICall> = try self.apiCallCollection()
await facApiCallCollection.reset()
let logApiCallCollection: ApiCallCollection<Log> = try self.apiCallCollection()
await logApiCallCollection.reset()
} catch {
Logger.error(error)
}
}
}
/// Resets the ApiCall whose type identifies with the provided collection
/// - Parameters:
/// - collection: The collection identifying the Storable type
public func resetApiCalls<T: SyncedStorable>(collection: StoredCollection<T>) {
do {
let apiCallCollection: ApiCallCollection<T> = try self.apiCallCollection()
Task {
await apiCallCollection.reset()
}
} catch {
Logger.error(error)
}
}
// MARK: - Api call rescheduling
/// Retry API calls immediately
fileprivate func _resumeApiCalls() {
guard self.collectionsCanSynchronize else {
return
}
Logger.log("_resumeApiCalls")
Task {
for collection in self._apiCallCollections.values {
await collection.resumeApiCalls()
}
}
}
/// Reschedule an ApiCall by id
func rescheduleApiCalls<T: SyncedStorable>(type: T.Type) async throws {
guard self.collectionsCanSynchronize else {
return
}
let collection: ApiCallCollection<T> = try self.apiCallCollection()
await collection.rescheduleApiCallsIfNecessary()
}
/// Executes an ApiCall
// fileprivate func _executeApiCall<T: SyncedStorable, V: Decodable>(_ apiCall: ApiCall<T>)
// async throws -> V
// {
// return try await self.service().runApiCall(apiCall)
// }
/// Executes an API call
func executeGet<T: SyncedStorable, V: Decodable>(apiCall: ApiCall<T>) async throws -> V {
return try await self.service().runGetApiCall(apiCall)
}
/// Executes an API call
public func execute<T: SyncedStorable>(apiCalls: [ApiCall<T>]) async throws -> [T] {
return try await self.service().runApiCalls(apiCalls)
}
// MARK: - Api calls
/// Returns whether the collection can synchronize
fileprivate func _canSynchronise() -> Bool {
return !self.forceNoSynchronization && self.collectionsCanSynchronize
&& self.userIsAllowed()
}
func sendOperationBatch<T: SyncedStorable>(_ batch: OperationBatch<T>) async throws -> [T] {
guard self._canSynchronise() else {
return []
}
return try await self.apiCallCollection().executeBatch(batch)
}
/// Transmit the insertion request to the ApiCall collection
/// - Parameters:
/// - instance: an object to insert
// func sendInsertion<T: SyncedStorable>(_ instance: T) async throws -> T? {
// guard self._canSynchronise() else {
// return nil
// }
// return try await self.apiCallCollection().sendInsertion(instance)
// }
//
// /// Transmit the update request to the ApiCall collection
// /// - Parameters:
// /// - instance: an object to update
// func sendUpdate<T: SyncedStorable>(_ instance: T) async throws -> T? {
// guard self._canSynchronise() else {
// return nil
// }
// return try await self.apiCallCollection().sendUpdate(instance)
// }
//
// /// Transmit the deletion request to the ApiCall collection
// /// - Parameters:
// /// - instance: an object to delete
// func sendDeletion<T: SyncedStorable>(_ instance: T) async throws {
// guard self._canSynchronise() else {
// return
// }
// try await self.apiCallCollection().sendDeletion(instance)
// }
/// Retrieves all the items on the server
func getItems<T: SyncedStorable>(identifier: String? = nil) async throws -> [T] {
return try await self.service().get(identifier: identifier)
}
// MARK: - Synchronization
/// Creates the ApiCallCollection to manage the calls to the API
fileprivate func _createSyncApiCallCollection() {
self.loadApiCallCollection(type: GetSyncData.self)
}
/// Loads all the data from the server for the users
public func initialSynchronization() {
Store.main.loadCollectionsFromServer()
// request data that has been shared with the user
Task {
do {
try await self.service().getUserDataAccess()
} catch {
Logger.error(error)
}
}
}
/// Basically asks the server for new content
public func synchronizeLastUpdates() async throws {
let lastSync = self._settingsStorage.item.lastSynchronization
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)
}
}
/// Processes Data Access data
func userDataAccessRetrieved(_ data: Data) async {
do {
guard
let json = try JSONSerialization.jsonObject(with: data, options: [])
as? [String: Any]
else {
Logger.w("data unrecognized")
return
}
try await self._parseSyncUpdates(json, shared: true)
} catch {
Logger.error(error)
}
}
/// Processes the data coming from a sync request
@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)
}
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]])
}
if let dateString = json["date"] as? String {
Logger.log("Sets sync date = \(dateString)")
self._settingsStorage.update { settings in
settings.lastSynchronization = dateString
}
}
} catch {
StoreCenter.main.log(message: error.localizedDescription)
Logger.error(error)
}
NotificationCenter.default.post(
name: NSNotification.Name.LeStorageDidSynchronize, object: self)
}
/// Processes data that should be inserted or updated inside the app
/// - Parameters:
/// - updates: the server updates
/// - shared: indicates if the content should be flagged as shared
@MainActor 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)")
continue
}
Logger.log(">>> UPDATE \(updateArray.count) \(className)")
let type = try StoreCenter.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()
StoreCenter.main.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 _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)
StoreCenter.main.synchronizationDelete(id: deletedObject.modelId, model: className, storeId: deletedObject.storeId)
} catch {
Logger.error(error)
}
}
}
}
/// Processes data that has been revoked
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)
StoreCenter.main.synchronizationDelete(id: revokedObject.modelId, model: className, storeId: revokedObject.storeId)
} catch {
Logger.error(error)
}
}
}
if let parents {
for level in parents {
for (className, parentData) in level {
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)
}
}
}
}
}
}
/// Returns a Type object for a class name
static func classFromName(_ className: String) throws -> any SyncedStorable.Type {
if let type = ClassLoader.getClass(className) {
if let syncedType = type as? any SyncedStorable.Type {
return syncedType
} else {
throw LeStorageError.cantFindClassFromName(name: className)
}
} else {
throw LeStorageError.cantFindClassFromName(name: className)
}
}
/// 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(identifier: storeId)
self._registerStore(store: store)
return store
}
} else {
return Store.main
}
}
/// Returns whether a data has already been deleted by, to avoid inserting it again
fileprivate func _hasAlreadyBeenDeleted<T: Storable>(_ instance: T) -> Bool {
return self._dataLogs.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: 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
func synchronizationDelete(id: String, model: String, storeId: String?) {
DispatchQueue.main.async {
do {
let type = try StoreCenter.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
func synchronizationRevoke(id: String, model: String, storeId: String?) {
DispatchQueue.main.async {
do {
let type = try StoreCenter.classFromName(model)
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)
}
}
}
/// Returns whether an instance has been shared with the user
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
}
/// Deletes a data log by data id
fileprivate func _cleanupDataLog(dataId: String) {
let logs = self._dataLogs.filter { $0.dataId == dataId }
self._dataLogs.delete(contentOfs: logs)
}
/// Creates a delete log for an instance
func createDeleteLog<T: Storable>(_ instance: T) {
self._addDataLog(instance, method: .delete)
}
/// 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._dataLogs.addOrUpdate(instance: dataLog)
}
// MARK: - Miscellanous
/// Returns the count of api calls for a Type
public func apiCallCount<T: SyncedStorable>(type: T.Type) async -> Int {
do {
let collection: ApiCallCollection<T> = try self.apiCallCollection()
return await collection.items.count
} catch {
return -1
}
}
/// Resets all registered collection
// public func reset() {
// Store.main.reset()
// for store in self._stores.values {
// store.reset()
// }
// }
/// Returns whether any collection has pending API calls
public func hasPendingAPICalls() async -> Bool {
for collection in self._apiCallCollections.values {
if await collection.hasPendingCalls() {
return true
}
}
return false
}
/// Returns the content of the api call file
public func apiCallsFileContent(resourceName: String) async -> String {
return await self._apiCallCollections[resourceName]?.contentOfFile() ?? ""
}
public func apiCalls<T: SyncedStorable>(type: T.Type) async -> [ApiCall<T>] {
if let apiCallCollection: ApiCallCollection<T> = try? self.apiCallCollection() {
return await apiCallCollection.apiCalls()
}
return []
}
/// This method triggers the framework to save and send failed api calls
public func logsFailedAPICalls() {
self._failedAPICallsCollection = Store.main.registerCollection(limit: 50)
}
/// If configured for, logs and send to the server a failed API call
/// Logs a failed API call that has failed at least 5 times
func logFailedAPICall(
_ apiCallId: String, request: URLRequest, collectionName: String, error: String
) {
guard let failedAPICallsCollection = self._failedAPICallsCollection,
let collection = self._apiCallCollections[collectionName],
collectionName != FailedAPICall.resourceName()
else {
return
}
Task {
if let apiCall = await collection.findCallById(apiCallId) {
if !failedAPICallsCollection.contains(where: { $0.callId == apiCallId })
&& apiCall.attemptsCount > 6
{
do {
let authValue = request.allHTTPHeaderFields?["Authorization"]
let string = try apiCall.jsonString()
let failedAPICall = FailedAPICall(
callId: apiCall.id, type: collectionName, apiCall: string, error: error,
authentication: authValue)
DispatchQueue.main.async {
failedAPICallsCollection.addOrUpdate(instance: failedAPICall)
}
} catch {
Logger.error(error)
}
}
}
}
}
/// Logs a failed Api call with its request and error message
func logFailedAPICall(request: URLRequest, error: String) {
guard let failedAPICallsCollection = self._failedAPICallsCollection,
let body: Data = request.httpBody,
let bodyString = String(data: body, encoding: .utf8),
let url = request.url?.absoluteString
else {
return
}
let authValue = request.allHTTPHeaderFields?["Authorization"]
let failedAPICall = FailedAPICall(
callId: request.hashValue.formatted(), type: url, apiCall: bodyString, error: error,
authentication: authValue)
failedAPICallsCollection.addOrUpdate(instance: failedAPICall)
}
/// Adds a userName to the black list
/// Black listed username cannot send data to the server
/// - Parameters:
/// - collection: The collection identifying the Storable type
public func blackListUserName(_ userName: String) {
self._blackListedUserName.append(userName)
}
/// Returns whether the current userName is allowed to sync with the server
func userIsAllowed() -> Bool {
guard let userName = self.userName() else {
return true
}
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 = "\(Store.storageDirectory)/\(identifier)"
FileManager.default.deleteDirectoryInDocuments(directoryName: directory)
self._stores.removeValue(forKey: identifier)
}
// MARK: - Instant update
/// Updates a local object with a server instance
func updateLocalInstances<T: SyncedStorable>(_ results: [T]) {
for result in results {
if let storedCollection: StoredCollection<T> = self.collectionOfInstance(result) {
if storedCollection.findById(result.id) != nil {
storedCollection.updateFromServerInstance(result)
}
}
}
}
/// Updates a local object with a server instance
// func updateFromServerInstance<T: SyncedStorable>(_ result: T) {
// if let storedCollection: StoredCollection<T> = self.collectionOfInstance(result) {
// if storedCollection.findById(result.id) != nil {
// storedCollection.updateFromServerInstance(result)
// }
// }
// }
/// Returns the collection hosting an instance
func collectionOfInstance<T: Storable>(_ instance: T) -> StoredCollection<T>? {
do {
let storedCollection: StoredCollection<T> = try Store.main.collection()
if storedCollection.findById(instance.id) != nil {
return storedCollection
} else {
return self.collectionOfInstanceInSubStores(instance)
}
} catch {
return self.collectionOfInstanceInSubStores(instance)
}
}
/// Search inside the additional stores to find the collection hosting the instance
func collectionOfInstanceInSubStores<T: Storable>(_ instance: T) -> StoredCollection<T>? {
for store in self._stores.values {
let storedCollection: StoredCollection<T>? = try? store.collection()
if storedCollection?.findById(instance.id) != nil {
return storedCollection
}
}
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._dataAccess else {
return []
}
if let dataAccess = dataAccessCollection.first(where: { $0.modelId == modelId }) {
return dataAccess.sharedWith
}
return []
}
/// 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
}
if let dataAccess = dataAccessCollection.first(where: { $0.modelId == instance.stringId }) {
if users.isEmpty {
dataAccessCollection.delete(instance: dataAccess)
} else {
dataAccess.sharedWith.removeAll()
dataAccess.sharedWith = users
}
} else {
let dataAccess = DataAccess(owner: userId, sharedWith: users, modelName: String(describing: type(of: instance)), modelId: instance.stringId)
dataAccessCollection.addOrUpdate(instance: dataAccess)
}
}
// MARK: - Logs
/// Returns the logs collection and instantiates it if necessary
fileprivate func _logsCollection() -> StoredCollection<Log> {
if let logs = self._logs {
return logs
} else {
let logsCollection: StoredCollection<Log> = Store.main.registerCollection(limit: 50)
self._logs = logsCollection
return logsCollection
}
}
/// Logs a message in the logs collection
public func log(message: String) {
DispatchQueue.main.async {
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)
}
}
class ObjectIdentifier: Codable {
var modelId: String
var storeId: String?
}