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.
788 lines
26 KiB
788 lines
26 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] = [:]
|
|
|
|
/// The URL of the django API
|
|
// public var synchronizationApiURL: String? {
|
|
// didSet {
|
|
// if let url = synchronizationApiURL {
|
|
// self._services = Services(url: url)
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
/// 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 _syncGetRequests: ApiCallCollection<GetSyncData>
|
|
|
|
/// A collection of DataLog objects, used for the synchronization
|
|
fileprivate var _dataLogs: StoredCollection<DataLog>
|
|
|
|
/// 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()
|
|
}
|
|
}
|
|
|
|
public func configureURLs(httpScheme: String, domain: String) {
|
|
let urlManager: URLManager = URLManager(httpScheme: httpScheme, domain: domain)
|
|
self._urlManager = urlManager
|
|
self._services = Services(url: urlManager.api)
|
|
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)")
|
|
}
|
|
|
|
/// 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 setUserInfo(user: UserBase) {
|
|
self._settingsStorage.update { settings in
|
|
settings.userId = user.id
|
|
settings.username = user.username
|
|
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._settingsStorage.update { settings in
|
|
settings.username = nil
|
|
settings.userId = nil
|
|
settings.lastSynchronization = nil
|
|
|
|
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()
|
|
} 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()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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>(id: String, 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 execute<T: SyncedStorable, V: Decodable>(apiCall: ApiCall<T>) async throws -> V {
|
|
return try await self._executeApiCall(apiCall)
|
|
}
|
|
|
|
// MARK: - Api calls
|
|
|
|
/// Returns whether the collection can synchronize
|
|
fileprivate func _canSynchronise() -> Bool {
|
|
return !self.forceNoSynchronization && self.collectionsCanSynchronize
|
|
&& self.userIsAllowed()
|
|
}
|
|
|
|
/// 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
|
|
|
|
fileprivate func _createSyncApiCallCollection() {
|
|
self.loadApiCallCollection(type: GetSyncData.self)
|
|
}
|
|
|
|
public func initialSynchronization() {
|
|
self._settingsStorage.update { settings in
|
|
settings.lastSynchronization = Date()
|
|
}
|
|
Store.main.loadCollectionsFromServer()
|
|
}
|
|
|
|
func synchronizeLastUpdates() async throws {
|
|
|
|
if let lastSync = self._settingsStorage.item.lastSynchronization {
|
|
|
|
let syncGetCollection: ApiCallCollection<GetSyncData> = try self.apiCallCollection()
|
|
if await syncGetCollection.hasPendingCalls() {
|
|
await syncGetCollection.rescheduleApiCallsIfNecessary()
|
|
} else {
|
|
let getSyncData = GetSyncData()
|
|
getSyncData.lastUpdate = lastSync
|
|
try await syncGetCollection.sendGetRequest(instance: getSyncData)
|
|
}
|
|
} else {
|
|
Logger.w("Can't sync due to missing saved date")
|
|
}
|
|
|
|
// let lastSync: Date? = self._settingsStorage.item.lastSynchronization
|
|
// try await self._services?.synchronizeLastUpdates(since: lastSync)
|
|
}
|
|
|
|
func synchronizeContent(_ data: Data, date: Date) {
|
|
|
|
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] {
|
|
do {
|
|
try self._parseSyncUpdates(updates)
|
|
} catch {
|
|
StoreCenter.main.log(message: error.localizedDescription)
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
if let deletions = json["deletions"] as? [String: Any] {
|
|
do {
|
|
try self._parseSyncDeletions(deletions)
|
|
} catch {
|
|
StoreCenter.main.log(message: error.localizedDescription)
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
self._settingsStorage.update { settings in
|
|
settings.lastSynchronization = date
|
|
}
|
|
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
fileprivate func _parseSyncUpdates(_ updates: [String: Any]) throws {
|
|
for (className, updateData) in updates {
|
|
guard let updateArray = updateData as? [[String: Any]] else {
|
|
Logger.w("Invalid update data for \(className)")
|
|
continue
|
|
}
|
|
|
|
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)
|
|
|
|
let storeId: String? = decodedObject.getStoreId()
|
|
StoreCenter.main.synchronizationAddOrUpdate(decodedObject, storeId: storeId)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate func _parseSyncDeletions(_ deletions: [String: Any]) throws {
|
|
for (className, updateDeletions) in deletions {
|
|
guard let deletedItem = updateDeletions as? [Any] else {
|
|
Logger.w("Invalid update data for \(className)")
|
|
continue
|
|
}
|
|
|
|
for deleted in deletedItem {
|
|
|
|
do {
|
|
let data = try JSONSerialization.data(withJSONObject: deleted, options: [])
|
|
let deletedObject = try JSON.decoder.decode(DeletedObject.self, from: data)
|
|
|
|
StoreCenter.main.synchronizationDelete(id: deletedObject.modelId, model: className, storeId: deletedObject.storeId)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
static func classFromName(_ className: String) throws -> any SyncedStorable.Type {
|
|
|
|
guard let projectName = Bundle.main.infoDictionary?["CFBundleName"] as? String else {
|
|
throw LeStorageError.cantAccessCFBundleName
|
|
}
|
|
|
|
let modelClass: AnyClass? = NSClassFromString("\(projectName).\(className)")
|
|
if let type = modelClass as? any SyncedStorable.Type {
|
|
return type
|
|
} else {
|
|
throw LeStorageError.cantFindClassFromName(name: className)
|
|
}
|
|
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
fileprivate func _hasAlreadyBeenDeleted<T: Storable>(_ instance: T) -> Bool {
|
|
return self._dataLogs.contains(where: {
|
|
$0.dataId == instance.stringId && $0.operation == .delete
|
|
})
|
|
}
|
|
|
|
func synchronizationAddOrUpdate<T: SyncedStorable>(_ instance: T, storeId: String?) {
|
|
let hasAlreadyBeenDeleted: Bool = self._hasAlreadyBeenDeleted(instance)
|
|
if !hasAlreadyBeenDeleted {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
|
self._store(id: storeId).addOrUpdateIfNewer(instance)
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// func synchronizationDelete<T: Storable>(instance: T, storeId: String?) {
|
|
// DispatchQueue.main.async {
|
|
// self._store(id: storeId)?.deleteNoSync(instance: instance)
|
|
// self._cleanupDataLog(dataId: instance.stringId)
|
|
// }
|
|
// }
|
|
|
|
fileprivate func _cleanupDataLog(dataId: String) {
|
|
let logs = self._dataLogs.filter { $0.dataId == dataId }
|
|
self._dataLogs.delete(contentOfs: logs)
|
|
}
|
|
|
|
// func createInsertLog<T: Storable>(_ instance: T) {
|
|
// self._addDataLog(instance, method: .post)
|
|
// }
|
|
|
|
func createDeleteLog<T: Storable>(_ instance: T) {
|
|
self._addDataLog(instance, method: .delete)
|
|
}
|
|
|
|
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
|
|
|
|
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() ?? ""
|
|
}
|
|
|
|
/// This method triggers the framework to save and send failed api calls
|
|
public func logsFailedAPICalls() {
|
|
self._failedAPICallsCollection = Store.main.registerCollection()
|
|
}
|
|
|
|
/// 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 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: - 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()
|
|
self._logs = logsCollection
|
|
return logsCollection
|
|
}
|
|
}
|
|
|
|
/// Logs a message in the logs collection
|
|
public func log(message: String) {
|
|
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 DeletedObject: Codable {
|
|
var modelId: String
|
|
var storeId: String?
|
|
}
|
|
|