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

385 lines
13 KiB

//
// 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 SomeCallCollection] = [:]
/// A collection storing FailedAPICall objects
fileprivate var _failedAPICallsCollection: StoredCollection<FailedAPICall>? = nil
/// A list of username that cannot synchronize with the server
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
}
}
/// Registers a store into the list of stores
/// - Parameters:
/// - store: A store to save
fileprivate func _registerStore(store: Store) {
guard let identifier = store.identifier?.value else {
fatalError("The store has no 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<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
}
}
// 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.resetApiCalls()
self._settingsStorage.update { settings in
settings.username = nil
settings.userId = nil
}
}
/// Returns whether the system has a user token
public func hasToken() -> Bool {
do {
_ = try self.service().keychainStore.getToken()
return true
} catch {
return false
}
}
func loadApiCallCollection<T: Storable>(type: T.Type) {
let apiCallCollection = ApiCallCollection<T>()
self._apiCallCollections[T.resourceName()] = apiCallCollection
Task {
do {
try await apiCallCollection.loadFromFile()
} catch {
Logger.error(error)
}
}
}
/// Returns or create the ApiCall collection matching the provided T type
// 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
// }
/// 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.collectionNotRegistered(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: Storable>(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: Storable>(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)
}
}
// 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()
}
}
}
/// Resets the ApiCall whose type identifies with the provided collection
/// - Parameters:
/// - collection: The collection identifying the Storable type
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.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(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)
}
}
/// 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)
}
/// Returns whether the collection can synchronize
fileprivate func _canSynchronise() -> Bool {
return self.collectionsCanSynchronize && self.userIsAllowed()
}
/// Transmit the insertion request to the ApiCall collection
/// - Parameters:
/// - instance: an object to insert
func sendInsertion<T: Storable>(_ 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: Storable>(_ 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: Storable>(_ instance: T) async throws -> T? {
guard self._canSynchronise() else {
return nil
}
return try await self.apiCallCollection().sendDeletion(instance)
}
}