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.
247 lines
8.2 KiB
247 lines
8.2 KiB
//
|
|
// Store.swift
|
|
// LeStorage
|
|
//
|
|
// Created by Laurent Morvillier on 02/02/2024.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
|
|
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()
|
|
|
|
// public fileprivate(set) var currentUser: User? = nil
|
|
|
|
/// 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?
|
|
|
|
/// The service instance
|
|
public var service: Services? {
|
|
return self._services
|
|
}
|
|
|
|
/// The dictionary of registered StoredCollections
|
|
fileprivate var _collections: [String : any SomeCollection] = [:]
|
|
|
|
// /// The dictionary of ApiCall StoredCollections corresponding to the synchronized registered collections
|
|
// fileprivate var _apiCallsCollections: [String : any SomeCollection] = [:]
|
|
|
|
/// The list of migrations to apply
|
|
fileprivate var _migrations: [SomeMigration] = []
|
|
|
|
/// The collection of performed migration on the store
|
|
fileprivate lazy var _migrationCollection: StoredCollection<MigrationHistory> = { StoredCollection(synchronized: false, store: Store.main, asynchronousIO: false)
|
|
}()
|
|
|
|
fileprivate var settingsStorage: MicroStorage<Settings> = MicroStorage()
|
|
|
|
public init() { }
|
|
|
|
/// 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) -> StoredCollection<T> {
|
|
|
|
// register collection
|
|
let collection = StoredCollection<T>(synchronized: synchronized, store: Store.main, indexed: indexed, loadCompletion: nil)
|
|
self._collections[T.resourceName()] = collection
|
|
|
|
// if synchronized { // register additional collection for api calls
|
|
// let apiCallCollection = StoredCollection<ApiCall<T>>(synchronized: false, store: Store.main, loadCompletion: { apiCallCollection in
|
|
// self._rescheduleCalls(collection: apiCallCollection)
|
|
// })
|
|
// self._apiCallsCollections[T.resourceName()] = apiCallCollection
|
|
// }
|
|
|
|
return collection
|
|
}
|
|
|
|
// MARK: - Settings
|
|
|
|
func setUserUUID(uuidString: String) {
|
|
self.settingsStorage.update { settings in
|
|
settings.userUUIDString = uuidString
|
|
}
|
|
}
|
|
|
|
public func currentUserUUID() throws -> UUID {
|
|
if let uuidString = self.settingsStorage.item.userUUIDString, let uuid = UUID(uuidString: uuidString) {
|
|
return uuid
|
|
} else {
|
|
let uuid = UIDevice.current.identifierForVendor ?? UUID()
|
|
self.settingsStorage.update { settings in
|
|
settings.userUUIDString = uuid.uuidString
|
|
}
|
|
return uuid
|
|
}
|
|
}
|
|
|
|
func userName() -> String? {
|
|
return self.settingsStorage.item.username
|
|
}
|
|
|
|
func setUserName(_ username: String) {
|
|
self.settingsStorage.item.username = username
|
|
}
|
|
|
|
public func disconnect() {
|
|
try? self.service?.disconnect()
|
|
self.settingsStorage.item.userUUIDString = nil
|
|
}
|
|
|
|
public func hasToken() -> Bool {
|
|
guard let service else { return false }
|
|
do {
|
|
_ = try 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: - Migration
|
|
|
|
/// [beta] Adds a migration to perform
|
|
public func addMigration(_ migration: SomeMigration) {
|
|
self._migrations.append(migration)
|
|
}
|
|
|
|
/// [beta] Performs the migration if necessary
|
|
func performMigrationIfNecessary<T: Storable>(_ collection: StoredCollection<T>) async throws {
|
|
|
|
// Check for migrations
|
|
let migrations = self._migrations.filter { $0.resourceName == T.resourceName() }
|
|
if migrations.isEmpty { return }
|
|
|
|
// Check for applied migrations
|
|
var version: Int = -1
|
|
var performedMigration: MigrationHistory? = self._migrationCollection.first(where: { $0.resourceName == T.resourceName() })
|
|
if let performedMigration {
|
|
version = Int(performedMigration.version)
|
|
}
|
|
|
|
// Apply necessary migrations
|
|
let applicableMigrations = migrations.filter { $0.version > version }
|
|
.sorted(keyPath: \.version)
|
|
for migration in applicableMigrations {
|
|
|
|
Logger.log("Start migration for \(migration.resourceName), version: \(migration.version)")
|
|
|
|
try migration.migrate(synchronized: collection.synchronized)
|
|
|
|
if let performedMigration {
|
|
performedMigration.version = migration.version
|
|
} else {
|
|
performedMigration = MigrationHistory(version: migration.version, resourceName: migration.resourceName)
|
|
}
|
|
|
|
try self._migrationCollection.addOrUpdate(instance: performedMigration!)
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
let collection: StoredCollection<T> = try self.collection()
|
|
collection.rescheduleApiCallsIfNecessary()
|
|
}
|
|
|
|
/// Executes an ApiCall
|
|
fileprivate func _executeApiCall<T: Storable>(_ apiCall: ApiCall<T>) async throws -> T {
|
|
guard let service else {
|
|
throw StoreError.missingService
|
|
}
|
|
return try await service.runApiCall(apiCall)
|
|
}
|
|
|
|
/// Executes an ApiCall
|
|
// func execute<T>(apiCall: ApiCall<T>) async throws {
|
|
// _ = try await self._executeApiCall(apiCall)
|
|
// }
|
|
|
|
func execute<T>(apiCall: ApiCall<T>) async throws -> T {
|
|
return try await self._executeApiCall(apiCall)
|
|
}
|
|
|
|
/// Retrieves all the items on the server
|
|
func getItems<T: Storable>() async throws -> [T] {
|
|
guard let service else {
|
|
throw StoreError.missingService
|
|
}
|
|
return try await service.get()
|
|
}
|
|
|
|
public func reset() {
|
|
for collection in self._collections.values {
|
|
collection.reset()
|
|
}
|
|
}
|
|
|
|
}
|
|
|