Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: create local stores and UTS - WPB-12100 #2141

Merged
merged 20 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
204fd96
Conversation - add UTs for local store and repository
jullianm Nov 6, 2024
07e8c01
User - add UTs for local store and repository
jullianm Nov 6, 2024
ee3927c
Connection - add UTs for local store and repository
jullianm Nov 7, 2024
3a3234b
Team - add UTs for local store and repository
jullianm Nov 7, 2024
8d72857
UserClients - add UTs for local store and repo
jullianm Nov 7, 2024
30796bf
FeatureConfig - add UTS for local store and repo
jullianm Nov 7, 2024
c47a3e2
UpdateEvents - add UTs for local store and repository
jullianm Nov 7, 2024
6e80d78
add missing documentation for FeatureConfigLocalStore
jullianm Nov 7, 2024
0a90373
ConversationLabels - add UTs for local store and repository
jullianm Nov 8, 2024
a185f03
add TeamModelMappings to avoid passing API object to storage layer
jullianm Nov 8, 2024
205b4d2
Merge branch 'develop' into refactor/create-local-stores-and-UTs
jullianm Nov 8, 2024
8aebf87
move message local store file to dedicated folder
jullianm Nov 8, 2024
aa8da54
remove dead code, clean up import, add domain models to avoid API obj…
jullianm Nov 8, 2024
36e1e98
created dedicated file for UserClientInfo domain model
jullianm Nov 8, 2024
3d9ee9d
create UserClientsModelMappings to map API model to domain model
jullianm Nov 8, 2024
ff83af9
rename Error to Failure, add Sendable, clean up code - WPB-12100 (#2141)
jullianm Nov 27, 2024
9bfcf4d
Merge develop
jullianm Nov 27, 2024
0994f68
Merge branch 'develop' into refactor/create-local-stores-and-UTs
jullianm Nov 27, 2024
8c79698
fix remaining merge conflict
jullianm Nov 27, 2024
59991d3
lint and format - WPB-12100 (#2141)
jullianm Nov 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 157 additions & 15 deletions WireDomain/Project/WireDomain Project.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ struct UserClientAddEventProcessor: UserClientAddEventProcessorProtocol {
func processEvent(_ event: UserClientAddEvent) async throws {
do {
let localUserClient = try await repository.fetchOrCreateClient(
with: event.client.id
id: event.client.id
)

try await repository.updateClient(
with: event.client.id,
id: event.client.id,
from: event.client,
isNewClient: localUserClient.isNew
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import CoreData
import Foundation
import WireAPI
import WireDataModel

protocol ConnectionsLocalStoreProtocol {
// sourcery: AutoMockable
public protocol ConnectionsLocalStoreProtocol {

/// Save connection and related objects to local storage.
/// - Parameter connectionInfo: connection object

func storeConnection(
_ connectionPayload: Connection
_ connectionInfo: ConnectionInfo
jullianm marked this conversation as resolved.
Show resolved Hide resolved
) async throws
}

Expand All @@ -44,15 +45,12 @@ final class ConnectionsLocalStore: ConnectionsLocalStoreProtocol {

// MARK: - Public

/// Save connection and related objects to local storage.
/// - Parameter connectionPayload: connection object from WireAPI

public func storeConnection(_ connectionPayload: Connection) async throws {
public func storeConnection(_ connectionInfo: ConnectionInfo) async throws {
try await context.perform { [self] in

let connection = try storedConnection(from: connectionPayload)
let connection = try storedConnection(from: connectionInfo)

let conversation = try storedConversation(from: connectionPayload, with: connection)
let conversation = try storedConversation(from: connectionInfo, with: connection)

connection.to.oneOnOneConversation = conversation

Expand All @@ -66,7 +64,10 @@ final class ConnectionsLocalStore: ConnectionsLocalStoreProtocol {
/// - storedConnection: ZMConnection object stored locally
/// - Returns: conversation object stored locally

private func storedConversation(from connection: Connection, with storedConnection: ZMConnection) throws -> ZMConversation {
private func storedConversation(
from connection: ConnectionInfo,
with storedConnection: ZMConnection
) throws -> ZMConversation {
guard let conversationID = connection.conversationID ?? connection.qualifiedConversationID?.uuid else {
throw ConnectionsRepositoryError.missingConversationId
}
Expand All @@ -87,7 +88,9 @@ final class ConnectionsLocalStore: ConnectionsLocalStoreProtocol {
/// - Parameter connection: connection payload from WireAPI
/// - Returns: connection object stored locally

private func storedConnection(from connection: Connection) throws -> ZMConnection {
private func storedConnection(
from connection: ConnectionInfo
) throws -> ZMConnection {
guard let userID = connection.receiverID ?? connection.receiverQualifiedID?.uuid else {
throw ConnectionsRepositoryError.missingReceiverId
}
Expand All @@ -98,7 +101,7 @@ final class ConnectionsLocalStore: ConnectionsLocalStoreProtocol {
in: context
)

storedConnection.status = connection.status.toDomainModel()
storedConnection.status = connection.status
storedConnection.lastUpdateDateInGMT = connection.lastUpdate
return storedConnection
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,18 @@ extension WireAPI.ConnectionStatus {
}

}

extension WireAPI.Connection {

func toDomainModel() -> ConnectionInfo {
.init(
senderID: senderID,
receiverID: receiverID,
receiverQualifiedID: receiverQualifiedID?.toDomainModel(),
conversationID: conversationID,
qualifiedConversationID: qualifiedConversationID?.toDomainModel(),
lastUpdate: lastUpdate,
status: status.toDomainModel()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public struct ConnectionsRepository: ConnectionsRepositoryProtocol {
await withThrowingTaskGroup(of: Void.self) { taskGroup in
for connection in connections {
taskGroup.addTask {
try await connectionsLocalStore.storeConnection(connection)
try await connectionsLocalStore.storeConnection(connection.toDomainModel())
jullianm marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand All @@ -80,7 +80,7 @@ public struct ConnectionsRepository: ConnectionsRepositoryProtocol {
public func updateConnection(
_ connection: Connection
) async throws {
try await connectionsLocalStore.storeConnection(connection)
try await connectionsLocalStore.storeConnection(connection.toDomainModel())
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// Wire
// Copyright (C) 2024 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import WireDataModel

public struct ConnectionInfo: Sendable {
public let senderID: UUID?
public let receiverID: UUID?
public let receiverQualifiedID: WireDataModel.QualifiedID?
public let conversationID: UUID?
public let qualifiedConversationID: WireDataModel.QualifiedID?
public let lastUpdate: Date
public let status: WireDataModel.ZMConnectionStatus
jullianm marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import CoreData
import WireAPI
jullianm marked this conversation as resolved.
Show resolved Hide resolved
import WireDataModel

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//
// Wire
// Copyright (C) 2024 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import WireDataModel

// sourcery: AutoMockable
public protocol ConversationLabelsLocalStoreProtocol {

/// Save label and related conversations objects to local storage.
/// - Parameter conversationLabel: conversation label from WireAPI

func storeLabel(
_ conversationLabel: ConversationLabelInfo
) async throws

/// Delete old `folder` labels and related conversations objects from local storage.
/// - Parameter excludedLabels: remote labels that should be excluded from deletion.
/// - Only old labels of type `folder` are deleted, `favorite` labels always remain in the local storage.

func deleteOldLabelsLocally(
excludedLabels: [ConversationLabelInfo]
) async throws
}

public final class ConversationLabelsLocalStore: ConversationLabelsLocalStoreProtocol {
jullianm marked this conversation as resolved.
Show resolved Hide resolved

// MARK: - Error

enum Error: Swift.Error {
case failedToStoreLabelLocally(UUID)
}
jullianm marked this conversation as resolved.
Show resolved Hide resolved

// MARK: - Properties

private let context: NSManagedObjectContext
private let logger = WireLogger(tag: "conversation-labels")

// MARK: - Object lifecycle

init(
context: NSManagedObjectContext
) {
self.context = context
}

// MARK: - Public

/// Save label and related conversations objects to local storage.
/// - Parameter conversationLabel: conversation label from WireAPI

public func storeLabel(
_ conversationLabel: ConversationLabelInfo
) async throws {
try await context.perform { [context] in
var created = false
let label: Label? = if conversationLabel.type == Label.Kind.favorite.rawValue {
Label.fetchFavoriteLabel(in: context)
} else {
Label.fetchOrCreate(remoteIdentifier: conversationLabel.id, create: true, in: context, created: &created)
}

guard let label else {
throw Error.failedToStoreLabelLocally(conversationLabel.id)
}

label.name = conversationLabel.name
label.kind = Label.Kind(rawValue: conversationLabel.type) ?? .folder

let conversations = ZMConversation.fetchObjects(
withRemoteIdentifiers: Set(conversationLabel.conversationIDs),
in: context
) as? Set<ZMConversation> ?? Set()

label.conversations = conversations
label.modifiedKeys = nil

do {
try context.save()
} catch {
throw Error.failedToStoreLabelLocally(conversationLabel.id)
}
}
}

public func deleteOldLabelsLocally(
excludedLabels: [ConversationLabelInfo]
) async throws {
try await context.perform { [self] in
let uuids = excludedLabels.map { $0.id.uuidData as NSData }
let predicateFormat = "type == \(Label.Kind.folder.rawValue) AND NOT remoteIdentifier_data IN %@"

let predicate = NSPredicate(
format: predicateFormat,
uuids as CVarArg
)

let fetchRequest: NSFetchRequest<NSFetchRequestResult>
fetchRequest = NSFetchRequest(entityName: Label.entityName())
fetchRequest.predicate = predicate

/// Since batch operations bypass the context processing,
/// relationships rules are often ignored (e.g delete rule)
/// Nevertheless, CoreData automatically handles two specific scenarios:
/// `Cascade` delete rule and `Nullify` delete rule on an optional property
/// Since `conversations` is nullify and optional, we can safely perform a batch delete.

let deleteRequest = NSBatchDeleteRequest(
fetchRequest: fetchRequest
)

deleteRequest.resultType = .resultTypeObjectIDs

do {
let batchDelete = try context.execute(deleteRequest) as? NSBatchDeleteResult

guard let deleteResult = batchDelete?.result as? [NSManagedObjectID] else {
throw ConversationLabelsRepositoryError.failedToDeleteStoredLabels
}

let deletedObjects: [AnyHashable: Any] = [
NSDeletedObjectsKey: deleteResult
]

/// Since `NSBatchDeleteRequest` only operates at the SQL level (in the persistent store itself),
/// we need to manually update our in-memory objects after execution.

NSManagedObjectContext.mergeChanges(
fromRemoteContextSave: deletedObjects,
into: [context]
)

} catch {
logger.error("Failed to delete old labels: \(error)")
throw error
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// Wire
// Copyright (C) 2024 Wire Swiss GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import WireAPI

extension WireAPI.ConversationLabel {

func toDomainModel() -> ConversationLabelInfo {
.init(
id: id,
name: name,
type: type,
conversationIDs: conversationIDs
)
}

}
Loading
Loading