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.
264 lines
8.1 KiB
264 lines
8.1 KiB
//
|
|
// Store.swift
|
|
// LeStorage
|
|
//
|
|
// Created by Laurent Morvillier on 02/02/2024.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
|
|
public enum ResetOption {
|
|
case all
|
|
case synchronizedOnly
|
|
}
|
|
|
|
enum StoreError: Error {
|
|
case missingService
|
|
case unexpectedCollectionType(name: String)
|
|
case apiCallCollectionNotRegistered(type: String)
|
|
case collectionNotRegistered(type: String)
|
|
case unSynchronizedCollection
|
|
}
|
|
|
|
public class Store {
|
|
|
|
/// The Store singleton
|
|
public static let main = Store()
|
|
|
|
/// A method to provide ids corresponding to the django storage
|
|
public static func randomId() -> String {
|
|
return UUID().uuidString.lowercased()
|
|
}
|
|
|
|
/// The URL of the django API
|
|
public var synchronizationApiURL: String? {
|
|
didSet {
|
|
if let url = synchronizationApiURL {
|
|
self._services = Services(url: url)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The services performing the API calls
|
|
fileprivate var _services: Services?
|
|
|
|
public func service() throws -> Services {
|
|
if let service = self._services {
|
|
return service
|
|
} else {
|
|
throw StoreError.missingService
|
|
}
|
|
}
|
|
|
|
/// The dictionary of registered StoredCollections
|
|
fileprivate var _collections: [String : any SomeCollection] = [:]
|
|
|
|
/// A store for the Settings object
|
|
fileprivate var _settingsStorage: MicroStorage<Settings> = MicroStorage(fileName: "settings.json")
|
|
|
|
/// The name of the directory to store the json files
|
|
static let storageDirectory = "storage"
|
|
|
|
/// Indicates to Stored Collection if they can synchronize
|
|
public var collectionsCanSynchronize: Bool = true {
|
|
didSet {
|
|
Logger.log(">>> collectionsCanSynchronize = \(self.collectionsCanSynchronize)")
|
|
}
|
|
}
|
|
|
|
public init() {
|
|
FileManager.default.createDirectoryInDocuments(directoryName: Store.storageDirectory)
|
|
}
|
|
|
|
/// Registers a collection
|
|
/// [synchronize] denotes a collection which modification will be sent to the django server
|
|
public func registerCollection<T : Storable>(synchronized: Bool, indexed: Bool = false, inMemory: Bool = false, sendsUpdate: Bool = true) -> StoredCollection<T> {
|
|
|
|
// register collection
|
|
let collection = StoredCollection<T>(synchronized: synchronized, store: Store.main, indexed: indexed, inMemory: inMemory, sendsUpdate: sendsUpdate, loadCompletion: nil)
|
|
self._collections[T.resourceName()] = collection
|
|
|
|
return collection
|
|
}
|
|
|
|
/// Registers a StoredSingleton instance
|
|
public func registerObject<T : Storable>(synchronized: Bool, inMemory: Bool = false, sendsUpdate: Bool = true) -> StoredSingleton<T> {
|
|
|
|
// register collection
|
|
let storedObject = StoredSingleton<T>(synchronized: synchronized, store: Store.main, inMemory: inMemory, sendsUpdate: sendsUpdate, loadCompletion: nil)
|
|
self._collections[T.resourceName()] = storedObject
|
|
|
|
return storedObject
|
|
}
|
|
|
|
// MARK: - Settings
|
|
|
|
/// Stores the user UUID
|
|
func setUserUUID(uuidString: String) {
|
|
self._settingsStorage.update { settings in
|
|
settings.userId = uuidString
|
|
}
|
|
}
|
|
|
|
/// Returns the user's UUID
|
|
public var currentUserUUID: UUID? {
|
|
if let uuidString = self._settingsStorage.item.userId,
|
|
let uuid = UUID(uuidString: uuidString) {
|
|
return uuid
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/// Returns a UUID, using the user's if possible
|
|
public func mandatoryUserUUID() -> UUID {
|
|
if let uuid = self.currentUserUUID {
|
|
return uuid
|
|
} else {
|
|
let uuid = UIDevice.current.identifierForVendor ?? UUID()
|
|
self._settingsStorage.update { settings in
|
|
settings.userId = uuid.uuidString
|
|
}
|
|
return uuid
|
|
}
|
|
}
|
|
|
|
/// Returns the username
|
|
func userName() -> String? {
|
|
return self._settingsStorage.item.username
|
|
}
|
|
|
|
/// Sets the username
|
|
func setUserName(_ username: String) {
|
|
self._settingsStorage.update { settings in
|
|
settings.username = username
|
|
}
|
|
}
|
|
|
|
/// Disconnect the user from the storage and resets collection
|
|
public func disconnect(resetOption: ResetOption? = nil) {
|
|
try? self.service().disconnect()
|
|
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
|
|
}
|
|
}
|
|
|
|
// MARK: - Convenience
|
|
|
|
/// Looks for an instance by id
|
|
public func findById<T: Storable>(_ id: String) -> T? {
|
|
guard let collection = self._collections[T.resourceName()] as? StoredCollection<T> else {
|
|
Logger.w("Collection \(T.resourceName()) not registered")
|
|
return nil
|
|
}
|
|
return collection.findById(id)
|
|
}
|
|
|
|
/// Filters a collection with a [isIncluded] predicate
|
|
public func filter<T: Storable>(isIncluded: (T) throws -> (Bool)) rethrows -> [T] {
|
|
do {
|
|
return try self.collection().filter(isIncluded)
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
/// Returns a collection by type
|
|
func collection<T: Storable>() throws -> StoredCollection<T> {
|
|
if let collection = self._collections[T.resourceName()] as? StoredCollection<T> {
|
|
return collection
|
|
}
|
|
throw StoreError.collectionNotRegistered(type: T.resourceName())
|
|
}
|
|
|
|
/// Deletes the dependencies of a collection
|
|
public func deleteDependencies<T: Storable>(items: any Sequence<T>) throws {
|
|
try self.collection().deleteDependencies(items)
|
|
}
|
|
|
|
// MARK: - Api call rescheduling
|
|
|
|
/// Deletes an ApiCall by [id] and [collectionName]
|
|
func deleteApiCallById(_ id: String, collectionName: String) throws {
|
|
if let collection = self._collections[collectionName] {
|
|
try collection.deleteApiCallById(id)
|
|
} else {
|
|
throw StoreError.collectionNotRegistered(type: collectionName)
|
|
}
|
|
}
|
|
|
|
/// Reschedule an ApiCall by id
|
|
func rescheduleApiCall<T: Storable>(id: String, type: T.Type) throws {
|
|
guard self.collectionsCanSynchronize else {
|
|
return
|
|
}
|
|
let collection: StoredCollection<T> = try self.collection()
|
|
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>() async throws -> [T] {
|
|
return try await self.service().get()
|
|
}
|
|
|
|
/// Resets all registered collection
|
|
public func reset() {
|
|
for collection in self._collections.values {
|
|
collection.reset()
|
|
}
|
|
}
|
|
|
|
/// Loads all collection with the data from the server
|
|
public func loadCollectionFromServer() {
|
|
for collection in self._collections.values {
|
|
Task {
|
|
try? await collection.loadDataFromServerIfAllowed()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns whether any collection has pending API calls
|
|
public func hasPendingAPICalls() -> Bool {
|
|
return self._collections.values.contains(where: { $0.hasPendingAPICalls() })
|
|
}
|
|
|
|
}
|
|
|