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.
444 lines
15 KiB
444 lines
15 KiB
//
|
|
// StoreCenter.swift
|
|
// LeStorage
|
|
//
|
|
// Created by Laurent Morvillier on 25/06/2024.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
|
|
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
|
|
|
|
fileprivate var _logs: StoredCollection<Log>? = 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
|
|
|
|
func setUserInfo(user: UserBase) {
|
|
self._settingsStorage.update { settings in
|
|
settings.userId = user.id
|
|
settings.username = user.username
|
|
}
|
|
}
|
|
|
|
/// 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.getValue()
|
|
}
|
|
|
|
/// 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.getValue()
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
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)
|
|
}
|
|
|
|
// MARK: - Logs
|
|
|
|
fileprivate func _logsCollection() -> StoredCollection<Log> {
|
|
if let logs = self._logs {
|
|
return logs
|
|
} else {
|
|
let logsCollection: StoredCollection<Log> = Store.main.registerCollection(synchronized: true)
|
|
self._logs = logsCollection
|
|
return logsCollection
|
|
}
|
|
}
|
|
|
|
/// Logs a message in the logs collection
|
|
public func log(message: String) {
|
|
let log = Log(message: message)
|
|
do {
|
|
try self._logsCollection().addOrUpdate(instance: log)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
// MARK: - Migration
|
|
|
|
public func migrateToken(_ services: Services) throws {
|
|
guard let userName = self.userName() else {
|
|
return
|
|
}
|
|
try self.service().migrateToken(services, userName: userName)
|
|
}
|
|
|
|
}
|
|
|