mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-10 17:46:07 -06:00
[PM-21602] refactor: Consolidate CoreData helpers in BitwardenKit (#1579)
This commit is contained in:
parent
6c0891a0d0
commit
8aabc70a87
@ -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
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
|
||||
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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])
|
||||
}
|
||||
}
|
||||
@ -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!
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import Foundation
|
||||
|
||||
/// Data model for an encrypted item
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import Combine
|
||||
import CoreData
|
||||
|
||||
|
||||
@ -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.
|
||||
///
|
||||
@ -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 {
|
||||
@ -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)
|
||||
}
|
||||
@ -1,8 +1,7 @@
|
||||
import BitwardenKit
|
||||
import CoreData
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
|
||||
class ManagedObjectTests: BitwardenTestCase {
|
||||
// MARK: Tests
|
||||
|
||||
@ -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:
|
||||
@ -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,
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
// swiftlint:disable:this file_name
|
||||
|
||||
import BitwardenKit
|
||||
import BitwardenSdk
|
||||
|
||||
// MARK: - Sends
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import BitwardenSdk
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import BitwardenSdk
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import BitwardenSdk
|
||||
import Combine
|
||||
import CoreData
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import BitwardenSdk
|
||||
import Combine
|
||||
import CoreData
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import BitwardenSdk
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import BitwardenSdk
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import BitwardenSdk
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import BitwardenSdk
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import BitwardenSdk
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import BitwardenSdk
|
||||
import Combine
|
||||
import CoreData
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import BitwardenSdk
|
||||
import Combine
|
||||
import CoreData
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import BitwardenSdk
|
||||
import Combine
|
||||
import CoreData
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import BitwardenKit
|
||||
import BitwardenSdk
|
||||
import Combine
|
||||
import CoreData
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user