// // Swiftfin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this // file, you can obtain one at https://mozilla.org/MPL/2.0/. // // Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine import CoreStore import Defaults import Factory import Foundation import SwiftUI extension SwiftfinStore.V2 { /// Used to store arbitrary data with a `name` and `ownerID`. /// /// Essentially just a bag-of-bytes model like UserDefaults, but for /// storing larger objects or arbitrary collection elements. /// /// Relationships generally take the form below, where `ownerID` is like /// an object, `domain`s are property names, and `key`s are values within /// the `domain`. An instance where `domain == key` is like a single-value /// property while a `domain` with many `keys` is like a dictionary. /// /// ownerID /// - domain /// - key(s) /// - domain /// - key(s) /// /// This can be useful to not require migrations on model objects for new /// "properties". final class AnyData: CoreStoreObject { @Field.Stored("data") var data: Data? @Field.Stored("domain") var domain: String = "" @Field.Stored("key") var key: String = "" @Field.Stored("ownerID") var ownerID: String = "" } } extension AnyStoredData { /// Note: if `domain == nil`, will default to "none" to avoid local typing issues. static func fetch(_ key: String, ownerID: String, domain: String? = nil) throws -> Value? { let domain = domain ?? "none" let ownerFilter: Where = Where(\.$ownerID == ownerID) let keyFilter: Where = Where(\.$key == key) let domainFilter: Where = Where(\.$domain == domain) let clause = From() .where(ownerFilter && keyFilter && domainFilter) let values = try SwiftfinStore.dataStack .fetchAll( clause ) .compactMap(\.data) .compactMap { try JSONDecoder().decode(Value.self, from: $0) } assert(values.count < 2, "More than one stored object for same name, id, and domain!") return values.first } /// Note: if `domain == nil`, will default to "none" to avoid local typing issues. static func store(value: Value, key: String, ownerID: String, domain: String? = nil) throws { let domain = domain ?? "none" let ownerFilter: Where = Where(\.$ownerID == ownerID) let keyFilter: Where = Where(\.$key == key) let domainFilter: Where = Where(\.$domain == domain) let clause = From() .where(ownerFilter && keyFilter && domainFilter) try SwiftfinStore.dataStack.perform { transaction in let existing = try transaction.fetchAll(clause) assert(existing.count < 2, "More than one stored object for same name, id, and domain!") let encodedData = try JSONEncoder().encode(value) if let existingObject = existing.first { let edit = transaction.edit(existingObject) edit?.data = encodedData } else { let newData = transaction.create(Into()) newData.data = encodedData newData.domain = domain newData.ownerID = ownerID newData.key = key } } } /// Creates a fetch clause to be used within local transactions static func fetchClause(ownerID: String) -> FetchChainBuilder { From() .where(\.$ownerID == ownerID) } /// Creates a fetch clause to be used within local transactions /// /// Note: if `domain == nil`, will default to "none" static func fetchClause(ownerID: String, domain: String? = nil) throws -> FetchChainBuilder { let domain = domain ?? "none" return From() .where(\.$ownerID == ownerID && \.$domain == domain) } /// Creates a fetch clause to be used within local transactions /// /// Note: if `domain == nil`, will default to "none" static func fetchClause(key: String, ownerID: String, domain: String? = nil) throws -> FetchChainBuilder { let domain = domain ?? "none" let ownerFilter: Where = Where(\.$ownerID == ownerID) let keyFilter: Where = Where(\.$key == key) let domainFilter: Where = Where(\.$domain == domain) return From() .where(ownerFilter && keyFilter && domainFilter) } /// Delete all data with the given `ownerID` /// /// Note: if performing deletion with another transaction, use `fetchClause` /// instead to delete within the other transaction static func deleteAll(ownerID: String) throws { try SwiftfinStore.dataStack.perform { transaction in let values = try transaction.fetchAll(fetchClause(ownerID: ownerID)) transaction.delete(values) } } /// Delete all data with the given `ownerID` and `domain` /// /// Note: if performing deletion with another transaction, use `fetchClause` /// instead to delete within the other transaction /// Note: if `domain == nil`, will default to "none" static func deleteAll(ownerID: String, domain: String? = nil) throws { try SwiftfinStore.dataStack.perform { transaction in let values = try transaction.fetchAll(fetchClause(ownerID: ownerID, domain: domain)) transaction.delete(values) } } /// Delete all data given `key`, `ownerID`, and `domain`. /// /// /// Note: if performing deletion with another transaction, use `fetchClause` /// instead to delete within the other transaction /// Note: if `domain == nil`, will default to "none" static func delete(key: String, ownerID: String, domain: String? = nil) throws { try SwiftfinStore.dataStack.perform { transaction in let values = try transaction.fetchAll(fetchClause(key: key, ownerID: ownerID, domain: domain)) transaction.delete(values) } } }