Improvements to handle user search and data access retrieval

sync2
Laurent 11 months ago
parent 5b86728d77
commit bd7ec4dcc9
  1. 8
      LeStorage.xcodeproj/project.pbxproj
  2. 2
      LeStorage/ApiCallCollection.swift
  3. 16
      LeStorage/Codables/ApiCall.swift
  4. 108
      LeStorage/Services.swift
  5. 33
      LeStorage/StoreCenter.swift
  6. 26
      LeStorage/Utils/Dictionary+Extensions.swift
  7. 22
      LeStorage/WebSocketManager.swift

@ -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 = "<group>"; };
C425D4382B6D24E1002A7B48 /* LeStorage.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = LeStorage.docc; sourceTree = "<group>"; };
C425D4572B6D2519002A7B48 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
C4339BFE2CFF86B3004E5F09 /* Dictionary+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+Extensions.swift"; sourceTree = "<group>"; };
C456EFE12BE52379007388E2 /* StoredSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredSingleton.swift; sourceTree = "<group>"; };
C45D35902C0A1DB5000F379F /* FailedAPICall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedAPICall.swift; sourceTree = "<group>"; };
C467AAE22CD2466400D76CD2 /* Formatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Formatter.swift; sourceTree = "<group>"; };
@ -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 */,

@ -207,7 +207,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
}
} catch {
// Logger.log("\(T.resourceName()) > API CALL RETRY ERROR:")
Logger.error(error)
// Logger.error(error)
}
}

@ -73,19 +73,3 @@ class ApiCall<T: Storable>: 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: "&")
}
}

@ -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<T: Encodable, U: Decodable>(serviceCall: ServiceCall, payload: T)
fileprivate func _runRequest<U: Decodable>(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<T: Encodable, U: Decodable>(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<T: SyncedStorable>(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<T: SyncedStorable>(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<T: SyncedStorable>(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<T: Encodable>: 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
}

@ -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<T: SyncedStorable>(_ 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)
}
}

@ -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: "&")
}
}

@ -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()
}
}

Loading…
Cancel
Save