diff --git a/LeStorage/ApiCallCollection.swift b/LeStorage/ApiCallCollection.swift index 3026328..8e22ec0 100644 --- a/LeStorage/ApiCallCollection.swift +++ b/LeStorage/ApiCallCollection.swift @@ -75,9 +75,13 @@ actor ApiCallCollection: SomeCallCollection { if FileManager.default.fileExists(atPath: fileURL.path()) { let jsonString: String = try FileUtils.readFile(fileURL: fileURL) - let decoded: [ApiCall] = try jsonString.decodeArray() ?? [] - // Logger.log("loaded \(fileURL.lastPathComponent) with \(decoded.count) items") - self.items = decoded + do { + let decoded: [ApiCall] = try jsonString.decodeArray() ?? [] + self.items = decoded + } catch { + let decoded: [OldApiCall] = try jsonString.decodeArray() ?? [] + self.items = decoded.compactMap { $0.toNewApiCall() } + } } } @@ -115,19 +119,12 @@ actor ApiCallCollection: SomeCallCollection { /// Deletes a call by a data id func deleteByDataId(_ dataId: String) { - if let apiCallIndex = self.items.firstIndex(where: { $0.dataId == dataId }) { + if let apiCallIndex = self.items.firstIndex(where: { $0.data?.stringId == dataId }) { self.items.remove(at: apiCallIndex) self._hasChanged = true } } -// func hasDeleteCallForDataId(_ dataId: String) -> Bool { -// if let apiCall = self.items.first(where: { $0.dataId == dataId }) { -// return apiCall.method == .delete -// } -// return false -// } - /// Returns the Api call associated with the provided id func findById(_ id: String) -> ApiCall? { return self.items.first(where: { $0.id == id }) @@ -251,7 +248,7 @@ actor ApiCallCollection: SomeCallCollection { fileprivate func _callForInstance(_ instance: T, method: HTTPMethod, transactionId: String? = nil) async throws -> ApiCall { // cleanup - let existingCalls = self.items.filter { $0.dataId == instance.stringId } + let existingCalls = self.items.filter { $0.data?.id == instance.id } self._deleteCalls(existingCalls) // create @@ -273,10 +270,9 @@ actor ApiCallCollection: SomeCallCollection { /// Creates an API call for the Storable [instance] and an HTTP [method] fileprivate func _createCall(_ method: HTTPMethod, instance: T?, transactionId: String? = nil) throws -> ApiCall { if let instance { - let jsonString = try instance.jsonString() - return ApiCall(method: method, dataId: instance.stringId, body: jsonString, transactionId: transactionId) + return ApiCall(method: method, data: instance, transactionId: transactionId) } else { - return ApiCall(method: .get) + return ApiCall(method: .get, data: nil) } } @@ -290,7 +286,7 @@ actor ApiCallCollection: SomeCallCollection { /// Sends an insert api call for the provided [instance] func sendGetRequest(instance: T) async throws where T : URLParameterConvertible { do { - let apiCall = ApiCall(method: .get) + let apiCall = ApiCall(method: .get, data: nil) apiCall.urlParameters = instance.queryParameters() let _: Empty? = try await self._prepareAndSendCall(apiCall) } catch { diff --git a/LeStorage/Codables/ApiCall.swift b/LeStorage/Codables/ApiCall.swift index f87dc0e..6af96ef 100644 --- a/LeStorage/Codables/ApiCall.swift +++ b/LeStorage/Codables/ApiCall.swift @@ -29,6 +29,69 @@ class ApiCall: ModelObject, Storable, SomeCall { /// The HTTP method of the call var method: HTTPMethod + /// The content of the call + var data: T? + + /// The number of times the call has been executed + var attemptsCount: Int = 0 + + /// The date of the last execution + var lastAttemptDate: Date = Date() + + /// The parameters to add in the URL to obtain : "?p1=v1&p2=v2" + var urlParameters: [String : String]? = nil + + init(method: HTTPMethod, data: T?, transactionId: String? = nil) { + self.method = method + self.data = data + if let transactionId { + self.transactionId = transactionId + } + } + + func copy(from other: any Storable) { + fatalError("should not happen") + } + + func formattedURLParameters() -> String? { + return self.urlParameters?.toQueryString() + } + + func urlExtension() -> String { + switch self.method { + case HTTPMethod.put, HTTPMethod.delete: + return T.path(id: self.data?.stringId) + case HTTPMethod.post: + return T.path() + case HTTPMethod.get: + if let parameters = self.urlParameters?.toQueryString() { + return T.path() + parameters + } else { + return T.path() + } + } + } + + static func relationships() -> [Relationship] { return [] } +} + + +class OldApiCall: ModelObject, Storable, SomeCall { + + static func resourceName() -> String { return "apicalls_" + T.resourceName() } + static func tokenExemptedMethods() -> [HTTPMethod] { return [] } + + var id: String = Store.randomId() + + /// The transactionId to group calls together + var transactionId: String? = Store.randomId() + + /// Creation date of the call + var creationDate: Date? = Date() + + /// The HTTP method of the call + var method: HTTPMethod + /// The content of the call var body: String? @@ -53,6 +116,15 @@ class ApiCall: ModelObject, Storable, SomeCall { } } + init(method: HTTPMethod, data: T, transactionId: String? = nil) throws { + self.method = method + self.dataId = data.stringId + self.body = try data.jsonString() + if let transactionId { + self.transactionId = transactionId + } + } + func copy(from other: any Storable) { fatalError("should not happen") } @@ -77,4 +149,19 @@ class ApiCall: ModelObject, Storable, SomeCall { } static func relationships() -> [Relationship] { return [] } + + func toNewApiCall() -> ApiCall? { + if let instance: T = try? self.body?.decode() { + let apiCall = ApiCall(method: self.method, data: instance, transactionId: self.transactionId) + apiCall.id = self.id + apiCall.creationDate = self.creationDate + apiCall.attemptsCount = self.attemptsCount + apiCall.lastAttemptDate = self.lastAttemptDate + apiCall.urlParameters = self.urlParameters + return apiCall + } else { + return nil + } + } + } diff --git a/LeStorage/Services.swift b/LeStorage/Services.swift index 7d971b4..bbf013a 100644 --- a/LeStorage/Services.swift +++ b/LeStorage/Services.swift @@ -241,23 +241,23 @@ public class Services { /// - Parameters: /// - method: the HTTP method to execute /// - payload: the content to put in the httpBody - fileprivate func _baseSyncRequest(method: HTTPMethod, payload: Encodable) throws -> URLRequest { - let urlString = baseURL + "data/" - - guard let url = URL(string: urlString) else { - throw ServiceError.urlCreationError(url: urlString) - } - - var request = URLRequest(url: url) - request.httpMethod = method.rawValue - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.httpBody = try JSON.encoder.encode(payload) - - let token = try self.keychainStore.getValue() - request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") - - return request - } +// fileprivate func _baseSyncRequest(method: HTTPMethod, payload: Encodable) throws -> URLRequest { +// let urlString = baseURL + "data/" +// +// guard let url = URL(string: urlString) else { +// throw ServiceError.urlCreationError(url: urlString) +// } +// +// var request = URLRequest(url: url) +// request.httpMethod = method.rawValue +// request.setValue("application/json", forHTTPHeaderField: "Content-Type") +// request.httpBody = try JSON.encoder.encode(payload) +// +// let token = try self.keychainStore.getValue() +// request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") +// +// return request +// } /// Runs a request using a traditional URLRequest /// - Parameters: @@ -392,19 +392,13 @@ public class Services { request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") let modelName = String(describing: T.self) - let operations = try apiCalls.map { apiCall in - - if let body = apiCall.body, let data = body.data(using: .utf8) { - let object = try JSON.decoder.decode(T.self, from: data) - return Operation(apiCallId: apiCall.id, - operation: apiCall.method.rawValue, - modelName: modelName, - data: object, - storeId: object.getStoreId()) - } else { - throw ServiceError.cantDecodeData(resource: T.resourceName(), method: apiCall.method.rawValue, content: apiCall.body) - } + let operations = apiCalls.map { apiCall in + return Operation(apiCallId: apiCall.id, + operation: apiCall.method.rawValue, + modelName: modelName, + data: apiCall.data, + storeId: apiCall.data?.getStoreId()) } let payload = SyncPayload(operations: operations, @@ -538,7 +532,7 @@ public class Services { let url = try self._url(from: apiCall) var request = URLRequest(url: url) request.httpMethod = apiCall.method.rawValue - request.httpBody = apiCall.body?.data(using: .utf8) + request.httpBody = try apiCall.data?.jsonData() request.setValue("application/json", forHTTPHeaderField: "Content-Type") if self._isTokenRequired(type: T.self, method: apiCall.method) { @@ -749,25 +743,26 @@ public class Services { try self._storeToken(username: userName, token: services.keychainStore.getValue()) } - // Tests + // MARK: - Convenience method for tests /// Executes a POST request - public func post(_ instance: T) async throws -> T { - var postRequest = try self._postRequest(type: T.self) - postRequest.httpBody = try JSON.encoder.encode(instance) - return try await self._runRequest(postRequest) + public func post(_ instance: T) async throws -> T? { + let apiCall: ApiCall = ApiCall(method: .post, data: instance) + let results: [T] = try await self.runApiCalls([apiCall]) + return results.first } /// Executes a PUT request - public func put(_ instance: T) async throws -> T { - var postRequest = try self._putRequest(type: T.self, id: instance.stringId) - postRequest.httpBody = try JSON.encoder.encode(instance) - return try await self._runRequest(postRequest) + public func put(_ instance: T) async throws -> T { + let apiCall: ApiCall = ApiCall(method: .put, data: instance) + let results: [T] = try await self.runApiCalls([apiCall]) + return results.first! } - public func delete(_ instance: T) async throws -> T { - let deleteRequest = try self._deleteRequest(type: T.self, id: instance.stringId) - return try await self._runRequest(deleteRequest) + public func delete(_ instance: T) async throws -> T { + let apiCall: ApiCall = ApiCall(method: .delete, data: instance) + let results: [T] = try await self.runApiCalls([apiCall]) + return results.first! } /// Returns a POST request for the resource diff --git a/LeStorage/StoreCenter.swift b/LeStorage/StoreCenter.swift index 6166be5..4618af6 100644 --- a/LeStorage/StoreCenter.swift +++ b/LeStorage/StoreCenter.swift @@ -465,7 +465,7 @@ public class StoreCenter { } /// Basically asks the server for new content - func synchronizeLastUpdates() async throws { + public func synchronizeLastUpdates() async throws { let lastSync = self._settingsStorage.item.lastSynchronization diff --git a/LeStorage/StoredCollection+Sync.swift b/LeStorage/StoredCollection+Sync.swift index 47e9fa5..a730fe7 100644 --- a/LeStorage/StoredCollection+Sync.swift +++ b/LeStorage/StoredCollection+Sync.swift @@ -157,12 +157,14 @@ extension StoredCollection: SomeSyncedCollection where T : SyncedStorable { } /// Deletes all items of the sequence by id and sets the collection as changed to trigger a write - public func delete(contentOfs sequence: any Sequence) { + public func delete(contentOfs sequence: any RandomAccessCollection) { defer { self.setChanged() } + guard sequence.isNotEmpty else { return } + for instance in sequence { self.deleteItem(instance) StoreCenter.main.createDeleteLog(instance)