From bd7ec4dcc9a3098d94d644bbbaa06aa89137d391 Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 3 Dec 2024 20:35:47 +0100 Subject: [PATCH] Improvements to handle user search and data access retrieval --- LeStorage.xcodeproj/project.pbxproj | 8 +- LeStorage/ApiCallCollection.swift | 2 +- LeStorage/Codables/ApiCall.swift | 16 --- LeStorage/Services.swift | 108 +++++++++----------- LeStorage/StoreCenter.swift | 33 +++++- LeStorage/Utils/Dictionary+Extensions.swift | 26 +++++ LeStorage/WebSocketManager.swift | 22 ++-- 7 files changed, 126 insertions(+), 89 deletions(-) create mode 100644 LeStorage/Utils/Dictionary+Extensions.swift diff --git a/LeStorage.xcodeproj/project.pbxproj b/LeStorage.xcodeproj/project.pbxproj index 00fb0eb..96cafc7 100644 --- a/LeStorage.xcodeproj/project.pbxproj +++ b/LeStorage.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ C425D4392B6D24E1002A7B48 /* LeStorage.docc in Sources */ = {isa = PBXBuildFile; fileRef = C425D4382B6D24E1002A7B48 /* LeStorage.docc */; }; C425D4452B6D24E1002A7B48 /* LeStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = C425D4372B6D24E1002A7B48 /* LeStorage.h */; settings = {ATTRIBUTES = (Public, ); }; }; C425D4582B6D2519002A7B48 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = C425D4572B6D2519002A7B48 /* Store.swift */; }; + C4339BFF2CFF86B3004E5F09 /* Dictionary+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4339BFE2CFF86B3004E5F09 /* Dictionary+Extensions.swift */; }; C456EFE22BE52379007388E2 /* StoredSingleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C456EFE12BE52379007388E2 /* StoredSingleton.swift */; }; C45D35912C0A1DB5000F379F /* FailedAPICall.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D35902C0A1DB5000F379F /* FailedAPICall.swift */; }; C467AAE32CD2467500D76CD2 /* Formatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C467AAE22CD2466400D76CD2 /* Formatter.swift */; }; @@ -62,6 +63,7 @@ C425D4372B6D24E1002A7B48 /* LeStorage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LeStorage.h; sourceTree = ""; }; C425D4382B6D24E1002A7B48 /* LeStorage.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = LeStorage.docc; sourceTree = ""; }; C425D4572B6D2519002A7B48 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; + C4339BFE2CFF86B3004E5F09 /* Dictionary+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+Extensions.swift"; sourceTree = ""; }; C456EFE12BE52379007388E2 /* StoredSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredSingleton.swift; sourceTree = ""; }; C45D35902C0A1DB5000F379F /* FailedAPICall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedAPICall.swift; sourceTree = ""; }; C467AAE22CD2466400D76CD2 /* Formatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Formatter.swift; sourceTree = ""; }; @@ -148,15 +150,15 @@ C49B6E4F2C2089B6002BDE1B /* ApiCallCollection.swift */, C4A47D6C2B71364600ADC637 /* ModelObject.swift */, C488C87F2CCBDC210082001F /* NetworkMonitor.swift */, + C4AC9CE92CF754CC00CC13DF /* Relationship.swift */, C4A47D602B6D3C1300ADC637 /* Services.swift */, C425D4572B6D2519002A7B48 /* Store.swift */, C4FC2E282C2B2EC30021F3BF /* StoreCenter.swift */, C4A47D642B6E92FE00ADC637 /* Storable.swift */, - C4AC9CE92CF754CC00CC13DF /* Relationship.swift */, - C4D4779E2CB92FD80077713D /* SyncedStorable.swift */, C4A47D4E2B6D280200ADC637 /* StoredCollection.swift */, C4D477A02CB9586A0077713D /* StoredCollection+Sync.swift */, C456EFE12BE52379007388E2 /* StoredSingleton.swift */, + C4D4779E2CB92FD80077713D /* SyncedStorable.swift */, C4FAE6992CEB84B300790446 /* WebSocketManager.swift */, C4A47D932B7CF7C500ADC637 /* MicroStorage.swift */, C4A47D822B7665BC00ADC637 /* Wip */, @@ -171,6 +173,7 @@ C4A47D502B6D2C4E00ADC637 /* Codable+Extensions.swift */, C4A47D6A2B71244100ADC637 /* Collection+Extension.swift */, C4D477962CB66EEA0077713D /* Date+Extensions.swift */, + C4339BFE2CFF86B3004E5F09 /* Dictionary+Extensions.swift */, C4A47DAE2B85FD3800ADC637 /* Errors.swift */, C49EF0232BD6BDC50077B5AA /* FileManager+Extensions.swift */, C4A47D542B6D2DBF00ADC637 /* FileUtils.swift */, @@ -337,6 +340,7 @@ C456EFE22BE52379007388E2 /* StoredSingleton.swift in Sources */, C467AAE32CD2467500D76CD2 /* Formatter.swift in Sources */, C4A47D652B6E92FE00ADC637 /* Storable.swift in Sources */, + C4339BFF2CFF86B3004E5F09 /* Dictionary+Extensions.swift in Sources */, C4D477972CB66EEA0077713D /* Date+Extensions.swift in Sources */, C488C8802CCBDC210082001F /* NetworkMonitor.swift in Sources */, C4A47D6D2B71364600ADC637 /* ModelObject.swift in Sources */, diff --git a/LeStorage/ApiCallCollection.swift b/LeStorage/ApiCallCollection.swift index 8a3b6de..989e41b 100644 --- a/LeStorage/ApiCallCollection.swift +++ b/LeStorage/ApiCallCollection.swift @@ -207,7 +207,7 @@ actor ApiCallCollection: SomeCallCollection { } } catch { // Logger.log("\(T.resourceName()) > API CALL RETRY ERROR:") - Logger.error(error) +// Logger.error(error) } } diff --git a/LeStorage/Codables/ApiCall.swift b/LeStorage/Codables/ApiCall.swift index bca0518..d6c29a8 100644 --- a/LeStorage/Codables/ApiCall.swift +++ b/LeStorage/Codables/ApiCall.swift @@ -73,19 +73,3 @@ class ApiCall: ModelObject, Storable, SomeCall { static func relationships() -> [Relationship] { return [] } } - -fileprivate extension Dictionary where Key == String, Value == String { - func toQueryString() -> String? { - guard !self.isEmpty else { - return nil - } - - let pairs = self.map { key, value in - let escapedKey = key.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? key - let escapedValue = value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? value - return "\(escapedKey)=\(escapedValue)" - } - - return "?" + pairs.joined(separator: "&") - } -} diff --git a/LeStorage/Services.swift b/LeStorage/Services.swift index 55df8fb..8666841 100644 --- a/LeStorage/Services.swift +++ b/LeStorage/Services.swift @@ -32,6 +32,10 @@ let changePasswordCall: ServiceCall = ServiceCall( path: "change-password/", method: .put, requiresToken: true) let postDeviceTokenCall: ServiceCall = ServiceCall( path: "device-token/", method: .post, requiresToken: true) +let getUserDataAccessCall: ServiceCall = ServiceCall( + path: "user-data-access/", method: .get, requiresToken: true) +let userSearchCall: ServiceCall = ServiceCall( + path: "users-search/", method: .get, requiresToken: true) /// A class used to send HTTP request to the django server public class Services { @@ -49,31 +53,27 @@ public class Services { /// The base API URL to send requests fileprivate(set) var baseURL: String -// fileprivate var jsonEncoder: JSONEncoder = { -// let encoder = JSONEncoder() -// encoder.keyEncodingStrategy = .convertToSnakeCase -// encoder.outputFormatting = .prettyPrinted -// encoder.dateEncodingStrategy = .iso8601 -// return encoder -// }() -// -// fileprivate var jsonDecoder: JSONDecoder = { -// let decoder = JSONDecoder() -// decoder.keyDecodingStrategy = .convertFromSnakeCase -// decoder.dateDecodingStrategy = .iso8601 -// return decoder -// }() // MARK: - Base /// Runs a request using a configuration object /// - Parameters: /// - serviceConf: A instance of ServiceConf - /// - payload: a codable value stored in the body of the request /// - apiCallId: an optional id referencing an ApiCall - fileprivate func _runRequest(serviceCall: ServiceCall, payload: T) + fileprivate func _runRequest(serviceCall: ServiceCall) async throws -> U { + let request = try self._baseRequest(call: serviceCall) + return try await _runRequest(request) + } + + /// Runs a request using a configuration object + /// - Parameters: + /// - serviceConf: A instance of ServiceConf + /// - payload: a codable value stored in the body of the request + /// - apiCallId: an optional id referencing an ApiCall + fileprivate func _runRequest(serviceCall: ServiceCall, payload: T) + async throws -> U { var request = try self._baseRequest(call: serviceCall) request.httpBody = try JSON.encoder.encode(payload) return try await _runRequest(request) @@ -197,41 +197,12 @@ public class Services { identifier: identifier) } - /// Returns a POST request for the resource - /// - Parameters: - /// - type: the type of the request resource -// fileprivate func _postRequest(type: T.Type) throws -> URLRequest { -// let requiresToken = self._isTokenRequired(type: T.self, method: .post) -// return try self._baseRequest( -// servicePath: T.path(), method: .post, requiresToken: requiresToken) -// } -// -// /// Returns a PUT request for the resource -// /// - Parameters: -// /// - type: the type of the request resource -// fileprivate func _putRequest(type: T.Type, id: String) throws -> URLRequest { -// let requiresToken = self._isTokenRequired(type: T.self, method: .put) -// return try self._baseRequest( -// servicePath: T.path(id: id), method: .put, requiresToken: requiresToken) -// } -// -// /// Returns a DELETE request for the resource -// /// - Parameters: -// /// - type: the type of the request resource -// fileprivate func _deleteRequest(type: T.Type, id: String) throws -// -> URLRequest -// { -// let requiresToken = self._isTokenRequired(type: T.self, method: .delete) -// return try self._baseRequest( -// servicePath: T.path(id: id), method: .delete, requiresToken: requiresToken) -// } - /// Returns the base URLRequest for a ServiceConf instance /// - Parameters: /// - conf: a ServiceConf instance - fileprivate func _baseRequest(call: ServiceCall) throws -> URLRequest { + fileprivate func _baseRequest(call: ServiceCall, getArguments: [String: String]? = nil) throws -> URLRequest { return try self._baseRequest( - servicePath: call.path, method: call.method, requiresToken: call.requiresToken) + servicePath: call.path, method: call.method, requiresToken: call.requiresToken, getArguments: getArguments) } /// Returns a base request for a path and method @@ -242,13 +213,18 @@ public class Services { /// - identifier: an optional StoreIdentifier that allows to filter GET requests with the StoreIdentifier values fileprivate func _baseRequest( servicePath: String, method: HTTPMethod, requiresToken: Bool? = nil, - identifier: String? = nil + identifier: String? = nil, getArguments: [String: String]? = nil ) throws -> URLRequest { var urlString = baseURL + servicePath + var arguments: [String:String] = getArguments ?? [:] if let identifier { - let component = "?store_id=\(identifier)" - urlString.append(component) + arguments["store_id"] = identifier +// let component = "?store_id=\(identifier)" +// urlString.append(component) } + + urlString.append(arguments.toQueryString()) + guard let url = URL(string: urlString) else { throw ServiceError.urlCreationError(url: urlString) } @@ -338,7 +314,9 @@ public class Services { /// - since: The date from which updates are retrieved func synchronizeLastUpdates(since: Date?) async throws { let request = try self._getSyncLogRequest(since: since) - try await self._runGetSyncLogRequest(request) + try await self._runRequest(request) { data in + StoreCenter.main.synchronizeContent(data) + } } /// Returns the URLRequest for an ApiCall @@ -370,7 +348,7 @@ public class Services { /// Runs the a sync request and forwards the response to the StoreCenter for processing /// - Parameters: /// - request: The synchronization request - fileprivate func _runGetSyncLogRequest(_ request: URLRequest) async throws { + fileprivate func _runRequest(_ request: URLRequest, _ success: (Data) -> ()) async throws { let debugURL = request.url?.absoluteString ?? "" // print("Run \(request.httpMethod ?? "") \(debugURL)") @@ -382,7 +360,7 @@ public class Services { print("\(debugURL) ended, status code = \(statusCode)") switch statusCode { case 200..<300: // success - StoreCenter.main.synchronizeContent(task.0) + success(task.0) default: // error Logger.log( "Failed Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") @@ -482,6 +460,13 @@ public class Services { throw ServiceError.urlCreationError(url: stringURL) } } + + // MARK: - Others + + public func searchUsers(string: String) async throws -> [ShortUser] { + let baseRequest = try self._baseRequest(call: userSearchCall, getArguments: ["search": string]) + return try await self._runRequest(baseRequest) + } // MARK: - Authentication @@ -553,6 +538,13 @@ public class Services { // Logger.log("Send device token = \(tokenString)") let _: Empty = try await self._runRequest(serviceCall: postDeviceTokenCall, payload: token) } + + public func getUserDataAccess() async throws { + let request = try self._baseRequest(call: getUserDataAccessCall) + try await self._runRequest(request) { data in + StoreCenter.main.userDataAccessRetrieved(data) + } + } /// A method that sends a request to change a user's password /// - Parameters: @@ -652,11 +644,6 @@ public class Services { } -//struct GetSyncLog: Codable { -// var updates: [String: Codable] -// var deletions: [String: Codable] -//} - struct SyncPayload: Encodable { var operation: String var modelName: String @@ -708,3 +695,8 @@ public protocol UserBase: Codable { public protocol UserPasswordBase: UserBase { var password: String { get } } +public struct ShortUser: Codable, Identifiable, Equatable { + public var id: String + public var firstName: String + public var lastName: String +} diff --git a/LeStorage/StoreCenter.swift b/LeStorage/StoreCenter.swift index 14e67b6..f89a8e8 100644 --- a/LeStorage/StoreCenter.swift +++ b/LeStorage/StoreCenter.swift @@ -64,6 +64,7 @@ public class StoreCenter { NetworkMonitor.shared.onConnectionEstablished = { self._resumeApiCalls() + self._configureWebSocket() } } @@ -393,6 +394,16 @@ public class StoreCenter { settings.lastSynchronization = Date() } Store.main.loadCollectionsFromServer() + + // request data that has been shared with the user + Task { + do { + try await self.service().getUserDataAccess() + } catch { + Logger.error(error) + } + } + } func synchronizeLastUpdates() async throws { @@ -415,6 +426,24 @@ public class StoreCenter { // try await self._services?.synchronizeLastUpdates(since: lastSync) } + func userDataAccessRetrieved(_ data: Data) { + do { + guard + let json = try JSONSerialization.jsonObject(with: data, options: []) + as? [String: Any] + else { + Logger.w("data unrecognized") + return + } + + try self._parseSyncUpdates(json, shared: true) + + } + catch { + Logger.error(error) + } + } + func synchronizeContent(_ data: Data) { do { @@ -572,7 +601,7 @@ public class StoreCenter { func synchronizationAddOrUpdate(_ instance: T, storeId: String?, shared: Bool) { let hasAlreadyBeenDeleted: Bool = self._hasAlreadyBeenDeleted(instance) if !hasAlreadyBeenDeleted { - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + DispatchQueue.main.async { self._store(id: storeId).addOrUpdateIfNewer(instance, shared: shared) } } @@ -809,7 +838,7 @@ public class StoreCenter { dataAccess.sharedWith.append(user) dataAccessCollection.addOrUpdate(instance: dataAccess) } else { - let dataAccess = DataAccess(owner: userId, sharedWith: [user], modelName: T.resourceName(), modelId: data.stringId) + let dataAccess = DataAccess(owner: userId, sharedWith: [user], modelName: String(describing: type(of: data)), modelId: data.stringId) dataAccessCollection.addOrUpdate(instance: dataAccess) } } diff --git a/LeStorage/Utils/Dictionary+Extensions.swift b/LeStorage/Utils/Dictionary+Extensions.swift new file mode 100644 index 0000000..f9a0a1d --- /dev/null +++ b/LeStorage/Utils/Dictionary+Extensions.swift @@ -0,0 +1,26 @@ +// +// Dictionary+Extensions.swift +// LeStorage +// +// Created by Laurent Morvillier on 03/12/2024. +// + +import Foundation + +extension Dictionary where Key == String, Value == String { + + func toQueryString() -> String { + guard !self.isEmpty else { + return "" + } + + let pairs = self.map { key, value in + let escapedKey = key.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? key + let escapedValue = value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? value + return "\(escapedKey)=\(escapedValue)" + } + + return "?" + pairs.joined(separator: "&") + } + +} diff --git a/LeStorage/WebSocketManager.swift b/LeStorage/WebSocketManager.swift index 1bd39ab..4b360f0 100644 --- a/LeStorage/WebSocketManager.swift +++ b/LeStorage/WebSocketManager.swift @@ -50,14 +50,14 @@ class WebSocketManager: ObservableObject { _webSocketTask?.receive { result in switch result { case .failure(let error): - print("Error in receiving message: \(error)") +// print("Error in receiving message: \(error)") self._handleWebSocketError(error) // self._setupWebSocket() case .success(let message): switch message { - case .string(let text): - print("Received text: \(text)") + case .string: +// print("Received text: \(text)") Task { do { try await StoreCenter.main.synchronizeLastUpdates() @@ -66,13 +66,15 @@ class WebSocketManager: ObservableObject { } } - DispatchQueue.main.async { +// DispatchQueue.main.async { // self.messages.append(text) - } - case .data(let data): - print("Received binary message: \(data)") +// } + case .data: + break +// print("Received binary message: \(data)") @unknown default: - print("received other = \(message)") + break +// print("received other = \(message)") } self._receiveMessage() @@ -81,7 +83,7 @@ class WebSocketManager: ObservableObject { } private func _handleWebSocketError(_ error: Error) { - print("WebSocket error: \(error)") +// print("WebSocket error: \(error)") // Exponential backoff for reconnection let delay = min(Double(self._reconnectAttempts), 10.0) @@ -89,7 +91,7 @@ class WebSocketManager: ObservableObject { DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in guard let self = self else { return } - print("Attempting to reconnect... (Attempt #\(self._reconnectAttempts))") + Logger.log("Attempting to reconnect... (Attempt #\(self._reconnectAttempts))") _setupWebSocket() } }