From e4bed5d8244e557f2aa670fa04b9bf027e8284cd Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Mon, 14 Oct 2024 15:59:42 -0400 Subject: [PATCH 1/9] reorganzing files [skip ci] --- .../{ => Databases}/BackgroundDatabase.swift | 0 .../{ => Databases}/Database+Extras.swift | 0 .../{ => Databases}/Database+ModelContext.swift | 0 Sources/DataThespian/{ => Databases}/Database.swift | 0 .../EnvironmentValues+Database.swift} | 9 +-------- .../{ => Databases}/ModelActor+Database.swift | 0 .../{ => Databases}/ModelActorDatabase.swift | 0 .../{ => Notification}/AgentRegister.swift | 0 .../Combine}/DatabaseChangePublicist.swift | 0 .../Combine}/DatabaseChangePublicistKey.swift | 11 +---------- .../{ => Notification/Combine}/PublishingAgent.swift | 0 .../Combine}/PublishingRegister.swift | 0 .../DataThespian/{ => Notification}/DataAgent.swift | 0 .../DataThespian/{ => Notification}/DataMonitor.swift | 0 .../{ => Notification}/DatabaseChangeSet.swift | 0 .../{ => Notification}/DatabaseChangeType.swift | 0 .../{ => Notification}/DatabaseMonitoring.swift | 0 .../{ => Notification}/ManagedObjectMetadata.swift | 0 .../{ => Notification}/Notification.swift | 0 .../{ => Notification}/NotificationDataUpdate.swift | 0 .../{ => Notification}/RegistrationCollection.swift | 0 .../{ => SwiftData}/FetchDescriptor.swift | 2 ++ .../{ => SwiftData}/ModelContext+Extension.swift | 8 +++++++- .../DataThespian/{ => SwiftData}/ModelContext.swift | 8 +++++--- .../{ => SwiftData}/NSManagedObjectID.swift | 0 25 files changed, 16 insertions(+), 22 deletions(-) rename Sources/DataThespian/{ => Databases}/BackgroundDatabase.swift (100%) rename Sources/DataThespian/{ => Databases}/Database+Extras.swift (100%) rename Sources/DataThespian/{ => Databases}/Database+ModelContext.swift (100%) rename Sources/DataThespian/{ => Databases}/Database.swift (100%) rename Sources/DataThespian/{DatabaseKey.swift => Databases/EnvironmentValues+Database.swift} (88%) rename Sources/DataThespian/{ => Databases}/ModelActor+Database.swift (100%) rename Sources/DataThespian/{ => Databases}/ModelActorDatabase.swift (100%) rename Sources/DataThespian/{ => Notification}/AgentRegister.swift (100%) rename Sources/DataThespian/{ => Notification/Combine}/DatabaseChangePublicist.swift (100%) rename Sources/DataThespian/{ => Notification/Combine}/DatabaseChangePublicistKey.swift (78%) rename Sources/DataThespian/{ => Notification/Combine}/PublishingAgent.swift (100%) rename Sources/DataThespian/{ => Notification/Combine}/PublishingRegister.swift (100%) rename Sources/DataThespian/{ => Notification}/DataAgent.swift (100%) rename Sources/DataThespian/{ => Notification}/DataMonitor.swift (100%) rename Sources/DataThespian/{ => Notification}/DatabaseChangeSet.swift (100%) rename Sources/DataThespian/{ => Notification}/DatabaseChangeType.swift (100%) rename Sources/DataThespian/{ => Notification}/DatabaseMonitoring.swift (100%) rename Sources/DataThespian/{ => Notification}/ManagedObjectMetadata.swift (100%) rename Sources/DataThespian/{ => Notification}/Notification.swift (100%) rename Sources/DataThespian/{ => Notification}/NotificationDataUpdate.swift (100%) rename Sources/DataThespian/{ => Notification}/RegistrationCollection.swift (100%) rename Sources/DataThespian/{ => SwiftData}/FetchDescriptor.swift (98%) rename Sources/DataThespian/{ => SwiftData}/ModelContext+Extension.swift (95%) rename Sources/DataThespian/{ => SwiftData}/ModelContext.swift (92%) rename Sources/DataThespian/{ => SwiftData}/NSManagedObjectID.swift (100%) diff --git a/Sources/DataThespian/BackgroundDatabase.swift b/Sources/DataThespian/Databases/BackgroundDatabase.swift similarity index 100% rename from Sources/DataThespian/BackgroundDatabase.swift rename to Sources/DataThespian/Databases/BackgroundDatabase.swift diff --git a/Sources/DataThespian/Database+Extras.swift b/Sources/DataThespian/Databases/Database+Extras.swift similarity index 100% rename from Sources/DataThespian/Database+Extras.swift rename to Sources/DataThespian/Databases/Database+Extras.swift diff --git a/Sources/DataThespian/Database+ModelContext.swift b/Sources/DataThespian/Databases/Database+ModelContext.swift similarity index 100% rename from Sources/DataThespian/Database+ModelContext.swift rename to Sources/DataThespian/Databases/Database+ModelContext.swift diff --git a/Sources/DataThespian/Database.swift b/Sources/DataThespian/Databases/Database.swift similarity index 100% rename from Sources/DataThespian/Database.swift rename to Sources/DataThespian/Databases/Database.swift diff --git a/Sources/DataThespian/DatabaseKey.swift b/Sources/DataThespian/Databases/EnvironmentValues+Database.swift similarity index 88% rename from Sources/DataThespian/DatabaseKey.swift rename to Sources/DataThespian/Databases/EnvironmentValues+Database.swift index 3c2d0fd..3509da2 100644 --- a/Sources/DataThespian/DatabaseKey.swift +++ b/Sources/DataThespian/Databases/EnvironmentValues+Database.swift @@ -42,15 +42,8 @@ } } - private struct DatabaseKey: EnvironmentKey { - static var defaultValue: any Database { DefaultDatabase.instance } - } - extension EnvironmentValues { - public var database: any Database { - get { self[DatabaseKey.self] } - set { self[DatabaseKey.self] = newValue } - } + @Entry public var database: any Database = DefaultDatabase.instance } extension Scene { diff --git a/Sources/DataThespian/ModelActor+Database.swift b/Sources/DataThespian/Databases/ModelActor+Database.swift similarity index 100% rename from Sources/DataThespian/ModelActor+Database.swift rename to Sources/DataThespian/Databases/ModelActor+Database.swift diff --git a/Sources/DataThespian/ModelActorDatabase.swift b/Sources/DataThespian/Databases/ModelActorDatabase.swift similarity index 100% rename from Sources/DataThespian/ModelActorDatabase.swift rename to Sources/DataThespian/Databases/ModelActorDatabase.swift diff --git a/Sources/DataThespian/AgentRegister.swift b/Sources/DataThespian/Notification/AgentRegister.swift similarity index 100% rename from Sources/DataThespian/AgentRegister.swift rename to Sources/DataThespian/Notification/AgentRegister.swift diff --git a/Sources/DataThespian/DatabaseChangePublicist.swift b/Sources/DataThespian/Notification/Combine/DatabaseChangePublicist.swift similarity index 100% rename from Sources/DataThespian/DatabaseChangePublicist.swift rename to Sources/DataThespian/Notification/Combine/DatabaseChangePublicist.swift diff --git a/Sources/DataThespian/DatabaseChangePublicistKey.swift b/Sources/DataThespian/Notification/Combine/DatabaseChangePublicistKey.swift similarity index 78% rename from Sources/DataThespian/DatabaseChangePublicistKey.swift rename to Sources/DataThespian/Notification/Combine/DatabaseChangePublicistKey.swift index 1f3b4e6..0035876 100644 --- a/Sources/DataThespian/DatabaseChangePublicistKey.swift +++ b/Sources/DataThespian/Notification/Combine/DatabaseChangePublicistKey.swift @@ -32,16 +32,7 @@ public import SwiftUI - private struct DatabaseChangePublicistKey: EnvironmentKey { - typealias Value = DatabaseChangePublicist - - nonisolated static let defaultValue: DatabaseChangePublicist = .never() - } - extension EnvironmentValues { - public var databaseChangePublicist: DatabaseChangePublicist { - get { self[DatabaseChangePublicistKey.self] } - set { self[DatabaseChangePublicistKey.self] = newValue } - } + @Entry public var databaseChangePublicist: DatabaseChangePublicist = .never() } #endif diff --git a/Sources/DataThespian/PublishingAgent.swift b/Sources/DataThespian/Notification/Combine/PublishingAgent.swift similarity index 100% rename from Sources/DataThespian/PublishingAgent.swift rename to Sources/DataThespian/Notification/Combine/PublishingAgent.swift diff --git a/Sources/DataThespian/PublishingRegister.swift b/Sources/DataThespian/Notification/Combine/PublishingRegister.swift similarity index 100% rename from Sources/DataThespian/PublishingRegister.swift rename to Sources/DataThespian/Notification/Combine/PublishingRegister.swift diff --git a/Sources/DataThespian/DataAgent.swift b/Sources/DataThespian/Notification/DataAgent.swift similarity index 100% rename from Sources/DataThespian/DataAgent.swift rename to Sources/DataThespian/Notification/DataAgent.swift diff --git a/Sources/DataThespian/DataMonitor.swift b/Sources/DataThespian/Notification/DataMonitor.swift similarity index 100% rename from Sources/DataThespian/DataMonitor.swift rename to Sources/DataThespian/Notification/DataMonitor.swift diff --git a/Sources/DataThespian/DatabaseChangeSet.swift b/Sources/DataThespian/Notification/DatabaseChangeSet.swift similarity index 100% rename from Sources/DataThespian/DatabaseChangeSet.swift rename to Sources/DataThespian/Notification/DatabaseChangeSet.swift diff --git a/Sources/DataThespian/DatabaseChangeType.swift b/Sources/DataThespian/Notification/DatabaseChangeType.swift similarity index 100% rename from Sources/DataThespian/DatabaseChangeType.swift rename to Sources/DataThespian/Notification/DatabaseChangeType.swift diff --git a/Sources/DataThespian/DatabaseMonitoring.swift b/Sources/DataThespian/Notification/DatabaseMonitoring.swift similarity index 100% rename from Sources/DataThespian/DatabaseMonitoring.swift rename to Sources/DataThespian/Notification/DatabaseMonitoring.swift diff --git a/Sources/DataThespian/ManagedObjectMetadata.swift b/Sources/DataThespian/Notification/ManagedObjectMetadata.swift similarity index 100% rename from Sources/DataThespian/ManagedObjectMetadata.swift rename to Sources/DataThespian/Notification/ManagedObjectMetadata.swift diff --git a/Sources/DataThespian/Notification.swift b/Sources/DataThespian/Notification/Notification.swift similarity index 100% rename from Sources/DataThespian/Notification.swift rename to Sources/DataThespian/Notification/Notification.swift diff --git a/Sources/DataThespian/NotificationDataUpdate.swift b/Sources/DataThespian/Notification/NotificationDataUpdate.swift similarity index 100% rename from Sources/DataThespian/NotificationDataUpdate.swift rename to Sources/DataThespian/Notification/NotificationDataUpdate.swift diff --git a/Sources/DataThespian/RegistrationCollection.swift b/Sources/DataThespian/Notification/RegistrationCollection.swift similarity index 100% rename from Sources/DataThespian/RegistrationCollection.swift rename to Sources/DataThespian/Notification/RegistrationCollection.swift diff --git a/Sources/DataThespian/FetchDescriptor.swift b/Sources/DataThespian/SwiftData/FetchDescriptor.swift similarity index 98% rename from Sources/DataThespian/FetchDescriptor.swift rename to Sources/DataThespian/SwiftData/FetchDescriptor.swift index 1ced50d..a421568 100644 --- a/Sources/DataThespian/FetchDescriptor.swift +++ b/Sources/DataThespian/SwiftData/FetchDescriptor.swift @@ -38,6 +38,8 @@ self.fetchLimit = fetchLimit } + + @available(*, deprecated) public init(model: Model) { let persistentIdentifier = model.persistentIdentifier self.init( diff --git a/Sources/DataThespian/ModelContext+Extension.swift b/Sources/DataThespian/SwiftData/ModelContext+Extension.swift similarity index 95% rename from Sources/DataThespian/ModelContext+Extension.swift rename to Sources/DataThespian/SwiftData/ModelContext+Extension.swift index 3013d03..037f18c 100644 --- a/Sources/DataThespian/ModelContext+Extension.swift +++ b/Sources/DataThespian/SwiftData/ModelContext+Extension.swift @@ -32,6 +32,7 @@ public import SwiftData extension ModelContext { + @available(*, deprecated) public func delete(_: T.Type, withID id: PersistentIdentifier) -> Bool { guard let model: T = self.registeredModel(for: id) else { return false @@ -51,6 +52,7 @@ self.insert(model) return model.persistentModelID } + public func fetch( _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor, with closure: @escaping @Sendable ([T]) throws -> U @@ -58,6 +60,8 @@ let models = try self.fetch(selectDescriptor()) return try closure(models) } + + @available(*, deprecated) public func fetch( _ selectDescriptorA: @escaping @Sendable () -> FetchDescriptor, _ selectDescriptorB: @escaping @Sendable () -> FetchDescriptor, @@ -68,13 +72,15 @@ return try closure(firstModels, secondModels) } + @available(*, deprecated) public func get( for objectID: PersistentIdentifier, with closure: @escaping @Sendable (T?) throws -> U ) throws -> U where T: PersistentModel, U: Sendable { let model: T? = try self.existingModel(for: objectID) return try closure(model) } - + + @available(*, deprecated) public func transaction(block: @escaping @Sendable (ModelContext) throws -> Void) throws { try self.transaction { try block(self) } } diff --git a/Sources/DataThespian/ModelContext.swift b/Sources/DataThespian/SwiftData/ModelContext.swift similarity index 92% rename from Sources/DataThespian/ModelContext.swift rename to Sources/DataThespian/SwiftData/ModelContext.swift index 422fae3..f5c515f 100644 --- a/Sources/DataThespian/ModelContext.swift +++ b/Sources/DataThespian/SwiftData/ModelContext.swift @@ -29,10 +29,11 @@ #if canImport(SwiftData) import Foundation - public import SwiftData + import SwiftData extension ModelContext { - public func existingModel(for objectID: PersistentIdentifier) throws -> T? + + internal func existingModel(for objectID: PersistentIdentifier) throws -> T? where T: PersistentModel { if let registered: T = registeredModel(for: objectID) { return registered @@ -42,7 +43,8 @@ } let fetchDescriptor = FetchDescriptor( - predicate: #Predicate { $0.persistentModelID == objectID } + predicate: #Predicate { $0.persistentModelID == objectID }, + fetchLimit: 1 ) return try fetch(fetchDescriptor).first diff --git a/Sources/DataThespian/NSManagedObjectID.swift b/Sources/DataThespian/SwiftData/NSManagedObjectID.swift similarity index 100% rename from Sources/DataThespian/NSManagedObjectID.swift rename to Sources/DataThespian/SwiftData/NSManagedObjectID.swift From 5c12edfd4b263c562eec9de1a0b5d5e80759ae95 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Mon, 14 Oct 2024 22:29:45 -0400 Subject: [PATCH 2/9] Adding Queryable --- .../DataThespian/Databases/Queryable.swift | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 Sources/DataThespian/Databases/Queryable.swift diff --git a/Sources/DataThespian/Databases/Queryable.swift b/Sources/DataThespian/Databases/Queryable.swift new file mode 100644 index 0000000..2acd010 --- /dev/null +++ b/Sources/DataThespian/Databases/Queryable.swift @@ -0,0 +1,67 @@ +// +// Queryable.swift +// DataThespian +// +// Created by Leo Dion on 10/14/24. +// + +public import SwiftData +public import Foundation + +public enum Selector : Sendable { + public enum Delete : Sendable{ + case predicate(Predicate) + case all + case model(Model) + } + public enum List : Sendable{ + case descriptor(FetchDescriptor) + } + public enum Get : Sendable{ + case model(Model) + case predicate(Predicate) + } +} + +public protocol Queryable { + func insert( + _ closuer: @Sendable @escaping () -> PersistentModelType, + with closure: @escaping @Sendable (PersistentModelType) throws -> U + ) async -> Model + + func get( + for selector: Selector.Get, + with closure: @escaping @Sendable (PersistentModelType?) throws -> U + ) async rethrows -> U + + func fetch( + for selector: Selector.List, + with closure: @escaping @Sendable ([PersistentModelType]) throws -> U + ) async rethrows -> U + + func delete(_ selector: Selector.Delete) async throws +} + +extension Queryable { + func insert( + _ closuer: @Sendable @escaping () -> PersistentModelType + ) async -> Model { + await self.insert(closuer, with: Model.init) + } + + func get( + for selector: Selector.Get + ) async -> Model? { + await self.get(for: selector) { persistentModel in + persistentModel.flatMap(Model.init) + } + } + + func fetch( + for selector: Selector.List + ) async -> [Model] { + await self.fetch(for: selector) { persistentModels in + persistentModels.map(Model.init) + } + } +} From 50638b189b97e39b0bb727d21553583d550fe002 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Tue, 15 Oct 2024 13:40:10 -0400 Subject: [PATCH 3/9] adding unique --- Example/Sources/Item.swift | 12 ++++++- .../DataThespian/Databases/Queryable.swift | 35 ++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/Example/Sources/Item.swift b/Example/Sources/Item.swift index 09b1e81..b9f0ab7 100644 --- a/Example/Sources/Item.swift +++ b/Example/Sources/Item.swift @@ -7,12 +7,22 @@ import Foundation import SwiftData +import DataThespian @Model -internal final class Item { +internal final class Item : Unique { internal private(set) var timestamp: Date internal init(timestamp: Date) { self.timestamp = timestamp } + + enum Keys : UniqueKeySet { + typealias Model = Item + + static let timestamp : UniqueKey = Self.unique(\.timestamp) + + + + } } diff --git a/Sources/DataThespian/Databases/Queryable.swift b/Sources/DataThespian/Databases/Queryable.swift index 2acd010..770487c 100644 --- a/Sources/DataThespian/Databases/Queryable.swift +++ b/Sources/DataThespian/Databases/Queryable.swift @@ -8,6 +8,30 @@ public import SwiftData public import Foundation +public protocol Unique { + associatedtype Keys : UniqueKeySet +} + +public protocol UniqueKeySet : Sendable { + associatedtype Model : Unique +} + +public struct UniqueKey : Sendable { + let keyPath : @Sendable () -> KeyPath +} + +extension UniqueKeySet { + public static func unique(keyPath : @escaping @Sendable () -> KeyPath) -> UniqueKey { + .init(keyPath: keyPath) + } + + public static func unique(_ keyPath : @autoclosure @escaping @Sendable () -> KeyPath) -> UniqueKey { + .init(keyPath: keyPath) + } + + +} + public enum Selector : Sendable { public enum Delete : Sendable{ case predicate(Predicate) @@ -23,11 +47,20 @@ public enum Selector : Sendable { } } +//extension Selector.Get { +// static func unique(_ key: UniqueKey, equals value: ValueType) -> Self { +// +// #Predicate({ input in +// +// }) +// } +//} + public protocol Queryable { func insert( _ closuer: @Sendable @escaping () -> PersistentModelType, with closure: @escaping @Sendable (PersistentModelType) throws -> U - ) async -> Model + ) async rethrows -> U func get( for selector: Selector.Get, From e3a44befaed4b2e4ba545f9d9838a52c19987cad Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Wed, 16 Oct 2024 11:00:51 -0400 Subject: [PATCH 4/9] fixing Queryable stuff --- Example/Sources/Item.swift | 13 +- .../Databases/Database+Extras.swift | 6 +- .../EnvironmentValues+Database.swift | 2 +- .../DataThespian/Databases/QueryError.swift | 34 +++++ .../Databases/Queryable+Extensions.swift | 117 ++++++++++++++++++ .../DataThespian/Databases/Queryable.swift | 109 +++++----------- Sources/DataThespian/Databases/Selector.swift | 58 +++++++++ Sources/DataThespian/Databases/Unique.swift | 32 +++++ .../DataThespian/Databases/UniqueKey.swift | 37 ++++++ .../Databases/UniqueKeyPath.swift | 42 +++++++ .../DataThespian/Databases/UniqueKeys.swift | 40 ++++++ ...nmentValues+DatabaseChangePublicist.swift} | 2 +- .../SwiftData/FetchDescriptor.swift | 2 +- .../SwiftData/ModelContext+Extension.swift | 6 +- .../DataThespian/SwiftData/ModelContext.swift | 1 - 15 files changed, 405 insertions(+), 96 deletions(-) create mode 100644 Sources/DataThespian/Databases/QueryError.swift create mode 100644 Sources/DataThespian/Databases/Queryable+Extensions.swift create mode 100644 Sources/DataThespian/Databases/Selector.swift create mode 100644 Sources/DataThespian/Databases/Unique.swift create mode 100644 Sources/DataThespian/Databases/UniqueKey.swift create mode 100644 Sources/DataThespian/Databases/UniqueKeyPath.swift create mode 100644 Sources/DataThespian/Databases/UniqueKeys.swift rename Sources/DataThespian/Notification/Combine/{DatabaseChangePublicistKey.swift => EnvironmentValues+DatabaseChangePublicist.swift} (96%) diff --git a/Example/Sources/Item.swift b/Example/Sources/Item.swift index b9f0ab7..55600a8 100644 --- a/Example/Sources/Item.swift +++ b/Example/Sources/Item.swift @@ -5,24 +5,21 @@ // Created by Leo Dion on 10/10/24. // +import DataThespian import Foundation import SwiftData -import DataThespian @Model -internal final class Item : Unique { +internal final class Item: Unique { internal private(set) var timestamp: Date internal init(timestamp: Date) { self.timestamp = timestamp } - - enum Keys : UniqueKeySet { - typealias Model = Item - - static let timestamp : UniqueKey = Self.unique(\.timestamp) - + enum Keys: UniqueKeys { + typealias Model = Item + static let timestamp = keyPath(\.timestamp) } } diff --git a/Sources/DataThespian/Databases/Database+Extras.swift b/Sources/DataThespian/Databases/Database+Extras.swift index 15644f5..867b9f6 100644 --- a/Sources/DataThespian/Databases/Database+Extras.swift +++ b/Sources/DataThespian/Databases/Database+Extras.swift @@ -53,7 +53,8 @@ } } - public func first(_ selectPredicate: Predicate) async throws -> Model? { + public func first(_ selectPredicate: Predicate) async throws -> Model? + { try await self.first(selectPredicate, with: Model.ifMap) } @@ -96,7 +97,8 @@ } public func deleteAll(of types: [any PersistentModel.Type]) async throws { - try await self.transaction { context in for type in types { try context.delete(model: type) } } + try await self.transaction { context in for type in types { try context.delete(model: type) } + } } public func fetch( diff --git a/Sources/DataThespian/Databases/EnvironmentValues+Database.swift b/Sources/DataThespian/Databases/EnvironmentValues+Database.swift index 3509da2..b2ed50a 100644 --- a/Sources/DataThespian/Databases/EnvironmentValues+Database.swift +++ b/Sources/DataThespian/Databases/EnvironmentValues+Database.swift @@ -1,5 +1,5 @@ // -// DatabaseKey.swift +// EnvironmentValues+Database.swift // DataThespian // // Created by Leo Dion. diff --git a/Sources/DataThespian/Databases/QueryError.swift b/Sources/DataThespian/Databases/QueryError.swift new file mode 100644 index 0000000..b2ef23d --- /dev/null +++ b/Sources/DataThespian/Databases/QueryError.swift @@ -0,0 +1,34 @@ +// +// QueryError.swift +// DataThespian +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import SwiftData + +public enum QueryError: Error { + case itemNotFound(Selector.Get) +} diff --git a/Sources/DataThespian/Databases/Queryable+Extensions.swift b/Sources/DataThespian/Databases/Queryable+Extensions.swift new file mode 100644 index 0000000..c8fe187 --- /dev/null +++ b/Sources/DataThespian/Databases/Queryable+Extensions.swift @@ -0,0 +1,117 @@ +// +// Queryable+Extensions.swift +// DataThespian +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import SwiftData + +extension Queryable { + public func insert( + _ closuer: @Sendable @escaping () -> PersistentModelType + ) async -> Model { + await self.insert(closuer, with: Model.init) + } + + public func getOptional( + for selector: Selector.Get + ) async -> Model? { + await self.getOptional(for: selector) { persistentModel in + persistentModel.flatMap(Model.init) + } + } + + public func fetch( + for selector: Selector.List + ) async -> [Model] { + await self.fetch(for: selector) { persistentModels in + persistentModels.map(Model.init) + } + } + + public func get( + for selector: Selector.Get + ) async throws -> Model { + try await self.getOptional(for: selector) { persistentModel in + guard let persistentModel else { + throw QueryError.itemNotFound(selector) + } + return Model(persistentModel) + } + } + + public func get( + for selector: Selector.Get, + with closure: @escaping @Sendable (PersistentModelType) throws -> U + ) async throws -> U { + try await self.getOptional(for: selector) { persistentModel in + guard let persistentModel else { + throw QueryError.itemNotFound(selector) + } + return try closure(persistentModel) + } + } + + public func update( + for selector: Selector.Get, + with closure: @escaping @Sendable (PersistentModelType) throws -> Void + ) async throws { + try await self.get(for: selector, with: closure) + } + + public func update( + for selector: Selector.List, + with closure: @escaping @Sendable ([PersistentModelType]) throws -> Void + ) async throws { + try await self.fetch(for: selector, with: closure) + } + + public func insertIf( + _ model: @Sendable @escaping () -> PersistentModelType, + notExist selector: @Sendable @escaping (PersistentModelType) -> + Selector.Get + ) async -> Model { + let persistentModel = model() + let selector = selector(persistentModel) + let modelOptional = await self.getOptional(for: selector) + + if let modelOptional { + return modelOptional + } else { + return await self.insert(model) + } + } + + public func insertIf( + _ model: @Sendable @escaping () -> PersistentModelType, + notExist selector: @Sendable @escaping (PersistentModelType) -> + Selector.Get, + with closure: @escaping @Sendable (PersistentModelType) throws -> U + ) async throws -> U { + let model = await self.insertIf(model, notExist: selector) + return try await self.get(for: .model(model), with: closure) + } +} diff --git a/Sources/DataThespian/Databases/Queryable.swift b/Sources/DataThespian/Databases/Queryable.swift index 770487c..6298775 100644 --- a/Sources/DataThespian/Databases/Queryable.swift +++ b/Sources/DataThespian/Databases/Queryable.swift @@ -2,99 +2,50 @@ // Queryable.swift // DataThespian // -// Created by Leo Dion on 10/14/24. +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. // public import SwiftData -public import Foundation - -public protocol Unique { - associatedtype Keys : UniqueKeySet -} - -public protocol UniqueKeySet : Sendable { - associatedtype Model : Unique -} - -public struct UniqueKey : Sendable { - let keyPath : @Sendable () -> KeyPath -} - -extension UniqueKeySet { - public static func unique(keyPath : @escaping @Sendable () -> KeyPath) -> UniqueKey { - .init(keyPath: keyPath) - } - - public static func unique(_ keyPath : @autoclosure @escaping @Sendable () -> KeyPath) -> UniqueKey { - .init(keyPath: keyPath) - } - - -} - -public enum Selector : Sendable { - public enum Delete : Sendable{ - case predicate(Predicate) - case all - case model(Model) - } - public enum List : Sendable{ - case descriptor(FetchDescriptor) - } - public enum Get : Sendable{ - case model(Model) - case predicate(Predicate) - } -} - -//extension Selector.Get { -// static func unique(_ key: UniqueKey, equals value: ValueType) -> Self { -// -// #Predicate({ input in -// -// }) -// } -//} public protocol Queryable { - func insert( + func save() async throws + + func insert( _ closuer: @Sendable @escaping () -> PersistentModelType, with closure: @escaping @Sendable (PersistentModelType) throws -> U ) async rethrows -> U - - func get( + + func getOptional( for selector: Selector.Get, with closure: @escaping @Sendable (PersistentModelType?) throws -> U ) async rethrows -> U - - func fetch( + + func fetch( for selector: Selector.List, with closure: @escaping @Sendable ([PersistentModelType]) throws -> U ) async rethrows -> U func delete(_ selector: Selector.Delete) async throws } - -extension Queryable { - func insert( - _ closuer: @Sendable @escaping () -> PersistentModelType - ) async -> Model { - await self.insert(closuer, with: Model.init) - } - - func get( - for selector: Selector.Get - ) async -> Model? { - await self.get(for: selector) { persistentModel in - persistentModel.flatMap(Model.init) - } - } - - func fetch( - for selector: Selector.List - ) async -> [Model] { - await self.fetch(for: selector) { persistentModels in - persistentModels.map(Model.init) - } - } -} diff --git a/Sources/DataThespian/Databases/Selector.swift b/Sources/DataThespian/Databases/Selector.swift new file mode 100644 index 0000000..757b809 --- /dev/null +++ b/Sources/DataThespian/Databases/Selector.swift @@ -0,0 +1,58 @@ +// +// Selector.swift +// DataThespian +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation +public import SwiftData + +public enum Selector: Sendable { + public enum Delete: Sendable { + case predicate(Predicate) + case all + case model(Model) + } + public enum List: Sendable { + case descriptor(FetchDescriptor) + } + public enum Get: Sendable { + case model(Model) + case predicate(Predicate) + } +} + +extension Selector.Get { + @available(*, unavailable, message: "Not implemented yet.") + public static func unique( + _ key: UniqueKeyableType, + equals value: UniqueKeyableType.ValueType + ) -> Self where UniqueKeyableType.Model == T { + .predicate( + key.predicate(equals: value) + ) + } +} diff --git a/Sources/DataThespian/Databases/Unique.swift b/Sources/DataThespian/Databases/Unique.swift new file mode 100644 index 0000000..5371263 --- /dev/null +++ b/Sources/DataThespian/Databases/Unique.swift @@ -0,0 +1,32 @@ +// +// Unique.swift +// DataThespian +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public protocol Unique { + associatedtype Keys: UniqueKeys +} diff --git a/Sources/DataThespian/Databases/UniqueKey.swift b/Sources/DataThespian/Databases/UniqueKey.swift new file mode 100644 index 0000000..9981b50 --- /dev/null +++ b/Sources/DataThespian/Databases/UniqueKey.swift @@ -0,0 +1,37 @@ +// +// UniqueKey.swift +// DataThespian +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation + +public protocol UniqueKey: Sendable { + associatedtype Model: Unique + associatedtype ValueType: Sendable & Equatable & Codable + + func predicate(equals value: ValueType) -> Predicate +} diff --git a/Sources/DataThespian/Databases/UniqueKeyPath.swift b/Sources/DataThespian/Databases/UniqueKeyPath.swift new file mode 100644 index 0000000..0976747 --- /dev/null +++ b/Sources/DataThespian/Databases/UniqueKeyPath.swift @@ -0,0 +1,42 @@ +// +// UniqueKeyPath.swift +// DataThespian +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation + +public struct UniqueKeyPath: UniqueKey { + internal init(keyPath: any KeyPath & Sendable) { + self.keyPath = keyPath + } + + private let keyPath: KeyPath & Sendable + + public func predicate(equals value: ValueType) -> Predicate { + fatalError("Not implemented yet.") + } +} diff --git a/Sources/DataThespian/Databases/UniqueKeys.swift b/Sources/DataThespian/Databases/UniqueKeys.swift new file mode 100644 index 0000000..ae92690 --- /dev/null +++ b/Sources/DataThespian/Databases/UniqueKeys.swift @@ -0,0 +1,40 @@ +// +// UniqueKeys.swift +// DataThespian +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public protocol UniqueKeys: Sendable { + associatedtype Model: Unique +} + +extension UniqueKeys { + public static func keyPath( + _ keyPath: any KeyPath & Sendable + ) -> UniqueKeyPath { + UniqueKeyPath(keyPath: keyPath) + } +} diff --git a/Sources/DataThespian/Notification/Combine/DatabaseChangePublicistKey.swift b/Sources/DataThespian/Notification/Combine/EnvironmentValues+DatabaseChangePublicist.swift similarity index 96% rename from Sources/DataThespian/Notification/Combine/DatabaseChangePublicistKey.swift rename to Sources/DataThespian/Notification/Combine/EnvironmentValues+DatabaseChangePublicist.swift index 0035876..673cca9 100644 --- a/Sources/DataThespian/Notification/Combine/DatabaseChangePublicistKey.swift +++ b/Sources/DataThespian/Notification/Combine/EnvironmentValues+DatabaseChangePublicist.swift @@ -1,5 +1,5 @@ // -// DatabaseChangePublicistKey.swift +// EnvironmentValues+DatabaseChangePublicist.swift // DataThespian // // Created by Leo Dion. diff --git a/Sources/DataThespian/SwiftData/FetchDescriptor.swift b/Sources/DataThespian/SwiftData/FetchDescriptor.swift index a421568..9bf6f63 100644 --- a/Sources/DataThespian/SwiftData/FetchDescriptor.swift +++ b/Sources/DataThespian/SwiftData/FetchDescriptor.swift @@ -38,7 +38,7 @@ self.fetchLimit = fetchLimit } - + @available(*, deprecated) public init(model: Model) { let persistentIdentifier = model.persistentIdentifier diff --git a/Sources/DataThespian/SwiftData/ModelContext+Extension.swift b/Sources/DataThespian/SwiftData/ModelContext+Extension.swift index 037f18c..8ffcc6c 100644 --- a/Sources/DataThespian/SwiftData/ModelContext+Extension.swift +++ b/Sources/DataThespian/SwiftData/ModelContext+Extension.swift @@ -52,7 +52,7 @@ self.insert(model) return model.persistentModelID } - + public func fetch( _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor, with closure: @escaping @Sendable ([T]) throws -> U @@ -60,7 +60,7 @@ let models = try self.fetch(selectDescriptor()) return try closure(models) } - + @available(*, deprecated) public func fetch( _ selectDescriptorA: @escaping @Sendable () -> FetchDescriptor, @@ -79,7 +79,7 @@ let model: T? = try self.existingModel(for: objectID) return try closure(model) } - + @available(*, deprecated) public func transaction(block: @escaping @Sendable (ModelContext) throws -> Void) throws { try self.transaction { try block(self) } diff --git a/Sources/DataThespian/SwiftData/ModelContext.swift b/Sources/DataThespian/SwiftData/ModelContext.swift index f5c515f..2b13bf3 100644 --- a/Sources/DataThespian/SwiftData/ModelContext.swift +++ b/Sources/DataThespian/SwiftData/ModelContext.swift @@ -32,7 +32,6 @@ import SwiftData extension ModelContext { - internal func existingModel(for objectID: PersistentIdentifier) throws -> T? where T: PersistentModel { if let registered: T = registeredModel(for: objectID) { From 5685ab3a20e610aa08e1431ba500431e2b7dbb34 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Wed, 16 Oct 2024 13:39:10 -0400 Subject: [PATCH 5/9] adding more to Queryable --- Example/Sources/Item.swift | 12 +-- .../Databases/Database+ModelContext.swift | 2 - .../Databases/Database+Queryable.swift | 71 ++++++++++++++++ Sources/DataThespian/Databases/Database.swift | 2 +- Sources/DataThespian/Databases/Selector.swift | 6 ++ .../Databases/UniqueKeyPath.swift | 2 +- .../DataThespian/Databases/UniqueKeys.swift | 3 + .../SwiftData/ModelContext+Extension.swift | 9 +++ .../SwiftData/ModelContext+Queryable.swift | 80 +++++++++++++++++++ .../DataThespian/SwiftData/ModelContext.swift | 4 + 10 files changed, 181 insertions(+), 10 deletions(-) create mode 100644 Sources/DataThespian/Databases/Database+Queryable.swift create mode 100644 Sources/DataThespian/SwiftData/ModelContext+Queryable.swift diff --git a/Example/Sources/Item.swift b/Example/Sources/Item.swift index 55600a8..4f69f20 100644 --- a/Example/Sources/Item.swift +++ b/Example/Sources/Item.swift @@ -11,15 +11,15 @@ import SwiftData @Model internal final class Item: Unique { + internal enum Keys: UniqueKeys { + internal typealias Model = Item + internal static let primary = timestamp + internal static let timestamp = keyPath(\.timestamp) + } + internal private(set) var timestamp: Date internal init(timestamp: Date) { self.timestamp = timestamp } - - enum Keys: UniqueKeys { - typealias Model = Item - - static let timestamp = keyPath(\.timestamp) - } } diff --git a/Sources/DataThespian/Databases/Database+ModelContext.swift b/Sources/DataThespian/Databases/Database+ModelContext.swift index 4107355..d544fff 100644 --- a/Sources/DataThespian/Databases/Database+ModelContext.swift +++ b/Sources/DataThespian/Databases/Database+ModelContext.swift @@ -34,8 +34,6 @@ public import SwiftData extension Database { - public func save() async throws { try await self.withModelContext { try $0.save() } } - @discardableResult public func delete( _ modelType: T.Type, withID id: PersistentIdentifier ) async -> Bool { await self.withModelContext { $0.delete(modelType, withID: id) } } diff --git a/Sources/DataThespian/Databases/Database+Queryable.swift b/Sources/DataThespian/Databases/Database+Queryable.swift new file mode 100644 index 0000000..f887682 --- /dev/null +++ b/Sources/DataThespian/Databases/Database+Queryable.swift @@ -0,0 +1,71 @@ +// +// Database+Queryable.swift +// DataThespian +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import SwiftData + +extension Database { + public func save() async throws { + try await self.withModelContext { try $0.save() } + } + + public func insert( + _ closuer: @Sendable @escaping () -> PersistentModelType, + with closure: @escaping @Sendable (PersistentModelType) throws -> U + ) async rethrows -> U { + try await self.withModelContext { + try $0.insert(closuer, with: closure) + } + } + + public func getOptional( + for selector: Selector.Get, + with closure: @escaping @Sendable (PersistentModelType?) throws -> U + ) async rethrows -> U { + try await self.withModelContext { + try $0.getOptional(for: selector, with: closure) + } + } + + public func fetch( + for selector: Selector.List, + with closure: @escaping @Sendable ([PersistentModelType]) throws -> U + ) async rethrows -> U { + try await self.withModelContext { + try $0.fetch(for: selector, with: closure) + } + } + + public func delete(_ selector: Selector.Delete) + async throws + { + try await self.withModelContext { + try $0.delete(selector) + } + } +} diff --git a/Sources/DataThespian/Databases/Database.swift b/Sources/DataThespian/Databases/Database.swift index 97dfa1b..6a8c6d2 100644 --- a/Sources/DataThespian/Databases/Database.swift +++ b/Sources/DataThespian/Databases/Database.swift @@ -33,7 +33,7 @@ public import SwiftData - public protocol Database: Sendable { + public protocol Database: Sendable, Queryable { func withModelContext(_ closure: @Sendable @escaping (ModelContext) throws -> T) async rethrows -> T } diff --git a/Sources/DataThespian/Databases/Selector.swift b/Sources/DataThespian/Databases/Selector.swift index 757b809..ce9f491 100644 --- a/Sources/DataThespian/Databases/Selector.swift +++ b/Sources/DataThespian/Databases/Selector.swift @@ -56,3 +56,9 @@ extension Selector.Get { ) } } + +extension Selector.List { + public static func all() -> Selector.List { + .descriptor(.init()) + } +} diff --git a/Sources/DataThespian/Databases/UniqueKeyPath.swift b/Sources/DataThespian/Databases/UniqueKeyPath.swift index 0976747..a1c1034 100644 --- a/Sources/DataThespian/Databases/UniqueKeyPath.swift +++ b/Sources/DataThespian/Databases/UniqueKeyPath.swift @@ -33,7 +33,7 @@ public struct UniqueKeyPath & Sendable) { self.keyPath = keyPath } - + private let keyPath: KeyPath & Sendable public func predicate(equals value: ValueType) -> Predicate { diff --git a/Sources/DataThespian/Databases/UniqueKeys.swift b/Sources/DataThespian/Databases/UniqueKeys.swift index ae92690..aded055 100644 --- a/Sources/DataThespian/Databases/UniqueKeys.swift +++ b/Sources/DataThespian/Databases/UniqueKeys.swift @@ -29,6 +29,9 @@ public protocol UniqueKeys: Sendable { associatedtype Model: Unique + associatedtype PrimaryKey: UniqueKey where PrimaryKey.Model == Model + + static var primary: PrimaryKey { get } } extension UniqueKeys { diff --git a/Sources/DataThespian/SwiftData/ModelContext+Extension.swift b/Sources/DataThespian/SwiftData/ModelContext+Extension.swift index 8ffcc6c..533767e 100644 --- a/Sources/DataThespian/SwiftData/ModelContext+Extension.swift +++ b/Sources/DataThespian/SwiftData/ModelContext+Extension.swift @@ -41,10 +41,12 @@ return true } + @available(*, deprecated) public func delete(where predicate: Predicate?) throws where T: PersistentModel { try self.delete(model: T.self, where: predicate) } + @available(*, deprecated) public func insert(_ closuer: @escaping @Sendable () -> some PersistentModel) -> PersistentIdentifier { @@ -53,6 +55,7 @@ return model.persistentModelID } + @available(*, deprecated) public func fetch( _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor, with closure: @escaping @Sendable ([T]) throws -> U @@ -84,5 +87,11 @@ public func transaction(block: @escaping @Sendable (ModelContext) throws -> Void) throws { try self.transaction { try block(self) } } + + public func first( + where predicate: Predicate? = nil + ) throws -> PersistentModelType? { + try self.fetch(FetchDescriptor(predicate: predicate, fetchLimit: 1)).first + } } #endif diff --git a/Sources/DataThespian/SwiftData/ModelContext+Queryable.swift b/Sources/DataThespian/SwiftData/ModelContext+Queryable.swift new file mode 100644 index 0000000..1f1a2bd --- /dev/null +++ b/Sources/DataThespian/SwiftData/ModelContext+Queryable.swift @@ -0,0 +1,80 @@ +// +// ModelContext+Queryable.swift +// DataThespian +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import SwiftData + +extension ModelContext { + public func insert( + _ closuer: @Sendable @escaping () -> PersistentModelType, + with closure: @escaping @Sendable (PersistentModelType) throws -> U + ) rethrows -> U { + let persistentModel = closuer() + self.insert(persistentModel) + return try closure(persistentModel) + } + + public func getOptional( + for selector: Selector.Get, + with closure: @escaping @Sendable (PersistentModelType?) throws -> U + ) throws -> U { + let persistentModel: PersistentModelType? + switch selector { + case .model(let model): + persistentModel = try self.existingModel(for: model) + case .predicate(let predicate): + persistentModel = try self.first(where: predicate) + } + return try closure(persistentModel) + } + + public func fetch( + for selector: Selector.List, + with closure: @escaping @Sendable ([PersistentModelType]) throws -> U + ) throws -> U { + let persistentModels: [PersistentModelType] + switch selector { + case .descriptor(let descriptor): + persistentModels = try self.fetch(descriptor) + } + return try closure(persistentModels) + } + + public func delete(_ selector: Selector.Delete) throws { + switch selector { + case .all: + try self.delete(model: PersistentModelType.self) + case .model(let model): + if let persistentModel = try self.existingModel(for: model) { + self.delete(persistentModel) + } + case .predicate(let predicate): + try self.delete(model: PersistentModelType.self, where: predicate) + } + } +} diff --git a/Sources/DataThespian/SwiftData/ModelContext.swift b/Sources/DataThespian/SwiftData/ModelContext.swift index 2b13bf3..b76bf18 100644 --- a/Sources/DataThespian/SwiftData/ModelContext.swift +++ b/Sources/DataThespian/SwiftData/ModelContext.swift @@ -32,6 +32,10 @@ import SwiftData extension ModelContext { + internal func existingModel(for model: Model) throws -> T? + where T: PersistentModel { + try self.existingModel(for: model.persistentIdentifier) + } internal func existingModel(for objectID: PersistentIdentifier) throws -> T? where T: PersistentModel { if let registered: T = registeredModel(for: objectID) { From 0663086e22d22c2092105256b87a3149e3ee9d34 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Wed, 16 Oct 2024 15:29:15 -0400 Subject: [PATCH 6/9] cleaning up before merge --- Example/Sources/ChildViewModel.swift | 28 ++++++ Example/Sources/ContentObject.swift | 62 ++++++++----- Example/Sources/ContentView.swift | 2 +- Example/Sources/Item.swift | 1 + Example/Sources/ItemChild.swift | 19 ++++ Example/Sources/ItemChildView.swift | 31 +++++++ .../{ItemModel.swift => ItemViewModel.swift} | 12 ++- .../Databases/Database+Extras.swift | 45 ++++------ .../Databases/Database+ModelContext.swift | 6 +- .../Databases/Queryable+Extensions.swift | 18 ++++ .../DataThespian/Databases/Queryable.swift | 2 +- Sources/DataThespian/Model.swift | 4 + .../DataThespian/SwiftData/ModelContext.swift | 4 +- .../SwiftData/PersistentIdentifier.swift | 88 +++++++++++++++++++ 14 files changed, 263 insertions(+), 59 deletions(-) create mode 100644 Example/Sources/ChildViewModel.swift create mode 100644 Example/Sources/ItemChild.swift create mode 100644 Example/Sources/ItemChildView.swift rename Example/Sources/{ItemModel.swift => ItemViewModel.swift} (51%) create mode 100644 Sources/DataThespian/SwiftData/PersistentIdentifier.swift diff --git a/Example/Sources/ChildViewModel.swift b/Example/Sources/ChildViewModel.swift new file mode 100644 index 0000000..aaae82a --- /dev/null +++ b/Example/Sources/ChildViewModel.swift @@ -0,0 +1,28 @@ +// +// ChildViewModel.swift +// DataThespian +// +// Created by Leo Dion on 10/16/24. +// + +import DataThespian +import Foundation +import SwiftData + +internal struct ChildViewModel: Sendable, Identifiable { + internal let model: Model + internal let timestamp: Date + + internal var id: PersistentIdentifier { + model.persistentIdentifier + } + + private init(model: Model, timestamp: Date) { + self.model = model + self.timestamp = timestamp + } + + internal init(child: ItemChild) { + self.init(model: .init(child), timestamp: child.timestamp) + } +} diff --git a/Example/Sources/ContentObject.swift b/Example/Sources/ContentObject.swift index 19e159b..85394b2 100644 --- a/Example/Sources/ContentObject.swift +++ b/Example/Sources/ContentObject.swift @@ -17,17 +17,17 @@ internal class ContentObject { private var databaseChangeCancellable: AnyCancellable? private var databaseChangeSubscription: AnyCancellable? private var database: (any Database)? - internal private(set) var items = [ItemModel]() - internal var selectedItemsID: Set = [] + internal private(set) var items = [ItemViewModel]() + internal var selectedItemsID: Set = [] private var newItem: AnyCancellable? internal var error: (any Error)? - internal var selectedItems: [ItemModel] { + internal var selectedItems: [ItemViewModel] { let selectedItemsID = self.selectedItemsID - let items: [ItemModel] + let items: [ItemViewModel] do { items = try self.items.filter( - #Predicate { + #Predicate { selectedItemsID.contains($0.id) } ) @@ -49,17 +49,7 @@ internal class ContentObject { private static func deleteModels(_ models: [Model], from database: (any Database)) async throws { - try await database.withModelContext { modelContext in - let items: [Item] = models.compactMap { - modelContext.model(for: $0.persistentIdentifier) as? Item - } - dump(items.first?.persistentModelID) - assert(items.count == models.count) - for item in items { - modelContext.delete(item) - } - try modelContext.save() - } + try await database.deleteModels(models) } private func beginUpdateItems() { @@ -76,10 +66,10 @@ internal class ContentObject { guard let database else { return } - self.items = try await database.withModelContext({ modelContext in + self.items = try await database.withModelContext { modelContext in let items = try modelContext.fetch(FetchDescriptor()) - return items.map(ItemModel.init) - }) + return items.map(ItemViewModel.init) + } } internal func initialize( @@ -114,20 +104,46 @@ internal class ContentObject { } Task { try await Self.deleteModels(models, from: database) + try await database.save() } } - internal func addItem(withDate date: Date = .init()) { + internal func addChild(to item: ItemViewModel) { guard let database else { return } Task { + let timestamp = Date() + let childModel = await database.insert { + ItemChild(timestamp: timestamp) + } + try await database.withModelContext { modelContext in - let newItem = Item(timestamp: date) - modelContext.insert(newItem) - dump(newItem.persistentModelID) + let item = try modelContext.existingModel(for: item.model) + let child = try modelContext.existingModel(for: childModel) + assert(child != nil && item != nil) + child?.parent = item try modelContext.save() } } } + + internal func addItem(withDate date: Date = .init()) { + guard let database else { + return + } + Task { + let insertedModel = await database.insert { Item(timestamp: date) } + print("inserted:", insertedModel.isTemporary) + try await database.save() + let savedModel = try await database.get( + for: .predicate( + #Predicate { + $0.timestamp == date + } + ) + ) + print("saved:", savedModel.isTemporary) + } + } } diff --git a/Example/Sources/ContentView.swift b/Example/Sources/ContentView.swift index 9635d21..e665d7c 100644 --- a/Example/Sources/ContentView.swift +++ b/Example/Sources/ContentView.swift @@ -41,7 +41,7 @@ internal struct ContentView: View { if selectedItems.count > 1 { Text("Multiple Selected") } else if let item = selectedItems.first { - Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))") + ItemChildView(object: object, item: item) } else { Text("Select an item") } diff --git a/Example/Sources/Item.swift b/Example/Sources/Item.swift index 4f69f20..82a3332 100644 --- a/Example/Sources/Item.swift +++ b/Example/Sources/Item.swift @@ -18,6 +18,7 @@ internal final class Item: Unique { } internal private(set) var timestamp: Date + internal private(set) var children: [ItemChild]? internal init(timestamp: Date) { self.timestamp = timestamp diff --git a/Example/Sources/ItemChild.swift b/Example/Sources/ItemChild.swift new file mode 100644 index 0000000..e398ce8 --- /dev/null +++ b/Example/Sources/ItemChild.swift @@ -0,0 +1,19 @@ +// +// ItemChild.swift +// DataThespian +// +// Created by Leo Dion on 10/16/24. +// +import Foundation +import SwiftData + +@Model +internal final class ItemChild { + internal var parent: Item? + internal private(set) var timestamp: Date + + internal init(parent: Item? = nil, timestamp: Date) { + self.parent = parent + self.timestamp = timestamp + } +} diff --git a/Example/Sources/ItemChildView.swift b/Example/Sources/ItemChildView.swift new file mode 100644 index 0000000..7504804 --- /dev/null +++ b/Example/Sources/ItemChildView.swift @@ -0,0 +1,31 @@ +// +// ItemChildView.swift +// DataThespianExample +// +// Created by Leo Dion on 10/16/24. +// + +import SwiftUI + +internal struct ItemChildView: View { + internal var object: ContentObject + internal let item: ItemViewModel + internal var body: some View { + VStack { + Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))") + Divider() + Button("Add Child") { + object.addChild(to: item) + } + ForEach(item.children) { child in + Text( + "Child at \(child.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))" + ) + } + } + } +} +// +// #Preview { +// ItemChildView() +// } diff --git a/Example/Sources/ItemModel.swift b/Example/Sources/ItemViewModel.swift similarity index 51% rename from Example/Sources/ItemModel.swift rename to Example/Sources/ItemViewModel.swift index f13e75b..02edeeb 100644 --- a/Example/Sources/ItemModel.swift +++ b/Example/Sources/ItemViewModel.swift @@ -9,20 +9,26 @@ import DataThespian import Foundation import SwiftData -internal struct ItemModel: Identifiable { +internal struct ItemViewModel: Sendable, Identifiable { internal let model: Model internal let timestamp: Date + internal let children: [ChildViewModel] internal var id: PersistentIdentifier { model.persistentIdentifier } - private init(model: Model, timestamp: Date) { + private init(model: Model, timestamp: Date, children: [ChildViewModel]?) { self.model = model self.timestamp = timestamp + self.children = children ?? [] } internal init(item: Item) { - self.init(model: .init(item), timestamp: item.timestamp) + self.init( + model: .init(item), + timestamp: item.timestamp, + children: item.children?.map(ChildViewModel.init) + ) } } diff --git a/Sources/DataThespian/Databases/Database+Extras.swift b/Sources/DataThespian/Databases/Database+Extras.swift index 867b9f6..aa2cf88 100644 --- a/Sources/DataThespian/Databases/Database+Extras.swift +++ b/Sources/DataThespian/Databases/Database+Extras.swift @@ -32,13 +32,6 @@ public import SwiftData extension Database { - public func insert( - _ closuer: @Sendable @escaping () -> PersistentModelType - ) async -> Model { - let id: PersistentIdentifier = await self.insert(closuer) - return .init(persistentIdentifier: id) - } - public func with( _ id: Model, _ closure: @escaping @Sendable (PersistentModelType) throws -> U @@ -68,25 +61,25 @@ } } - public func first( - fetchWith selectPredicate: Predicate, - otherwiseInsertBy insert: @Sendable @escaping () -> T, - with closure: @escaping @Sendable (T) throws -> U - ) async throws -> U { - let value = try await self.fetch { - .init(predicate: selectPredicate, fetchLimit: 1) - } with: { models in - try models.first.map(closure) - } - - if let value { - return value - } - - let inserted: Model = await self.insert(insert) - - return try await self.with(inserted, closure) - } + // public func first( + // fetchWith selectPredicate: Predicate, + // otherwiseInsertBy insert: @Sendable @escaping () -> T, + // with closure: @escaping @Sendable (T) throws -> U + // ) async throws -> U { + // let value = try await self.fetch { + // .init(predicate: selectPredicate, fetchLimit: 1) + // } with: { models in + // try models.first.map(closure) + // } + // + // if let value { + // return value + // } + // + // let inserted: Model = await self.insert(insert) + // + // return try await self.with(inserted, closure) + // } public func delete(model _: T.Type, where predicate: Predicate? = nil) async throws diff --git a/Sources/DataThespian/Databases/Database+ModelContext.swift b/Sources/DataThespian/Databases/Database+ModelContext.swift index d544fff..90faeb6 100644 --- a/Sources/DataThespian/Databases/Database+ModelContext.swift +++ b/Sources/DataThespian/Databases/Database+ModelContext.swift @@ -42,9 +42,9 @@ try await self.withModelContext { try $0.delete(where: predicate) } } - public func insert(_ closuer: @Sendable @escaping () -> some PersistentModel) async - -> PersistentIdentifier - { await self.withModelContext { $0.insert(closuer) } } + // public func insert(_ closuer: @Sendable @escaping () -> some PersistentModel) async + // -> PersistentIdentifier + // { await self.withModelContext { $0.insert(closuer) } } public func fetch( _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor, diff --git a/Sources/DataThespian/Databases/Queryable+Extensions.swift b/Sources/DataThespian/Databases/Queryable+Extensions.swift index c8fe187..91d97fa 100644 --- a/Sources/DataThespian/Databases/Queryable+Extensions.swift +++ b/Sources/DataThespian/Databases/Queryable+Extensions.swift @@ -30,6 +30,7 @@ public import SwiftData extension Queryable { + @discardableResult public func insert( _ closuer: @Sendable @escaping () -> PersistentModelType ) async -> Model { @@ -115,3 +116,20 @@ extension Queryable { return try await self.get(for: .model(model), with: closure) } } + +extension Queryable { + public func deleteModels(_ models: [Model]) async throws + { + try await withThrowingTaskGroup( + of: Void.self, + body: { group in + for model in models { + group.addTask { + try await self.delete(.model(model)) + } + } + try await group.waitForAll() + } + ) + } +} diff --git a/Sources/DataThespian/Databases/Queryable.swift b/Sources/DataThespian/Databases/Queryable.swift index 6298775..dc3b697 100644 --- a/Sources/DataThespian/Databases/Queryable.swift +++ b/Sources/DataThespian/Databases/Queryable.swift @@ -29,7 +29,7 @@ public import SwiftData -public protocol Queryable { +public protocol Queryable: Sendable { func save() async throws func insert( diff --git a/Sources/DataThespian/Model.swift b/Sources/DataThespian/Model.swift index ed01dac..547d250 100644 --- a/Sources/DataThespian/Model.swift +++ b/Sources/DataThespian/Model.swift @@ -46,6 +46,10 @@ } extension Model where T: PersistentModel { + public var isTemporary: Bool { + self.persistentIdentifier.isTemporary ?? false + } + public init(_ model: T) { self.init(persistentIdentifier: model.persistentModelID) } internal static func ifMap(_ model: T?) -> Model? { model.map(self.init) } diff --git a/Sources/DataThespian/SwiftData/ModelContext.swift b/Sources/DataThespian/SwiftData/ModelContext.swift index b76bf18..ea462d5 100644 --- a/Sources/DataThespian/SwiftData/ModelContext.swift +++ b/Sources/DataThespian/SwiftData/ModelContext.swift @@ -29,10 +29,10 @@ #if canImport(SwiftData) import Foundation - import SwiftData + public import SwiftData extension ModelContext { - internal func existingModel(for model: Model) throws -> T? + public func existingModel(for model: Model) throws -> T? where T: PersistentModel { try self.existingModel(for: model.persistentIdentifier) } diff --git a/Sources/DataThespian/SwiftData/PersistentIdentifier.swift b/Sources/DataThespian/SwiftData/PersistentIdentifier.swift new file mode 100644 index 0000000..b0f39ba --- /dev/null +++ b/Sources/DataThespian/SwiftData/PersistentIdentifier.swift @@ -0,0 +1,88 @@ +// +// PersistentIdentifier.swift +// DataThespian +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import CoreData +import Foundation +import SwiftData + +/// Returns the value of a child property of an object using reflection. +/// +/// - Parameters: +/// - object: The object to inspect. +/// - childName: The name of the child property to retrieve. +/// - Returns: The value of the child property, or nil if it does not exist. +private func getMirrorChildValue(of object: Any, childName: String) -> Any? { + guard let child = Mirror(reflecting: object).children.first(where: { $0.label == childName }) + else { + return nil + } + + return child.value +} + +// Extension to add computed properties for accessing underlying CoreData +// implementation details of PersistentIdentifier +extension PersistentIdentifier { + // Private stored property to hold reference to underlying implementation + private var mirrorImplementation: Any? { + guard let implementation = getMirrorChildValue(of: self, childName: "implementation") else { + assertionFailure("Should always be there.") + return nil + } + return implementation + } + + // Computed property to access managedObjectID from implementation + private var objectID: NSManagedObjectID? { + guard let mirrorImplementation, + let objectID = getMirrorChildValue(of: mirrorImplementation, childName: "managedObjectID") + as? NSManagedObjectID + else { + return nil + } + return objectID + } + + // Computed property to access uriRepresentation from objectID + private var uriRepresentation: URL? { + objectID?.uriRepresentation() + } + + // swiftlint:disable:next discouraged_optional_boolean + internal var isTemporary: Bool? { + guard let mirrorImplementation, + let isTemporary = getMirrorChildValue(of: mirrorImplementation, childName: "isTemporary") + as? Bool + else { + assertionFailure("Should always be there.") + return nil + } + return isTemporary + } +} From a294e013dee017a59a4c6a78eed49dc376f952d5 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Wed, 16 Oct 2024 16:40:21 -0400 Subject: [PATCH 7/9] last commit before PR --- .../Databases/Database+ModelContext.swift | 4 - .../Databases/Database+Queryable.swift | 66 +++---- .../EnvironmentValues+Database.swift | 2 +- .../DataThespian/Databases/QueryError.swift | 10 +- .../Databases/Queryable+Extensions.swift | 170 +++++++++--------- .../DataThespian/Databases/Queryable.swift | 36 ++-- Sources/DataThespian/Databases/Selector.swift | 60 ++++--- .../Databases/UniqueKeyPath.swift | 5 +- .../SwiftData/ModelContext+Queryable.swift | 86 ++++----- .../SwiftData/PersistentIdentifier.swift | 100 ++++++----- 10 files changed, 275 insertions(+), 264 deletions(-) diff --git a/Sources/DataThespian/Databases/Database+ModelContext.swift b/Sources/DataThespian/Databases/Database+ModelContext.swift index 90faeb6..8735e58 100644 --- a/Sources/DataThespian/Databases/Database+ModelContext.swift +++ b/Sources/DataThespian/Databases/Database+ModelContext.swift @@ -42,10 +42,6 @@ try await self.withModelContext { try $0.delete(where: predicate) } } - // public func insert(_ closuer: @Sendable @escaping () -> some PersistentModel) async - // -> PersistentIdentifier - // { await self.withModelContext { $0.insert(closuer) } } - public func fetch( _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor, with closure: @escaping @Sendable ([T]) throws -> U diff --git a/Sources/DataThespian/Databases/Database+Queryable.swift b/Sources/DataThespian/Databases/Database+Queryable.swift index f887682..403e585 100644 --- a/Sources/DataThespian/Databases/Database+Queryable.swift +++ b/Sources/DataThespian/Databases/Database+Queryable.swift @@ -27,45 +27,47 @@ // OTHER DEALINGS IN THE SOFTWARE. // -public import SwiftData +#if canImport(SwiftData) + public import SwiftData -extension Database { - public func save() async throws { - try await self.withModelContext { try $0.save() } - } + extension Database { + public func save() async throws { + try await self.withModelContext { try $0.save() } + } - public func insert( - _ closuer: @Sendable @escaping () -> PersistentModelType, - with closure: @escaping @Sendable (PersistentModelType) throws -> U - ) async rethrows -> U { - try await self.withModelContext { - try $0.insert(closuer, with: closure) + public func insert( + _ closuer: @Sendable @escaping () -> PersistentModelType, + with closure: @escaping @Sendable (PersistentModelType) throws -> U + ) async rethrows -> U { + try await self.withModelContext { + try $0.insert(closuer, with: closure) + } } - } - public func getOptional( - for selector: Selector.Get, - with closure: @escaping @Sendable (PersistentModelType?) throws -> U - ) async rethrows -> U { - try await self.withModelContext { - try $0.getOptional(for: selector, with: closure) + public func getOptional( + for selector: Selector.Get, + with closure: @escaping @Sendable (PersistentModelType?) throws -> U + ) async rethrows -> U { + try await self.withModelContext { + try $0.getOptional(for: selector, with: closure) + } } - } - public func fetch( - for selector: Selector.List, - with closure: @escaping @Sendable ([PersistentModelType]) throws -> U - ) async rethrows -> U { - try await self.withModelContext { - try $0.fetch(for: selector, with: closure) + public func fetch( + for selector: Selector.List, + with closure: @escaping @Sendable ([PersistentModelType]) throws -> U + ) async rethrows -> U { + try await self.withModelContext { + try $0.fetch(for: selector, with: closure) + } } - } - public func delete(_ selector: Selector.Delete) - async throws - { - try await self.withModelContext { - try $0.delete(selector) + public func delete(_ selector: Selector.Delete) + async throws + { + try await self.withModelContext { + try $0.delete(selector) + } } } -} +#endif diff --git a/Sources/DataThespian/Databases/EnvironmentValues+Database.swift b/Sources/DataThespian/Databases/EnvironmentValues+Database.swift index b2ed50a..966ef15 100644 --- a/Sources/DataThespian/Databases/EnvironmentValues+Database.swift +++ b/Sources/DataThespian/Databases/EnvironmentValues+Database.swift @@ -27,7 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -#if canImport(SwiftUI) +#if canImport(SwiftUI) && canImport(SwiftData) import Foundation import SwiftData public import SwiftUI diff --git a/Sources/DataThespian/Databases/QueryError.swift b/Sources/DataThespian/Databases/QueryError.swift index b2ef23d..7b31182 100644 --- a/Sources/DataThespian/Databases/QueryError.swift +++ b/Sources/DataThespian/Databases/QueryError.swift @@ -27,8 +27,10 @@ // OTHER DEALINGS IN THE SOFTWARE. // -public import SwiftData +#if canImport(SwiftData) + public import SwiftData -public enum QueryError: Error { - case itemNotFound(Selector.Get) -} + public enum QueryError: Error { + case itemNotFound(Selector.Get) + } +#endif diff --git a/Sources/DataThespian/Databases/Queryable+Extensions.swift b/Sources/DataThespian/Databases/Queryable+Extensions.swift index 91d97fa..4594bc6 100644 --- a/Sources/DataThespian/Databases/Queryable+Extensions.swift +++ b/Sources/DataThespian/Databases/Queryable+Extensions.swift @@ -27,109 +27,111 @@ // OTHER DEALINGS IN THE SOFTWARE. // -public import SwiftData +#if canImport(SwiftData) + public import SwiftData -extension Queryable { - @discardableResult - public func insert( - _ closuer: @Sendable @escaping () -> PersistentModelType - ) async -> Model { - await self.insert(closuer, with: Model.init) - } + extension Queryable { + @discardableResult + public func insert( + _ closuer: @Sendable @escaping () -> PersistentModelType + ) async -> Model { + await self.insert(closuer, with: Model.init) + } - public func getOptional( - for selector: Selector.Get - ) async -> Model? { - await self.getOptional(for: selector) { persistentModel in - persistentModel.flatMap(Model.init) + public func getOptional( + for selector: Selector.Get + ) async -> Model? { + await self.getOptional(for: selector) { persistentModel in + persistentModel.flatMap(Model.init) + } } - } - public func fetch( - for selector: Selector.List - ) async -> [Model] { - await self.fetch(for: selector) { persistentModels in - persistentModels.map(Model.init) + public func fetch( + for selector: Selector.List + ) async -> [Model] { + await self.fetch(for: selector) { persistentModels in + persistentModels.map(Model.init) + } } - } - public func get( - for selector: Selector.Get - ) async throws -> Model { - try await self.getOptional(for: selector) { persistentModel in - guard let persistentModel else { - throw QueryError.itemNotFound(selector) + public func get( + for selector: Selector.Get + ) async throws -> Model { + try await self.getOptional(for: selector) { persistentModel in + guard let persistentModel else { + throw QueryError.itemNotFound(selector) + } + return Model(persistentModel) } - return Model(persistentModel) } - } - public func get( - for selector: Selector.Get, - with closure: @escaping @Sendable (PersistentModelType) throws -> U - ) async throws -> U { - try await self.getOptional(for: selector) { persistentModel in - guard let persistentModel else { - throw QueryError.itemNotFound(selector) + public func get( + for selector: Selector.Get, + with closure: @escaping @Sendable (PersistentModelType) throws -> U + ) async throws -> U { + try await self.getOptional(for: selector) { persistentModel in + guard let persistentModel else { + throw QueryError.itemNotFound(selector) + } + return try closure(persistentModel) } - return try closure(persistentModel) } - } - public func update( - for selector: Selector.Get, - with closure: @escaping @Sendable (PersistentModelType) throws -> Void - ) async throws { - try await self.get(for: selector, with: closure) - } + public func update( + for selector: Selector.Get, + with closure: @escaping @Sendable (PersistentModelType) throws -> Void + ) async throws { + try await self.get(for: selector, with: closure) + } - public func update( - for selector: Selector.List, - with closure: @escaping @Sendable ([PersistentModelType]) throws -> Void - ) async throws { - try await self.fetch(for: selector, with: closure) - } + public func update( + for selector: Selector.List, + with closure: @escaping @Sendable ([PersistentModelType]) throws -> Void + ) async throws { + try await self.fetch(for: selector, with: closure) + } - public func insertIf( - _ model: @Sendable @escaping () -> PersistentModelType, - notExist selector: @Sendable @escaping (PersistentModelType) -> - Selector.Get - ) async -> Model { - let persistentModel = model() - let selector = selector(persistentModel) - let modelOptional = await self.getOptional(for: selector) + public func insertIf( + _ model: @Sendable @escaping () -> PersistentModelType, + notExist selector: @Sendable @escaping (PersistentModelType) -> + Selector.Get + ) async -> Model { + let persistentModel = model() + let selector = selector(persistentModel) + let modelOptional = await self.getOptional(for: selector) - if let modelOptional { - return modelOptional - } else { - return await self.insert(model) + if let modelOptional { + return modelOptional + } else { + return await self.insert(model) + } } - } - public func insertIf( - _ model: @Sendable @escaping () -> PersistentModelType, - notExist selector: @Sendable @escaping (PersistentModelType) -> - Selector.Get, - with closure: @escaping @Sendable (PersistentModelType) throws -> U - ) async throws -> U { - let model = await self.insertIf(model, notExist: selector) - return try await self.get(for: .model(model), with: closure) + public func insertIf( + _ model: @Sendable @escaping () -> PersistentModelType, + notExist selector: @Sendable @escaping (PersistentModelType) -> + Selector.Get, + with closure: @escaping @Sendable (PersistentModelType) throws -> U + ) async throws -> U { + let model = await self.insertIf(model, notExist: selector) + return try await self.get(for: .model(model), with: closure) + } } -} -extension Queryable { - public func deleteModels(_ models: [Model]) async throws - { - try await withThrowingTaskGroup( - of: Void.self, - body: { group in - for model in models { - group.addTask { - try await self.delete(.model(model)) + extension Queryable { + public func deleteModels(_ models: [Model]) async throws + { + try await withThrowingTaskGroup( + of: Void.self, + body: { group in + for model in models { + group.addTask { + try await self.delete(.model(model)) + } } + try await group.waitForAll() } - try await group.waitForAll() - } - ) + ) + } } -} +#endif diff --git a/Sources/DataThespian/Databases/Queryable.swift b/Sources/DataThespian/Databases/Queryable.swift index dc3b697..8bb0de9 100644 --- a/Sources/DataThespian/Databases/Queryable.swift +++ b/Sources/DataThespian/Databases/Queryable.swift @@ -27,25 +27,27 @@ // OTHER DEALINGS IN THE SOFTWARE. // -public import SwiftData +#if canImport(SwiftData) + public import SwiftData -public protocol Queryable: Sendable { - func save() async throws + public protocol Queryable: Sendable { + func save() async throws - func insert( - _ closuer: @Sendable @escaping () -> PersistentModelType, - with closure: @escaping @Sendable (PersistentModelType) throws -> U - ) async rethrows -> U + func insert( + _ closuer: @Sendable @escaping () -> PersistentModelType, + with closure: @escaping @Sendable (PersistentModelType) throws -> U + ) async rethrows -> U - func getOptional( - for selector: Selector.Get, - with closure: @escaping @Sendable (PersistentModelType?) throws -> U - ) async rethrows -> U + func getOptional( + for selector: Selector.Get, + with closure: @escaping @Sendable (PersistentModelType?) throws -> U + ) async rethrows -> U - func fetch( - for selector: Selector.List, - with closure: @escaping @Sendable ([PersistentModelType]) throws -> U - ) async rethrows -> U + func fetch( + for selector: Selector.List, + with closure: @escaping @Sendable ([PersistentModelType]) throws -> U + ) async rethrows -> U - func delete(_ selector: Selector.Delete) async throws -} + func delete(_ selector: Selector.Delete) async throws + } +#endif diff --git a/Sources/DataThespian/Databases/Selector.swift b/Sources/DataThespian/Databases/Selector.swift index ce9f491..6b2e559 100644 --- a/Sources/DataThespian/Databases/Selector.swift +++ b/Sources/DataThespian/Databases/Selector.swift @@ -27,38 +27,40 @@ // OTHER DEALINGS IN THE SOFTWARE. // -public import Foundation -public import SwiftData +#if canImport(SwiftData) + public import Foundation + public import SwiftData -public enum Selector: Sendable { - public enum Delete: Sendable { - case predicate(Predicate) - case all - case model(Model) + public enum Selector: Sendable { + public enum Delete: Sendable { + case predicate(Predicate) + case all + case model(Model) + } + public enum List: Sendable { + case descriptor(FetchDescriptor) + } + public enum Get: Sendable { + case model(Model) + case predicate(Predicate) + } } - public enum List: Sendable { - case descriptor(FetchDescriptor) - } - public enum Get: Sendable { - case model(Model) - case predicate(Predicate) - } -} -extension Selector.Get { - @available(*, unavailable, message: "Not implemented yet.") - public static func unique( - _ key: UniqueKeyableType, - equals value: UniqueKeyableType.ValueType - ) -> Self where UniqueKeyableType.Model == T { - .predicate( - key.predicate(equals: value) - ) + extension Selector.Get { + @available(*, unavailable, message: "Not implemented yet.") + public static func unique( + _ key: UniqueKeyableType, + equals value: UniqueKeyableType.ValueType + ) -> Self where UniqueKeyableType.Model == T { + .predicate( + key.predicate(equals: value) + ) + } } -} -extension Selector.List { - public static func all() -> Selector.List { - .descriptor(.init()) + extension Selector.List { + public static func all() -> Selector.List { + .descriptor(.init()) + } } -} +#endif diff --git a/Sources/DataThespian/Databases/UniqueKeyPath.swift b/Sources/DataThespian/Databases/UniqueKeyPath.swift index a1c1034..9c065ca 100644 --- a/Sources/DataThespian/Databases/UniqueKeyPath.swift +++ b/Sources/DataThespian/Databases/UniqueKeyPath.swift @@ -30,12 +30,13 @@ public import Foundation public struct UniqueKeyPath: UniqueKey { + private let keyPath: KeyPath & Sendable + internal init(keyPath: any KeyPath & Sendable) { self.keyPath = keyPath } - private let keyPath: KeyPath & Sendable - + // swiftlint:disable:next unavailable_function public func predicate(equals value: ValueType) -> Predicate { fatalError("Not implemented yet.") } diff --git a/Sources/DataThespian/SwiftData/ModelContext+Queryable.swift b/Sources/DataThespian/SwiftData/ModelContext+Queryable.swift index 1f1a2bd..934e8b5 100644 --- a/Sources/DataThespian/SwiftData/ModelContext+Queryable.swift +++ b/Sources/DataThespian/SwiftData/ModelContext+Queryable.swift @@ -27,54 +27,56 @@ // OTHER DEALINGS IN THE SOFTWARE. // -public import SwiftData +#if canImport(SwiftData) + public import SwiftData -extension ModelContext { - public func insert( - _ closuer: @Sendable @escaping () -> PersistentModelType, - with closure: @escaping @Sendable (PersistentModelType) throws -> U - ) rethrows -> U { - let persistentModel = closuer() - self.insert(persistentModel) - return try closure(persistentModel) - } + extension ModelContext { + public func insert( + _ closuer: @Sendable @escaping () -> PersistentModelType, + with closure: @escaping @Sendable (PersistentModelType) throws -> U + ) rethrows -> U { + let persistentModel = closuer() + self.insert(persistentModel) + return try closure(persistentModel) + } - public func getOptional( - for selector: Selector.Get, - with closure: @escaping @Sendable (PersistentModelType?) throws -> U - ) throws -> U { - let persistentModel: PersistentModelType? - switch selector { - case .model(let model): - persistentModel = try self.existingModel(for: model) - case .predicate(let predicate): - persistentModel = try self.first(where: predicate) + public func getOptional( + for selector: Selector.Get, + with closure: @escaping @Sendable (PersistentModelType?) throws -> U + ) throws -> U { + let persistentModel: PersistentModelType? + switch selector { + case .model(let model): + persistentModel = try self.existingModel(for: model) + case .predicate(let predicate): + persistentModel = try self.first(where: predicate) + } + return try closure(persistentModel) } - return try closure(persistentModel) - } - public func fetch( - for selector: Selector.List, - with closure: @escaping @Sendable ([PersistentModelType]) throws -> U - ) throws -> U { - let persistentModels: [PersistentModelType] - switch selector { - case .descriptor(let descriptor): - persistentModels = try self.fetch(descriptor) + public func fetch( + for selector: Selector.List, + with closure: @escaping @Sendable ([PersistentModelType]) throws -> U + ) throws -> U { + let persistentModels: [PersistentModelType] + switch selector { + case .descriptor(let descriptor): + persistentModels = try self.fetch(descriptor) + } + return try closure(persistentModels) } - return try closure(persistentModels) - } - public func delete(_ selector: Selector.Delete) throws { - switch selector { - case .all: - try self.delete(model: PersistentModelType.self) - case .model(let model): - if let persistentModel = try self.existingModel(for: model) { - self.delete(persistentModel) + public func delete(_ selector: Selector.Delete) throws { + switch selector { + case .all: + try self.delete(model: PersistentModelType.self) + case .model(let model): + if let persistentModel = try self.existingModel(for: model) { + self.delete(persistentModel) + } + case .predicate(let predicate): + try self.delete(model: PersistentModelType.self, where: predicate) } - case .predicate(let predicate): - try self.delete(model: PersistentModelType.self, where: predicate) } } -} +#endif diff --git a/Sources/DataThespian/SwiftData/PersistentIdentifier.swift b/Sources/DataThespian/SwiftData/PersistentIdentifier.swift index b0f39ba..dee0c99 100644 --- a/Sources/DataThespian/SwiftData/PersistentIdentifier.swift +++ b/Sources/DataThespian/SwiftData/PersistentIdentifier.swift @@ -27,62 +27,64 @@ // OTHER DEALINGS IN THE SOFTWARE. // -import CoreData -import Foundation -import SwiftData +#if canImport(CoreData) && canImport(SwiftData) + import CoreData + import Foundation + import SwiftData -/// Returns the value of a child property of an object using reflection. -/// -/// - Parameters: -/// - object: The object to inspect. -/// - childName: The name of the child property to retrieve. -/// - Returns: The value of the child property, or nil if it does not exist. -private func getMirrorChildValue(of object: Any, childName: String) -> Any? { - guard let child = Mirror(reflecting: object).children.first(where: { $0.label == childName }) - else { - return nil - } - - return child.value -} - -// Extension to add computed properties for accessing underlying CoreData -// implementation details of PersistentIdentifier -extension PersistentIdentifier { - // Private stored property to hold reference to underlying implementation - private var mirrorImplementation: Any? { - guard let implementation = getMirrorChildValue(of: self, childName: "implementation") else { - assertionFailure("Should always be there.") + /// Returns the value of a child property of an object using reflection. + /// + /// - Parameters: + /// - object: The object to inspect. + /// - childName: The name of the child property to retrieve. + /// - Returns: The value of the child property, or nil if it does not exist. + private func getMirrorChildValue(of object: Any, childName: String) -> Any? { + guard let child = Mirror(reflecting: object).children.first(where: { $0.label == childName }) + else { return nil } - return implementation + + return child.value } - // Computed property to access managedObjectID from implementation - private var objectID: NSManagedObjectID? { - guard let mirrorImplementation, - let objectID = getMirrorChildValue(of: mirrorImplementation, childName: "managedObjectID") - as? NSManagedObjectID - else { - return nil + // Extension to add computed properties for accessing underlying CoreData + // implementation details of PersistentIdentifier + extension PersistentIdentifier { + // Private stored property to hold reference to underlying implementation + private var mirrorImplementation: Any? { + guard let implementation = getMirrorChildValue(of: self, childName: "implementation") else { + assertionFailure("Should always be there.") + return nil + } + return implementation } - return objectID - } - // Computed property to access uriRepresentation from objectID - private var uriRepresentation: URL? { - objectID?.uriRepresentation() - } + // Computed property to access managedObjectID from implementation + private var objectID: NSManagedObjectID? { + guard let mirrorImplementation, + let objectID = getMirrorChildValue(of: mirrorImplementation, childName: "managedObjectID") + as? NSManagedObjectID + else { + return nil + } + return objectID + } - // swiftlint:disable:next discouraged_optional_boolean - internal var isTemporary: Bool? { - guard let mirrorImplementation, - let isTemporary = getMirrorChildValue(of: mirrorImplementation, childName: "isTemporary") - as? Bool - else { - assertionFailure("Should always be there.") - return nil + // Computed property to access uriRepresentation from objectID + private var uriRepresentation: URL? { + objectID?.uriRepresentation() + } + + // swiftlint:disable:next discouraged_optional_boolean + internal var isTemporary: Bool? { + guard let mirrorImplementation, + let isTemporary = getMirrorChildValue(of: mirrorImplementation, childName: "isTemporary") + as? Bool + else { + assertionFailure("Should always be there.") + return nil + } + return isTemporary } - return isTemporary } -} +#endif From e67db79ad66a53a3c0ea7752f168f9ce3ab30366 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Wed, 16 Oct 2024 17:25:08 -0400 Subject: [PATCH 8/9] finishing PR --- Example/Sources/ContentObject.swift | 4 +-- .../Databases/Database+Extras.swift | 32 +++++++------------ .../Databases/Database+ModelContext.swift | 6 ++++ .../Databases/Queryable+Extensions.swift | 3 +- .../SwiftData/ModelContext+Queryable.swift | 7 ++-- .../DataThespian/SwiftData/ModelContext.swift | 2 +- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Example/Sources/ContentObject.swift b/Example/Sources/ContentObject.swift index 85394b2..d0ebf39 100644 --- a/Example/Sources/ContentObject.swift +++ b/Example/Sources/ContentObject.swift @@ -119,8 +119,8 @@ internal class ContentObject { } try await database.withModelContext { modelContext in - let item = try modelContext.existingModel(for: item.model) - let child = try modelContext.existingModel(for: childModel) + let item = try modelContext.get(item.model) + let child = try modelContext.get(childModel) assert(child != nil && item != nil) child?.parent = item try modelContext.save() diff --git a/Sources/DataThespian/Databases/Database+Extras.swift b/Sources/DataThespian/Databases/Database+Extras.swift index aa2cf88..5ff5f78 100644 --- a/Sources/DataThespian/Databases/Database+Extras.swift +++ b/Sources/DataThespian/Databases/Database+Extras.swift @@ -32,6 +32,7 @@ public import SwiftData extension Database { + @available(*, deprecated) public func with( _ id: Model, _ closure: @escaping @Sendable (PersistentModelType) throws -> U @@ -46,11 +47,13 @@ } } + @available(*, deprecated) public func first(_ selectPredicate: Predicate) async throws -> Model? { try await self.first(selectPredicate, with: Model.ifMap) } + @available(*, deprecated) public func first( _ selectPredicate: Predicate, with closure: @escaping @Sendable (T?) throws -> U ) async throws -> U { @@ -61,39 +64,23 @@ } } - // public func first( - // fetchWith selectPredicate: Predicate, - // otherwiseInsertBy insert: @Sendable @escaping () -> T, - // with closure: @escaping @Sendable (T) throws -> U - // ) async throws -> U { - // let value = try await self.fetch { - // .init(predicate: selectPredicate, fetchLimit: 1) - // } with: { models in - // try models.first.map(closure) - // } - // - // if let value { - // return value - // } - // - // let inserted: Model = await self.insert(insert) - // - // return try await self.with(inserted, closure) - // } - + @available(*, deprecated) public func delete(model _: T.Type, where predicate: Predicate? = nil) async throws { try await self.delete(where: predicate) } + @available(*, deprecated) public func delete(_ model: Model) async { await self.delete(T.self, withID: model.persistentIdentifier) } + @available(*, deprecated) public func deleteAll(of types: [any PersistentModel.Type]) async throws { try await self.transaction { context in for type in types { try context.delete(model: type) } } } + @available(*, deprecated) public func fetch( _: T.Type, with closure: @escaping @Sendable ([T]) throws -> U ) async throws -> U { @@ -104,15 +91,19 @@ } } + @available(*, deprecated) public func fetch(_: T.Type) async throws -> [Model] { try await self.fetch(T.self) { models in models.map(Model.init) } } + + @available(*, deprecated) public func fetch( _: T.Type, _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor ) async throws -> [Model] { await self.fetch(selectDescriptor) { models in models.map(Model.init) } } + @available(*, deprecated) public func fetch( of _: T.Type, for objectIDs: [PersistentIdentifier], @@ -129,6 +120,7 @@ } } + @available(*, deprecated) public func get( of _: T.Type, for objectID: PersistentIdentifier, diff --git a/Sources/DataThespian/Databases/Database+ModelContext.swift b/Sources/DataThespian/Databases/Database+ModelContext.swift index 8735e58..763889c 100644 --- a/Sources/DataThespian/Databases/Database+ModelContext.swift +++ b/Sources/DataThespian/Databases/Database+ModelContext.swift @@ -34,14 +34,17 @@ public import SwiftData extension Database { + @available(*, deprecated) @discardableResult public func delete( _ modelType: T.Type, withID id: PersistentIdentifier ) async -> Bool { await self.withModelContext { $0.delete(modelType, withID: id) } } + @available(*, deprecated) public func delete(where predicate: Predicate?) async throws { try await self.withModelContext { try $0.delete(where: predicate) } } + @available(*, deprecated) public func fetch( _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor, with closure: @escaping @Sendable ([T]) throws -> U @@ -49,6 +52,7 @@ try await self.withModelContext { try $0.fetch(selectDescriptor, with: closure) } } + @available(*, deprecated) public func fetch( _ selectDescriptorA: @escaping @Sendable () -> FetchDescriptor, _ selectDescriptorB: @escaping @Sendable () -> FetchDescriptor, @@ -59,12 +63,14 @@ } } + @available(*, deprecated) public func get( for objectID: PersistentIdentifier, with closure: @escaping @Sendable (T?) throws -> U ) async rethrows -> U where T: PersistentModel { try await self.withModelContext { try $0.get(for: objectID, with: closure) } } + @available(*, deprecated) public func transaction(_ block: @Sendable @escaping (ModelContext) throws -> Void) async throws { try await self.withModelContext { try $0.transaction(block: block) } } } diff --git a/Sources/DataThespian/Databases/Queryable+Extensions.swift b/Sources/DataThespian/Databases/Queryable+Extensions.swift index 4594bc6..903aefe 100644 --- a/Sources/DataThespian/Databases/Queryable+Extensions.swift +++ b/Sources/DataThespian/Databases/Queryable+Extensions.swift @@ -119,7 +119,8 @@ } extension Queryable { - public func deleteModels(_ models: [Model]) async throws + public func deleteModels(_ models: [Model]) + async throws { try await withThrowingTaskGroup( of: Void.self, diff --git a/Sources/DataThespian/SwiftData/ModelContext+Queryable.swift b/Sources/DataThespian/SwiftData/ModelContext+Queryable.swift index 934e8b5..4be7149 100644 --- a/Sources/DataThespian/SwiftData/ModelContext+Queryable.swift +++ b/Sources/DataThespian/SwiftData/ModelContext+Queryable.swift @@ -47,7 +47,7 @@ let persistentModel: PersistentModelType? switch selector { case .model(let model): - persistentModel = try self.existingModel(for: model) + persistentModel = try self.get(model) case .predicate(let predicate): persistentModel = try self.first(where: predicate) } @@ -66,12 +66,13 @@ return try closure(persistentModels) } - public func delete(_ selector: Selector.Delete) throws { + public func delete(_ selector: Selector.Delete) throws + { switch selector { case .all: try self.delete(model: PersistentModelType.self) case .model(let model): - if let persistentModel = try self.existingModel(for: model) { + if let persistentModel = try self.get(model) { self.delete(persistentModel) } case .predicate(let predicate): diff --git a/Sources/DataThespian/SwiftData/ModelContext.swift b/Sources/DataThespian/SwiftData/ModelContext.swift index ea462d5..ef77a06 100644 --- a/Sources/DataThespian/SwiftData/ModelContext.swift +++ b/Sources/DataThespian/SwiftData/ModelContext.swift @@ -32,7 +32,7 @@ public import SwiftData extension ModelContext { - public func existingModel(for model: Model) throws -> T? + public func get(_ model: Model) throws -> T? where T: PersistentModel { try self.existingModel(for: model.persistentIdentifier) } From 5a104c545fe36606cea06585510bb9382c16d57a Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Wed, 16 Oct 2024 17:32:21 -0400 Subject: [PATCH 9/9] one more thing --- .../DataThespian/SwiftData/ModelContext.swift | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Sources/DataThespian/SwiftData/ModelContext.swift b/Sources/DataThespian/SwiftData/ModelContext.swift index ef77a06..251dfb3 100644 --- a/Sources/DataThespian/SwiftData/ModelContext.swift +++ b/Sources/DataThespian/SwiftData/ModelContext.swift @@ -34,8 +34,27 @@ extension ModelContext { public func get(_ model: Model) throws -> T? where T: PersistentModel { - try self.existingModel(for: model.persistentIdentifier) + try self.persistentModel(withID: model.persistentIdentifier) } + + private func persistentModel(withID objectID: PersistentIdentifier) throws -> T? + where T: PersistentModel { + if let registered: T = registeredModel(for: objectID) { + return registered + } + if let notRegistered: T = model(for: objectID) as? T { + return notRegistered + } + + let fetchDescriptor = FetchDescriptor( + predicate: #Predicate { $0.persistentModelID == objectID }, + fetchLimit: 1 + ) + + return try fetch(fetchDescriptor).first + } + + @available(*, deprecated) internal func existingModel(for objectID: PersistentIdentifier) throws -> T? where T: PersistentModel { if let registered: T = registeredModel(for: objectID) {