parent
3b9c12c868
commit
ae5c292795
@ -0,0 +1,361 @@ |
||||
// |
||||
// StoreCenter.swift |
||||
// LeStorage |
||||
// |
||||
// Created by Laurent Morvillier on 25/06/2024. |
||||
// |
||||
|
||||
import Foundation |
||||
|
||||
public class StoreCenter { |
||||
|
||||
public static let main: StoreCenter = StoreCenter() |
||||
|
||||
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 |
||||
|
||||
/// 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 dictionary of registered StoredCollections |
||||
fileprivate var _apiCallCollections: [String : any ACCollection] = [:] |
||||
|
||||
fileprivate var _failedAPICallsCollection: StoredCollection<FailedAPICall>? = nil |
||||
|
||||
fileprivate var blackListedUserName: [String] = [] |
||||
|
||||
init() { |
||||
self._loadExistingApiCollections() |
||||
} |
||||
|
||||
/// Returns the service instance |
||||
public func service() throws -> Services { |
||||
if let service = self._services { |
||||
return service |
||||
} else { |
||||
throw StoreError.missingService |
||||
} |
||||
} |
||||
|
||||
fileprivate func _registerStore(store: Store) { |
||||
guard let identifier = store.identifier?.value else { |
||||
fatalError("The store has no identifier") |
||||
} |
||||
self._stores[identifier] = store |
||||
} |
||||
|
||||
public func store<T: Store>(identifier: String, parameter: String) -> T { |
||||
if let store = self._stores[identifier] as? T { |
||||
return store |
||||
} else { |
||||
let store = T(identifier: identifier, parameter: parameter) |
||||
self._registerStore(store: store) |
||||
return store |
||||
} |
||||
} |
||||
|
||||
fileprivate func _loadExistingApiCollections() { |
||||
let string = "clubs" |
||||
|
||||
|
||||
} |
||||
|
||||
// MARK: - Settings |
||||
|
||||
/// Stores the user UUID |
||||
func setUserUUID(uuidString: String) { |
||||
self._settingsStorage.update { settings in |
||||
settings.userId = uuidString |
||||
} |
||||
} |
||||
|
||||
/// 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 |
||||
} |
||||
|
||||
/// Sets the username |
||||
func setUserName(_ username: String) { |
||||
self._settingsStorage.update { settings in |
||||
settings.username = username |
||||
} |
||||
} |
||||
|
||||
/// Returns the stored token |
||||
public func token() -> String? { |
||||
return try? self.service().keychainStore.getToken() |
||||
} |
||||
|
||||
/// Disconnect the user from the storage and resets collection |
||||
public func disconnect() { |
||||
try? self.service().deleteToken() |
||||
self._settingsStorage.update { settings in |
||||
settings.username = nil |
||||
settings.userId = nil |
||||
} |
||||
|
||||
// switch resetOption { |
||||
// case .all: |
||||
// for collection in self._collections.values { |
||||
// collection.reset() |
||||
// } |
||||
// case .synchronizedOnly: |
||||
// for collection in self._collections.values { |
||||
// if collection.synchronized { |
||||
// collection.reset() |
||||
// } |
||||
// } |
||||
// default: |
||||
// break |
||||
// } |
||||
|
||||
} |
||||
|
||||
/// Returns whether the system has a user token |
||||
public func hasToken() -> Bool { |
||||
do { |
||||
_ = try self.service().keychainStore.getToken() |
||||
return true |
||||
} catch { |
||||
return false |
||||
} |
||||
} |
||||
|
||||
func getOrCreateApiCallCollection<T: Storable>() -> ApiCallCollection<T> { |
||||
if let apiCallCollection = self._apiCallCollections[T.resourceName()] as? ApiCallCollection<T> { |
||||
return apiCallCollection |
||||
} |
||||
let apiCallCollection = ApiCallCollection<T>() |
||||
self._apiCallCollections[T.resourceName()] = apiCallCollection |
||||
return apiCallCollection |
||||
// apiCallCollection.loadFromFile() |
||||
} |
||||
|
||||
func apiCallCollection<T: Storable>() throws -> ApiCallCollection<T> { |
||||
if let collection = self._apiCallCollections[T.resourceName()] as? ApiCallCollection<T> { |
||||
return collection |
||||
} |
||||
throw StoreError.collectionNotRegistered(type: T.resourceName()) |
||||
} |
||||
|
||||
func deleteApiCallByDataId<T: Storable>(type: T.Type, id: String) async throws { |
||||
let apiCallCollection: ApiCallCollection<T> = try self.apiCallCollection() |
||||
await apiCallCollection.deleteByDataId(id) |
||||
} |
||||
|
||||
func deleteApiCallById<T: Storable>(type: T.Type, id: String) async throws { |
||||
let apiCallCollection: ApiCallCollection<T> = try self.apiCallCollection() |
||||
await apiCallCollection.deleteById(id) |
||||
} |
||||
|
||||
func deleteApiCallById(_ id: String, collectionName: String) async throws { |
||||
if let apiCallCollection = self._apiCallCollections[collectionName] { |
||||
await apiCallCollection.deleteById(id) |
||||
} else { |
||||
throw StoreError.collectionNotRegistered(type: collectionName) |
||||
} |
||||
} |
||||
|
||||
// MARK: - Api call rescheduling |
||||
|
||||
/// Reschedule an ApiCall by id |
||||
func rescheduleApiCalls<T: Storable>(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: Storable>(_ apiCall: ApiCall<T>) async throws -> T { |
||||
return try await self.service().runApiCall(apiCall) |
||||
} |
||||
|
||||
/// Executes an API call |
||||
func execute<T>(apiCall: ApiCall<T>) async throws -> T { |
||||
return try await self._executeApiCall(apiCall) |
||||
} |
||||
|
||||
// MARK: - |
||||
|
||||
/// Retrieves all the items on the server |
||||
func getItems<T: Storable>(identifier: StoreIdentifier? = nil) async throws -> [T] { |
||||
return try await self.service().get(identifier: identifier) |
||||
} |
||||
|
||||
/// Resets all registered collection |
||||
public func reset() { |
||||
Store.main.reset() |
||||
for store in self._stores.values { |
||||
store.reset() |
||||
} |
||||
} |
||||
|
||||
/// Resets all the api call collections |
||||
public func resetApiCalls() { |
||||
Task { |
||||
for collection in self._apiCallCollections.values { |
||||
await collection.reset() |
||||
} |
||||
} |
||||
} |
||||
|
||||
public func resetApiCalls<T: Storable>(collection: StoredCollection<T>) { |
||||
do { |
||||
let apiCallCollection: ApiCallCollection<T> = try self.apiCallCollection() |
||||
Task { |
||||
await apiCallCollection.reset() |
||||
} |
||||
} catch { |
||||
Logger.error(error) |
||||
} |
||||
} |
||||
|
||||
/// Returns whether any collection has pending API calls |
||||
public func hasPendingAPICalls() async -> Bool { |
||||
for collection in self._apiCallCollections.values { |
||||
if await collection.hasPendingAPICalls() { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
/// Returns the content of the api call file |
||||
public func apiCallsFileContent(resourceName: String) async -> String { |
||||
return await self._apiCallCollections[resourceName]?.contentOfApiCallFile() ?? "" |
||||
} |
||||
|
||||
/// This method triggers the framework to save and send failed api calls |
||||
public func logsFailedAPICalls() { |
||||
self._failedAPICallsCollection = Store.main.registerCollection(synchronized: true) |
||||
} |
||||
|
||||
/// 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) |
||||
try 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"] |
||||
|
||||
do { |
||||
let failedAPICall = FailedAPICall(callId: request.hashValue.formatted(), type: url, apiCall: bodyString, error: error, authentication: authValue) |
||||
try failedAPICallsCollection.addOrUpdate(instance: failedAPICall) |
||||
} catch { |
||||
Logger.error(error) |
||||
} |
||||
|
||||
} |
||||
|
||||
public func blackListUserName(_ userName: String) { |
||||
self.blackListedUserName.append(userName) |
||||
} |
||||
|
||||
func userIsAllowed() -> Bool { |
||||
guard let userName = self.userName() else { |
||||
return true |
||||
} |
||||
return !self.blackListedUserName.contains(where: { $0 == userName } ) |
||||
} |
||||
|
||||
// fileprivate func _registerStore(identifier: String, parameter: String) -> Store { |
||||
// let store = Store(identifier: identifier, parameter: parameter) |
||||
// self._stores[identifier] = store |
||||
// return store |
||||
// } |
||||
// |
||||
// public func store(identifier: String, parameter: String) -> Store { |
||||
// if let store = self._stores[identifier] { |
||||
// return store |
||||
// } |
||||
// return self._registerStore(identifier: identifier, parameter: parameter) |
||||
// } |
||||
|
||||
public func destroyStore(identifier: String) { |
||||
let directory = "\(Store.storageDirectory)/\(identifier)" |
||||
FileManager.default.deleteDirectoryInDocuments(directoryName: directory) |
||||
} |
||||
|
||||
/// Returns whether the collection can synchronize |
||||
fileprivate func _canSynchronise() -> Bool { |
||||
return self.collectionsCanSynchronize && self.userIsAllowed() |
||||
} |
||||
|
||||
func sendInsertion<T: Storable>(_ instance: T) async throws { |
||||
guard self._canSynchronise() else { |
||||
return |
||||
} |
||||
try await self.apiCallCollection().sendInsertion(instance) |
||||
} |
||||
|
||||
func sendUpdate<T: Storable>(_ instance: T) async throws { |
||||
guard self._canSynchronise() else { |
||||
return |
||||
} |
||||
try await self.apiCallCollection().sendUpdate(instance) |
||||
} |
||||
|
||||
func sendDeletion<T: Storable>(_ instance: T) async throws { |
||||
guard self._canSynchronise() else { |
||||
return |
||||
} |
||||
try await self.apiCallCollection().sendDeletion(instance) |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue