upgrade apicall + test conveniences + fix

sync2
Laurent 9 months ago
parent dc885d4a72
commit c12a041e70
  1. 28
      LeStorage/ApiCallCollection.swift
  2. 87
      LeStorage/Codables/ApiCall.swift
  3. 79
      LeStorage/Services.swift
  4. 2
      LeStorage/StoreCenter.swift
  5. 4
      LeStorage/StoredCollection+Sync.swift

@ -75,9 +75,13 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
if FileManager.default.fileExists(atPath: fileURL.path()) { if FileManager.default.fileExists(atPath: fileURL.path()) {
let jsonString: String = try FileUtils.readFile(fileURL: fileURL) let jsonString: String = try FileUtils.readFile(fileURL: fileURL)
let decoded: [ApiCall<T>] = try jsonString.decodeArray() ?? [] do {
// Logger.log("loaded \(fileURL.lastPathComponent) with \(decoded.count) items") let decoded: [ApiCall<T>] = try jsonString.decodeArray() ?? []
self.items = decoded self.items = decoded
} catch {
let decoded: [OldApiCall<T>] = try jsonString.decodeArray() ?? []
self.items = decoded.compactMap { $0.toNewApiCall() }
}
} }
} }
@ -115,19 +119,12 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
/// Deletes a call by a data id /// Deletes a call by a data id
func deleteByDataId(_ dataId: String) { 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.items.remove(at: apiCallIndex)
self._hasChanged = true 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 /// Returns the Api call associated with the provided id
func findById(_ id: String) -> ApiCall<T>? { func findById(_ id: String) -> ApiCall<T>? {
return self.items.first(where: { $0.id == id }) return self.items.first(where: { $0.id == id })
@ -251,7 +248,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
fileprivate func _callForInstance(_ instance: T, method: HTTPMethod, transactionId: String? = nil) async throws -> ApiCall<T> { fileprivate func _callForInstance(_ instance: T, method: HTTPMethod, transactionId: String? = nil) async throws -> ApiCall<T> {
// cleanup // cleanup
let existingCalls = self.items.filter { $0.dataId == instance.stringId } let existingCalls = self.items.filter { $0.data?.id == instance.id }
self._deleteCalls(existingCalls) self._deleteCalls(existingCalls)
// create // create
@ -273,10 +270,9 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
/// Creates an API call for the Storable [instance] and an HTTP [method] /// Creates an API call for the Storable [instance] and an HTTP [method]
fileprivate func _createCall(_ method: HTTPMethod, instance: T?, transactionId: String? = nil) throws -> ApiCall<T> { fileprivate func _createCall(_ method: HTTPMethod, instance: T?, transactionId: String? = nil) throws -> ApiCall<T> {
if let instance { if let instance {
let jsonString = try instance.jsonString() return ApiCall(method: method, data: instance, transactionId: transactionId)
return ApiCall(method: method, dataId: instance.stringId, body: jsonString, transactionId: transactionId)
} else { } else {
return ApiCall(method: .get) return ApiCall(method: .get, data: nil)
} }
} }
@ -290,7 +286,7 @@ actor ApiCallCollection<T: SyncedStorable>: SomeCallCollection {
/// Sends an insert api call for the provided [instance] /// Sends an insert api call for the provided [instance]
func sendGetRequest(instance: T) async throws where T : URLParameterConvertible { func sendGetRequest(instance: T) async throws where T : URLParameterConvertible {
do { do {
let apiCall = ApiCall<T>(method: .get) let apiCall = ApiCall<T>(method: .get, data: nil)
apiCall.urlParameters = instance.queryParameters() apiCall.urlParameters = instance.queryParameters()
let _: Empty? = try await self._prepareAndSendCall(apiCall) let _: Empty? = try await self._prepareAndSendCall(apiCall)
} catch { } catch {

@ -29,6 +29,69 @@ class ApiCall<T: Storable>: ModelObject, Storable, SomeCall {
/// The HTTP method of the call /// The HTTP method of the call
var method: HTTPMethod 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<T: Storable>: 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 /// The content of the call
var body: String? var body: String?
@ -53,6 +116,15 @@ class ApiCall<T: Storable>: 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) { func copy(from other: any Storable) {
fatalError("should not happen") fatalError("should not happen")
} }
@ -77,4 +149,19 @@ class ApiCall<T: Storable>: ModelObject, Storable, SomeCall {
} }
static func relationships() -> [Relationship] { return [] } static func relationships() -> [Relationship] { return [] }
func toNewApiCall() -> ApiCall<T>? {
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
}
}
} }

@ -241,23 +241,23 @@ public class Services {
/// - Parameters: /// - Parameters:
/// - method: the HTTP method to execute /// - method: the HTTP method to execute
/// - payload: the content to put in the httpBody /// - payload: the content to put in the httpBody
fileprivate func _baseSyncRequest(method: HTTPMethod, payload: Encodable) throws -> URLRequest { // fileprivate func _baseSyncRequest(method: HTTPMethod, payload: Encodable) throws -> URLRequest {
let urlString = baseURL + "data/" // let urlString = baseURL + "data/"
//
guard let url = URL(string: urlString) else { // guard let url = URL(string: urlString) else {
throw ServiceError.urlCreationError(url: urlString) // throw ServiceError.urlCreationError(url: urlString)
} // }
//
var request = URLRequest(url: url) // var request = URLRequest(url: url)
request.httpMethod = method.rawValue // request.httpMethod = method.rawValue
request.setValue("application/json", forHTTPHeaderField: "Content-Type") // request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSON.encoder.encode(payload) // request.httpBody = try JSON.encoder.encode(payload)
//
let token = try self.keychainStore.getValue() // let token = try self.keychainStore.getValue()
request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") // request.addValue("Token \(token)", forHTTPHeaderField: "Authorization")
//
return request // return request
} // }
/// Runs a request using a traditional URLRequest /// Runs a request using a traditional URLRequest
/// - Parameters: /// - Parameters:
@ -392,19 +392,13 @@ public class Services {
request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") request.addValue("Token \(token)", forHTTPHeaderField: "Authorization")
let modelName = String(describing: T.self) let modelName = String(describing: T.self)
let operations = try apiCalls.map { apiCall in let operations = 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)
}
return Operation(apiCallId: apiCall.id,
operation: apiCall.method.rawValue,
modelName: modelName,
data: apiCall.data,
storeId: apiCall.data?.getStoreId())
} }
let payload = SyncPayload(operations: operations, let payload = SyncPayload(operations: operations,
@ -538,7 +532,7 @@ public class Services {
let url = try self._url(from: apiCall) let url = try self._url(from: apiCall)
var request = URLRequest(url: url) var request = URLRequest(url: url)
request.httpMethod = apiCall.method.rawValue 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") request.setValue("application/json", forHTTPHeaderField: "Content-Type")
if self._isTokenRequired(type: T.self, method: apiCall.method) { 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()) try self._storeToken(username: userName, token: services.keychainStore.getValue())
} }
// Tests // MARK: - Convenience method for tests
/// Executes a POST request /// Executes a POST request
public func post<T: Storable>(_ instance: T) async throws -> T { public func post<T: SyncedStorable>(_ instance: T) async throws -> T? {
var postRequest = try self._postRequest(type: T.self) let apiCall: ApiCall<T> = ApiCall(method: .post, data: instance)
postRequest.httpBody = try JSON.encoder.encode(instance) let results: [T] = try await self.runApiCalls([apiCall])
return try await self._runRequest(postRequest) return results.first
} }
/// Executes a PUT request /// Executes a PUT request
public func put<T: Storable>(_ instance: T) async throws -> T { public func put<T: SyncedStorable>(_ instance: T) async throws -> T {
var postRequest = try self._putRequest(type: T.self, id: instance.stringId) let apiCall: ApiCall<T> = ApiCall(method: .put, data: instance)
postRequest.httpBody = try JSON.encoder.encode(instance) let results: [T] = try await self.runApiCalls([apiCall])
return try await self._runRequest(postRequest) return results.first!
} }
public func delete<T: Storable>(_ instance: T) async throws -> T { public func delete<T: SyncedStorable>(_ instance: T) async throws -> T {
let deleteRequest = try self._deleteRequest(type: T.self, id: instance.stringId) let apiCall: ApiCall<T> = ApiCall(method: .delete, data: instance)
return try await self._runRequest(deleteRequest) let results: [T] = try await self.runApiCalls([apiCall])
return results.first!
} }
/// Returns a POST request for the resource /// Returns a POST request for the resource

@ -465,7 +465,7 @@ public class StoreCenter {
} }
/// Basically asks the server for new content /// Basically asks the server for new content
func synchronizeLastUpdates() async throws { public func synchronizeLastUpdates() async throws {
let lastSync = self._settingsStorage.item.lastSynchronization let lastSync = self._settingsStorage.item.lastSynchronization

@ -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 /// 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<T>) { public func delete(contentOfs sequence: any RandomAccessCollection<T>) {
defer { defer {
self.setChanged() self.setChanged()
} }
guard sequence.isNotEmpty else { return }
for instance in sequence { for instance in sequence {
self.deleteItem(instance) self.deleteItem(instance)
StoreCenter.main.createDeleteLog(instance) StoreCenter.main.createDeleteLog(instance)

Loading…
Cancel
Save