[PM-21602] refactor: Consolidate CoreData helpers in BitwardenKit (#1579)

This commit is contained in:
Katherine Bertelsen 2025-05-14 14:30:49 -05:00 committed by GitHub
parent 6c0891a0d0
commit 8aabc70a87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 36 additions and 923 deletions

View File

@ -1,3 +1,4 @@
import BitwardenKit
import CoreData
import Foundation
@ -47,7 +48,7 @@ extension AuthenticatorBridgeItemData: ManagedUserObject {
/// - id: The id to match in the predicate
/// - Returns: The NSPredicate for searching/filtering by userId and id
///
static func userIdAndIdPredicate(userId: String, id: String) -> NSPredicate {
public static func userIdAndIdPredicate(userId: String, id: String) -> NSPredicate {
NSPredicate(
format: "%K == %@ AND %K == %@",
#keyPath(AuthenticatorBridgeItemData.userId),
@ -62,7 +63,7 @@ extension AuthenticatorBridgeItemData: ManagedUserObject {
/// - Parameter userId: The userId to match in the predicate
/// - Returns: The NSPredicate for searching/filtering by userId
///
static func userIdPredicate(userId: String) -> NSPredicate {
public static func userIdPredicate(userId: String) -> NSPredicate {
NSPredicate(format: "%K == %@", #keyPath(AuthenticatorBridgeItemData.userId), userId)
}
@ -72,7 +73,7 @@ extension AuthenticatorBridgeItemData: ManagedUserObject {
/// - value: the `AuthenticatorBridgeItemDataModel` to use in updating the object
/// - userId: userId to update this object with.
///
func update(with value: AuthenticatorBridgeItemDataModel, userId: String) throws {
public func update(with value: AuthenticatorBridgeItemDataModel, userId: String) throws {
id = value.id
model = value
self.userId = userId

View File

@ -1,3 +1,4 @@
import BitwardenKit
import Combine
import Foundation

View File

@ -1,40 +0,0 @@
import CoreData
import OSLog
/// A protocol for a `NSManagedObject` which persists a data model as JSON encoded data. The model
/// can be set via the `model` property which encodes the model to the data property, which should
/// be a `@NSManaged` property of the `NSManagedObject`. When the managed object is populated from
/// the database, the `model` property can be read to decode the data.
///
protocol CodableModelData: AnyObject, NSManagedObject {
associatedtype Model: Codable
/// A `@NSManaged` property of the manage object for storing the encoded model as data.
var modelData: Data? { get set }
}
extension CodableModelData {
/// Encodes or decodes the model to/from the data instance.
var model: Model? {
get {
guard let modelData else { return nil }
do {
return try JSONDecoder().decode(Model.self, from: modelData)
} catch {
Logger.bridgeKit.error("Error decoding \(String(describing: Model.self)): \(error)")
return nil
}
}
set {
guard let newValue else {
modelData = nil
return
}
do {
modelData = try JSONEncoder().encode(newValue)
} catch {
Logger.bridgeKit.error("Error encoding \(String(describing: Model.self)): \(error)")
}
}
}
}

View File

@ -1,78 +0,0 @@
import CoreData
/// A protocol for an `NSManagedObject` data model that adds some convenience methods for working
/// with Core Data.
///
protocol ManagedObject: AnyObject {
/// The name of the entity of the managed object, as defined in the data model.
static var entityName: String { get }
}
extension ManagedObject where Self: NSManagedObject {
static var entityName: String {
String(describing: self)
}
/// Returns a `NSBatchInsertRequest` for batch inserting an array of objects.
///
/// - Parameters:
/// - objects: The objects (or objects that can be converted to managed objects) to insert.
/// - handler: A handler that is called for each object to set the properties on the
/// `NSManagedObject` to insert.
/// - Returns: A `NSBatchInsertRequest` for batch inserting an array of objects.
///
static func batchInsertRequest<T>(
objects: [T],
handler: @escaping (Self, T) throws -> Void
) throws -> NSBatchInsertRequest {
var index = 0
var errorToThrow: Error?
let insertRequest = NSBatchInsertRequest(entityName: entityName) { (managedObject: NSManagedObject) -> Bool in
guard index < objects.count else { return true }
defer { index += 1 }
if let managedObject = (managedObject as? Self) {
do {
try handler(managedObject, objects[index])
} catch {
// The error can't be thrown directly in this closure, so capture it, return
// from the closure, and then throw it.
errorToThrow = error
return true
}
}
return false
}
if let errorToThrow {
throw errorToThrow
}
return insertRequest
}
/// Returns a `NSFetchRequest` for fetching instances of the managed object.
///
/// - Parameter predicate: An optional predicate to apply to the fetch request.
/// - Returns: A `NSFetchRequest` used to fetch instances of the managed object.
///
static func fetchRequest(predicate: NSPredicate? = nil) -> NSFetchRequest<Self> {
let fetchRequest = NSFetchRequest<Self>(entityName: entityName)
fetchRequest.predicate = predicate
return fetchRequest
}
/// Returns a `NSFetchRequest` for fetching a generic `NSFetchRequestResult` instances of the
/// managed object.
///
/// - Parameter predicate: An optional predicate to apply to the fetch request.
/// - Returns: A `NSFetchRequest` used to fetch generic `NSFetchRequestResult` instances of the
/// managed object.
///
static func fetchResultRequest(predicate: NSPredicate? = nil) -> NSFetchRequest<NSFetchRequestResult> {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
fetchRequest.predicate = predicate
return fetchRequest
}
}

View File

@ -1,74 +0,0 @@
import CoreData
/// A protocol for a `ManagedObject` data model associated with a user that adds some convenience
/// methods for building `NSPersistentStoreRequest` for common CRUD operations.
///
protocol ManagedUserObject: ManagedObject {
/// The value type (struct) associated with the managed object that is persisted in the database.
associatedtype ValueType
/// Returns a `NSPredicate` used for filtering by a user's ID.
///
/// - Parameter userId: The user ID associated with the managed object.
///
static func userIdPredicate(userId: String) -> NSPredicate
/// Returns a `NSPredicate` used for filtering by a user and managed object ID.
///
/// - Parameter userId: The user ID associated with the managed object.
///
static func userIdAndIdPredicate(userId: String, id: String) -> NSPredicate
/// Updates the managed object from its associated value type object and user ID.
///
/// - Parameters:
/// - value: The value type object used to update the managed object.
/// - userId: The user ID associated with the object.
///
func update(with value: ValueType, userId: String) throws
}
extension ManagedUserObject where Self: NSManagedObject {
/// A `NSBatchInsertRequest` that inserts objects for the specified user.
///
/// - Parameters:
/// - objects: The list of objects to insert.
/// - userId: The user associated with the objects to insert.
/// - Returns: A `NSBatchInsertRequest` that inserts the objects for the user.
///
static func batchInsertRequest(objects: [ValueType], userId: String) throws -> NSBatchInsertRequest {
try batchInsertRequest(objects: objects) { object, value in
try object.update(with: value, userId: userId)
}
}
/// A `NSBatchDeleteRequest` that deletes all objects for the specified user.
///
/// - Parameter userId: The user associated with the objects to delete.
/// - Returns: A `NSBatchDeleteRequest` that deletes all objects for the user.
///
static func deleteByUserIdRequest(userId: String) -> NSBatchDeleteRequest {
let fetchRequest = fetchResultRequest(predicate: userIdPredicate(userId: userId))
return NSBatchDeleteRequest(fetchRequest: fetchRequest)
}
/// A `NSFetchRequest` that fetches objects for the specified user matching an ID.
///
/// - Parameters:
/// - id: The ID of the object to fetch.
/// - userId: The user associated with the object to fetch.
/// - Returns: A `NSFetchRequest` that fetches all objects for the user.
///
static func fetchByIdRequest(id: String, userId: String) -> NSFetchRequest<Self> {
fetchRequest(predicate: userIdAndIdPredicate(userId: userId, id: id))
}
/// A `NSFetchRequest` that fetches all objects for the specified user.
///
/// - Parameter userId: The user associated with the objects to delete.
/// - Returns: A `NSFetchRequest` that fetches all objects for the user.
///
static func fetchByUserIdRequest(userId: String) -> NSFetchRequest<Self> {
fetchRequest(predicate: userIdPredicate(userId: userId))
}
}

View File

@ -1,52 +0,0 @@
import CoreData
extension NSManagedObjectContext {
/// Executes the batch delete request and/or batch insert request and merges any changes into
/// the current context plus any additional contexts.
///
/// - Parameters:
/// - batchDeleteRequest: The batch delete request to execute.
/// - batchInsertRequest: The batch insert request to execute.
/// - additionalContexts: Any additional contexts other than the current to merge the changes into.
///
func executeAndMergeChanges(
batchDeleteRequest: NSBatchDeleteRequest? = nil,
batchInsertRequest: NSBatchInsertRequest? = nil,
additionalContexts: [NSManagedObjectContext] = []
) throws {
var changes: [AnyHashable: Any] = [:]
if let batchDeleteRequest {
batchDeleteRequest.resultType = .resultTypeObjectIDs
if let deleteResult = try execute(batchDeleteRequest) as? NSBatchDeleteResult {
changes[NSDeletedObjectsKey] = deleteResult.result as? [NSManagedObjectID] ?? []
}
}
if let batchInsertRequest {
batchInsertRequest.resultType = .objectIDs
if let insertResult = try execute(batchInsertRequest) as? NSBatchInsertResult {
changes[NSInsertedObjectsKey] = insertResult.result as? [NSManagedObjectID] ?? []
}
}
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self] + additionalContexts)
}
/// Performs the closure on the context's queue and saves the context if there are any changes.
///
/// - Parameter closure: The closure to perform.
///
func performAndSave(closure: @escaping () throws -> Void) async throws {
try await perform {
try closure()
try self.saveIfChanged()
}
}
/// Saves the context if there are changes.
func saveIfChanged() throws {
guard hasChanges else { return }
try save()
}
}

View File

@ -1,24 +0,0 @@
import CoreData
import XCTest
@testable import AuthenticatorBridgeKit
class ManagedObjectTests: AuthenticatorBridgeKitTestCase {
// MARK: Tests
/// `fetchRequest()` returns a `NSFetchRequest` for the entity.
func test_fetchRequest() {
let fetchRequest = TestManagedObject.fetchRequest()
XCTAssertEqual(fetchRequest.entityName, "TestManagedObject")
}
/// `fetchResultRequest()` returns a `NSFetchRequest` for the entity.
func test_fetchResultRequest() {
let fetchRequest = TestManagedObject.fetchResultRequest()
XCTAssertEqual(fetchRequest.entityName, "TestManagedObject")
}
}
private class TestManagedObject: NSManagedObject, ManagedObject {
static var entityName = "TestManagedObject"
}

View File

@ -1,60 +0,0 @@
import Combine
import XCTest
@testable import AuthenticatorBridgeKit
class PublisherAsyncTests: AuthenticatorBridgeKitTestCase {
// MARK: Properties
var cancellable: AnyCancellable?
// MARK: Setup & Teardown
override func tearDown() {
super.tearDown()
cancellable = nil
}
// MARK: Tests
/// `asyncCompactMap(_:)` maps the output of a publisher, discarding any `nil` values.
func test_asyncCompactMap() {
var receivedValues = [Int]()
let expectation = expectation(description: #function)
let sequence = [1, 2, 3, 4, 5]
cancellable = sequence
.publisher
.asyncCompactMap { $0 % 2 == 0 ? $0 : nil }
.collect()
.sink { values in
receivedValues = values
expectation.fulfill()
}
waitForExpectations(timeout: 1)
XCTAssertEqual(receivedValues, [2, 4])
}
/// `asyncMap(_:)` maps the output of a publisher.
func test_asyncMap() {
var receivedValues = [Int]()
let expectation = expectation(description: #function)
let sequence = [1, 2, 3, 4, 5]
cancellable = sequence
.publisher
.asyncMap { $0 * 2 }
.collect()
.sink { values in
receivedValues = values
expectation.fulfill()
}
waitForExpectations(timeout: 1)
XCTAssertEqual(receivedValues, [2, 4, 6, 8, 10])
}
}

View File

@ -1,20 +0,0 @@
import OSLog
public extension Logger {
// MARK: Type Properties
/// Logger instance for the app's action extension.
static let appExtension = Logger(subsystem: subsystem, category: "AppExtension")
/// Logger instance for general application-level logs.
static let application = Logger(subsystem: subsystem, category: "Application")
/// Logger instance for use by processors in the application.
static let processor = Logger(subsystem: subsystem, category: "Processor")
// MARK: Private
/// The Logger subsystem passed along with logs to the logging system to identify logs from this
/// application.
private static var subsystem = Bundle.main.bundleIdentifier!
}

View File

@ -1,52 +0,0 @@
import CoreData
extension NSManagedObjectContext {
/// Executes the batch delete request and/or batch insert request and merges any changes into
/// the current context plus any additional contexts.
///
/// - Parameters:
/// - batchDeleteRequest: The batch delete request to execute.
/// - batchInsertRequest: The batch insert request to execute.
/// - additionalContexts: Any additional contexts other than the current to merge the changes into.
///
func executeAndMergeChanges(
batchDeleteRequest: NSBatchDeleteRequest? = nil,
batchInsertRequest: NSBatchInsertRequest? = nil,
additionalContexts: [NSManagedObjectContext] = []
) throws {
var changes: [AnyHashable: Any] = [:]
if let batchDeleteRequest {
batchDeleteRequest.resultType = .resultTypeObjectIDs
if let deleteResult = try execute(batchDeleteRequest) as? NSBatchDeleteResult {
changes[NSDeletedObjectsKey] = deleteResult.result as? [NSManagedObjectID] ?? []
}
}
if let batchInsertRequest {
batchInsertRequest.resultType = .objectIDs
if let insertResult = try execute(batchInsertRequest) as? NSBatchInsertResult {
changes[NSInsertedObjectsKey] = insertResult.result as? [NSManagedObjectID] ?? []
}
}
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self] + additionalContexts)
}
/// Performs the closure on the context's queue and saves the context if there are any changes.
///
/// - Parameter closure: The closure to perform.
///
func performAndSave(closure: @escaping () throws -> Void) async throws {
try await perform {
try closure()
try self.saveIfChanged()
}
}
/// Saves the context if there are changes.
func saveIfChanged() throws {
guard hasChanges else { return }
try save()
}
}

View File

@ -1,40 +0,0 @@
import CoreData
import OSLog
/// A protocol for a `NSManagedObject` which persists a data model as JSON encoded data. The model
/// can be set via the `model` property which encodes the model to the data property, which should
/// be a `@NSManaged` property of the `NSManagedObject`. When the managed object is populated from
/// the database, the `model` property can be read to decode the data.
///
protocol CodableModelData: AnyObject, NSManagedObject {
associatedtype Model: Codable
/// A `@NSManaged` property of the manage object for storing the encoded model as data.
var modelData: Data? { get set }
}
extension CodableModelData {
/// Encodes or decodes the model to/from the data instance.
var model: Model? {
get {
guard let modelData else { return nil }
do {
return try JSONDecoder().decode(Model.self, from: modelData)
} catch {
Logger.application.error("Error decoding \(String(describing: Model.self)): \(error)")
return nil
}
}
set {
guard let newValue else {
modelData = nil
return
}
do {
modelData = try JSONEncoder().encode(newValue)
} catch {
Logger.application.error("Error encoding \(String(describing: Model.self)): \(error)")
}
}
}
}

View File

@ -1,78 +0,0 @@
import CoreData
/// A protocol for an `NSManagedObject` data model that adds some convenience methods for working
/// with Core Data.
///
protocol ManagedObject: AnyObject {
/// The name of the entity of the managed object, as defined in the data model.
static var entityName: String { get }
}
extension ManagedObject where Self: NSManagedObject {
static var entityName: String {
String(describing: self)
}
/// Returns a `NSBatchInsertRequest` for batch inserting an array of objects.
///
/// - Parameters:
/// - objects: The objects (or objects that can be converted to managed objects) to insert.
/// - handler: A handler that is called for each object to set the properties on the
/// `NSManagedObject` to insert.
/// - Returns: A `NSBatchInsertRequest` for batch inserting an array of objects.
///
static func batchInsertRequest<T>(
objects: [T],
handler: @escaping (Self, T) throws -> Void
) throws -> NSBatchInsertRequest {
var index = 0
var errorToThrow: Error?
let insertRequest = NSBatchInsertRequest(entityName: entityName) { (managedObject: NSManagedObject) -> Bool in
guard index < objects.count else { return true }
defer { index += 1 }
if let managedObject = (managedObject as? Self) {
do {
try handler(managedObject, objects[index])
} catch {
// The error can't be thrown directly in this closure, so capture it, return
// from the closure, and then throw it.
errorToThrow = error
return true
}
}
return false
}
if let errorToThrow {
throw errorToThrow
}
return insertRequest
}
/// Returns a `NSFetchRequest` for fetching instances of the managed object.
///
/// - Parameter predicate: An optional predicate to apply to the fetch request.
/// - Returns: A `NSFetchRequest` used to fetch instances of the managed object.
///
static func fetchRequest(predicate: NSPredicate? = nil) -> NSFetchRequest<Self> {
let fetchRequest = NSFetchRequest<Self>(entityName: entityName)
fetchRequest.predicate = predicate
return fetchRequest
}
/// Returns a `NSFetchRequest` for fetching a generic `NSFetchRequestResult` instances of the
/// managed object.
///
/// - Parameter predicate: An optional predicate to apply to the fetch request.
/// - Returns: A `NSFetchRequest` used to fetch generic `NSFetchRequestResult` instances of the
/// managed object.
///
static func fetchResultRequest(predicate: NSPredicate? = nil) -> NSFetchRequest<NSFetchRequestResult> {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
fetchRequest.predicate = predicate
return fetchRequest
}
}

View File

@ -1,24 +0,0 @@
import CoreData
import XCTest
@testable import AuthenticatorShared
class ManagedObjectTests: BitwardenTestCase {
// MARK: Tests
/// `fetchRequest()` returns a `NSFetchRequest` for the entity.
func test_fetchRequest() {
let fetchRequest = TestManagedObject.fetchRequest()
XCTAssertEqual(fetchRequest.entityName, "TestManagedObject")
}
/// `fetchResultRequest()` returns a `NSFetchRequest` for the entity.
func test_fetchResultRequest() {
let fetchRequest = TestManagedObject.fetchResultRequest()
XCTAssertEqual(fetchRequest.entityName, "TestManagedObject")
}
}
private class TestManagedObject: NSManagedObject, ManagedObject {
static var entityName = "TestManagedObject"
}

View File

@ -1,145 +0,0 @@
import Combine
import CoreData
// MARK: - FetchedResultsPublisher
/// A Combine publisher that publishes the initial result set and any future data changes for a
/// Core Data fetch request.
///
/// Adapted from https://gist.github.com/darrarski/28d2f5a28ef2c5669d199069c30d3d52
///
class FetchedResultsPublisher<ResultType>: Publisher where ResultType: NSFetchRequestResult {
// MARK: Types
typealias Output = [ResultType]
typealias Failure = Error
// MARK: Properties
/// The managed object context that the fetch request is executed against.
let context: NSManagedObjectContext
/// The fetch request used to get the objects.
let request: NSFetchRequest<ResultType>
// MARK: Initialization
/// Initialize a `FetchedResultsPublisher`.
///
/// - Parameters:
/// - context: The managed object context that the fetch request is executed against.
/// - request: The fetch request used to get the objects.
///
init(context: NSManagedObjectContext, request: NSFetchRequest<ResultType>) {
self.context = context
self.request = request
}
// MARK: Publisher
func receive<S>(subscriber: S) where S: Subscriber, S.Failure == Failure, S.Input == Output {
subscriber.receive(subscription: FetchedResultsSubscription(
context: context,
request: request,
subscriber: subscriber
))
}
}
// MARK: - FetchedResultsSubscription
/// A `Subscription` to a `FetchedResultsPublisher` which fetches results from Core Data via a
/// `NSFetchedResultsController` and notifies the subscriber of any changes to the data.
///
private final class FetchedResultsSubscription<SubscriberType, ResultType>: NSObject, Subscription,
NSFetchedResultsControllerDelegate
where SubscriberType: Subscriber,
SubscriberType.Input == [ResultType],
SubscriberType.Failure == Error,
ResultType: NSFetchRequestResult {
// MARK: Properties
/// The fetched results controller to manage the results of a Core Data fetch request.
private var controller: NSFetchedResultsController<ResultType>?
/// The current demand from the subscriber.
private var demand: Subscribers.Demand = .none
/// Whether the subscription has changes to send to the subscriber.
private var hasChangesToSend = false
/// The subscriber to the subscription that is notified of the fetched results.
private var subscriber: SubscriberType?
// MARK: Initialization
/// Initialize a `FetchedResultsSubscription`.
///
/// - Parameters:
/// - context: The managed object context that the fetch request is executed against.
/// - request: The fetch request used to get the objects.
/// - subscriber: The subscriber to the subscription that is notified of the fetched results.
///
init(
context: NSManagedObjectContext,
request: NSFetchRequest<ResultType>,
subscriber: SubscriberType
) {
controller = NSFetchedResultsController(
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: nil,
cacheName: nil
)
self.subscriber = subscriber
super.init()
controller?.delegate = self
do {
try controller?.performFetch()
if controller?.fetchedObjects != nil {
hasChangesToSend = true
fulfillDemand()
}
} catch {
subscriber.receive(completion: .failure(error))
}
}
// MARK: Subscription
func request(_ demand: Subscribers.Demand) {
self.demand += demand
fulfillDemand()
}
// MARK: Cancellable
func cancel() {
controller = nil
subscriber = nil
}
// MARK: NSFetchedResultsControllerDelegate
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
hasChangesToSend = true
fulfillDemand()
}
// MARK: Private
private func fulfillDemand() {
guard demand > 0, hasChangesToSend,
let subscriber,
let fetchedObjects = controller?.fetchedObjects
else { return }
hasChangesToSend = false
demand -= 1
demand += subscriber.receive(fetchedObjects)
}
}

View File

@ -1,3 +1,4 @@
import BitwardenKit
import CoreData
import Foundation

View File

@ -1,3 +1,4 @@
import BitwardenKit
import Foundation
/// Data model for an encrypted item

View File

@ -1,3 +1,4 @@
import BitwardenKit
import Combine
import CoreData

View File

@ -1,6 +1,6 @@
import CoreData
extension NSManagedObjectContext {
public extension NSManagedObjectContext {
/// Executes the batch delete request and/or batch insert request and merges any changes into
/// the current context plus any additional contexts.
///

View File

@ -6,14 +6,14 @@ import OSLog
/// be a `@NSManaged` property of the `NSManagedObject`. When the managed object is populated from
/// the database, the `model` property can be read to decode the data.
///
protocol CodableModelData: AnyObject, NSManagedObject {
public protocol CodableModelData: AnyObject, NSManagedObject {
associatedtype Model: Codable
/// A `@NSManaged` property of the manage object for storing the encoded model as data.
var modelData: Data? { get set }
}
extension CodableModelData {
public extension CodableModelData {
/// Encodes or decodes the model to/from the data instance.
var model: Model? {
get {

View File

@ -3,12 +3,12 @@ import CoreData
/// A protocol for an `NSManagedObject` data model that adds some convenience methods for working
/// with Core Data.
///
protocol ManagedObject: AnyObject {
public protocol ManagedObject: AnyObject {
/// The name of the entity of the managed object, as defined in the data model.
static var entityName: String { get }
}
extension ManagedObject where Self: NSManagedObject {
public extension ManagedObject where Self: NSManagedObject {
static var entityName: String {
String(describing: self)
}

View File

@ -1,8 +1,7 @@
import BitwardenKit
import CoreData
import XCTest
@testable import BitwardenShared
class ManagedObjectTests: BitwardenTestCase {
// MARK: Tests

View File

@ -3,7 +3,7 @@ import CoreData
/// A protocol for a `ManagedObject` data model associated with a user that adds some convenience
/// methods for building `NSPersistentStoreRequest` for common CRUD operations.
///
protocol ManagedUserObject: ManagedObject {
public protocol ManagedUserObject: ManagedObject {
/// The value type (struct) associated with the managed object that is persisted in the database.
associatedtype ValueType
@ -28,7 +28,7 @@ protocol ManagedUserObject: ManagedObject {
func update(with value: ValueType, userId: String) throws
}
extension ManagedUserObject where Self: NSManagedObject {
public extension ManagedUserObject where Self: NSManagedObject {
/// A `NSBatchInsertRequest` that inserts objects for the specified user.
///
/// - Parameters:

View File

@ -8,12 +8,12 @@ import CoreData
///
/// Adapted from https://gist.github.com/darrarski/28d2f5a28ef2c5669d199069c30d3d52
///
class FetchedResultsPublisher<ResultType>: Publisher where ResultType: NSFetchRequestResult {
public class FetchedResultsPublisher<ResultType>: Publisher where ResultType: NSFetchRequestResult {
// MARK: Types
typealias Output = [ResultType]
public typealias Output = [ResultType]
typealias Failure = Error
public typealias Failure = Error
// MARK: Properties
@ -31,14 +31,14 @@ class FetchedResultsPublisher<ResultType>: Publisher where ResultType: NSFetchRe
/// - context: The managed object context that the fetch request is executed against.
/// - request: The fetch request used to get the objects.
///
init(context: NSManagedObjectContext, request: NSFetchRequest<ResultType>) {
public init(context: NSManagedObjectContext, request: NSFetchRequest<ResultType>) {
self.context = context
self.request = request
}
// MARK: Publisher
func receive<S>(subscriber: S) where S: Subscriber, S.Failure == Failure, S.Input == Output {
public func receive<S>(subscriber: S) where S: Subscriber, S.Failure == Failure, S.Input == Output {
subscriber.receive(subscription: FetchedResultsSubscription(
context: context,
request: request,

View File

@ -1,74 +0,0 @@
import CoreData
/// A protocol for a `ManagedObject` data model associated with a user that adds some convenience
/// methods for building `NSPersistentStoreRequest` for common CRUD operations.
///
protocol ManagedUserObject: ManagedObject {
/// The value type (struct) associated with the managed object that is persisted in the database.
associatedtype ValueType
/// Returns a `NSPredicate` used for filtering by a user's ID.
///
/// - Parameter userId: The user ID associated with the managed object.
///
static func userIdPredicate(userId: String) -> NSPredicate
/// Returns a `NSPredicate` used for filtering by a user and managed object ID.
///
/// - Parameter userId: The user ID associated with the managed object.
///
static func userIdAndIdPredicate(userId: String, id: String) -> NSPredicate
/// Updates the managed object from its associated value type object and user ID.
///
/// - Parameters:
/// - value: The value type object used to update the managed object.
/// - userId: The user ID associated with the object.
///
func update(with value: ValueType, userId: String) throws
}
extension ManagedUserObject where Self: NSManagedObject {
/// A `NSBatchInsertRequest` that inserts objects for the specified user.
///
/// - Parameters:
/// - objects: The list of objects to insert.
/// - userId: The user associated with the objects to insert.
/// - Returns: A `NSBatchInsertRequest` that inserts the objects for the user.
///
static func batchInsertRequest(objects: [ValueType], userId: String) throws -> NSBatchInsertRequest {
try batchInsertRequest(objects: objects) { object, value in
try object.update(with: value, userId: userId)
}
}
/// A `NSBatchDeleteRequest` that deletes all objects for the specified user.
///
/// - Parameter userId: The user associated with the objects to delete.
/// - Returns: A `NSBatchDeleteRequest` that deletes all objects for the user.
///
static func deleteByUserIdRequest(userId: String) -> NSBatchDeleteRequest {
let fetchRequest = fetchResultRequest(predicate: userIdPredicate(userId: userId))
return NSBatchDeleteRequest(fetchRequest: fetchRequest)
}
/// A `NSFetchRequest` that fetches objects for the specified user matching an ID.
///
/// - Parameters:
/// - id: The ID of the object to fetch.
/// - userId: The user associated with the object to fetch.
/// - Returns: A `NSFetchRequest` that fetches all objects for the user.
///
static func fetchByIdRequest(id: String, userId: String) -> NSFetchRequest<Self> {
fetchRequest(predicate: userIdAndIdPredicate(userId: userId, id: id))
}
/// A `NSFetchRequest` that fetches all objects for the specified user.
///
/// - Parameter userId: The user associated with the objects to delete.
/// - Returns: A `NSFetchRequest` that fetches all objects for the user.
///
static func fetchByUserIdRequest(userId: String) -> NSFetchRequest<Self> {
fetchRequest(predicate: userIdPredicate(userId: userId))
}
}

View File

@ -1,145 +0,0 @@
import Combine
import CoreData
// MARK: - FetchedResultsPublisher
/// A Combine publisher that publishes the initial result set and any future data changes for a
/// Core Data fetch request.
///
/// Adapted from https://gist.github.com/darrarski/28d2f5a28ef2c5669d199069c30d3d52
///
class FetchedResultsPublisher<ResultType>: Publisher where ResultType: NSFetchRequestResult {
// MARK: Types
typealias Output = [ResultType]
typealias Failure = Error
// MARK: Properties
/// The managed object context that the fetch request is executed against.
let context: NSManagedObjectContext
/// The fetch request used to get the objects.
let request: NSFetchRequest<ResultType>
// MARK: Initialization
/// Initialize a `FetchedResultsPublisher`.
///
/// - Parameters:
/// - context: The managed object context that the fetch request is executed against.
/// - request: The fetch request used to get the objects.
///
init(context: NSManagedObjectContext, request: NSFetchRequest<ResultType>) {
self.context = context
self.request = request
}
// MARK: Publisher
func receive<S>(subscriber: S) where S: Subscriber, S.Failure == Failure, S.Input == Output {
subscriber.receive(subscription: FetchedResultsSubscription(
context: context,
request: request,
subscriber: subscriber
))
}
}
// MARK: - FetchedResultsSubscription
/// A `Subscription` to a `FetchedResultsPublisher` which fetches results from Core Data via a
/// `NSFetchedResultsController` and notifies the subscriber of any changes to the data.
///
private final class FetchedResultsSubscription<SubscriberType, ResultType>: NSObject, Subscription,
NSFetchedResultsControllerDelegate
where SubscriberType: Subscriber,
SubscriberType.Input == [ResultType],
SubscriberType.Failure == Error,
ResultType: NSFetchRequestResult {
// MARK: Properties
/// The fetched results controller to manage the results of a Core Data fetch request.
private var controller: NSFetchedResultsController<ResultType>?
/// The current demand from the subscriber.
private var demand: Subscribers.Demand = .none
/// Whether the subscription has changes to send to the subscriber.
private var hasChangesToSend = false
/// The subscriber to the subscription that is notified of the fetched results.
private var subscriber: SubscriberType?
// MARK: Initialization
/// Initialize a `FetchedResultsSubscription`.
///
/// - Parameters:
/// - context: The managed object context that the fetch request is executed against.
/// - request: The fetch request used to get the objects.
/// - subscriber: The subscriber to the subscription that is notified of the fetched results.
///
init(
context: NSManagedObjectContext,
request: NSFetchRequest<ResultType>,
subscriber: SubscriberType
) {
controller = NSFetchedResultsController(
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: nil,
cacheName: nil
)
self.subscriber = subscriber
super.init()
controller?.delegate = self
do {
try controller?.performFetch()
if controller?.fetchedObjects != nil {
hasChangesToSend = true
fulfillDemand()
}
} catch {
subscriber.receive(completion: .failure(error))
}
}
// MARK: Subscription
func request(_ demand: Subscribers.Demand) {
self.demand += demand
fulfillDemand()
}
// MARK: Cancellable
func cancel() {
controller = nil
subscriber = nil
}
// MARK: NSFetchedResultsControllerDelegate
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
hasChangesToSend = true
fulfillDemand()
}
// MARK: Private
private func fulfillDemand() {
guard demand > 0, hasChangesToSend,
let subscriber,
let fetchedObjects = controller?.fetchedObjects
else { return }
hasChangesToSend = false
demand -= 1
demand += subscriber.receive(fetchedObjects)
}
}

View File

@ -1,5 +1,6 @@
// swiftlint:disable:this file_name
import BitwardenKit
import BitwardenSdk
// MARK: - Sends

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenSdk
import CoreData
import Foundation

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenSdk
import CoreData
import Foundation

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenSdk
import Combine
import CoreData

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenSdk
import Combine
import CoreData

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenSdk
import CoreData
import Foundation

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenSdk
import CoreData
import Foundation

View File

@ -1,3 +1,4 @@
import BitwardenKit
import CoreData
import Foundation

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenSdk
import CoreData
import Foundation

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenSdk
import CoreData
import Foundation

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenSdk
import CoreData
import Foundation

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenSdk
import Combine
import CoreData

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenSdk
import Combine
import CoreData

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenSdk
import Combine
import CoreData

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenSdk
import Combine
import CoreData