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()) {
let jsonString: String = try FileUtils.readFile(fileURL: fileURL)
let decoded: [ApiCall<T>] = try jsonString.decodeArray() ?? []
// Logger.log("loaded \(fileURL.lastPathComponent) with \(decoded.count) items")
self.items = decoded
do {
let decoded: [ApiCall<T>] = try jsonString.decodeArray() ?? []
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
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<T>? {
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> {
// 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<T: SyncedStorable>: 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<T> {
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<T: SyncedStorable>: SomeCallCollection {
/// Sends an insert api call for the provided [instance]
func sendGetRequest(instance: T) async throws where T : URLParameterConvertible {
do {
let apiCall = ApiCall<T>(method: .get)
let apiCall = ApiCall<T>(method: .get, data: nil)
apiCall.urlParameters = instance.queryParameters()
let _: Empty? = try await self._prepareAndSendCall(apiCall)
} catch {

@ -29,6 +29,69 @@ class ApiCall<T: Storable>: 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<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
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) {
fatalError("should not happen")
}
@ -77,4 +149,19 @@ class ApiCall<T: Storable>: ModelObject, Storable, SomeCall {
}
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:
/// - 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<T: Storable>(_ 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<T: SyncedStorable>(_ instance: T) async throws -> T? {
let apiCall: ApiCall<T> = ApiCall(method: .post, data: instance)
let results: [T] = try await self.runApiCalls([apiCall])
return results.first
}
/// Executes a PUT request
public func put<T: Storable>(_ 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<T: SyncedStorable>(_ instance: T) async throws -> T {
let apiCall: ApiCall<T> = ApiCall(method: .put, data: instance)
let results: [T] = try await self.runApiCalls([apiCall])
return results.first!
}
public func delete<T: Storable>(_ 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<T: SyncedStorable>(_ instance: T) async throws -> T {
let apiCall: ApiCall<T> = ApiCall(method: .delete, data: instance)
let results: [T] = try await self.runApiCalls([apiCall])
return results.first!
}
/// Returns a POST request for the resource

@ -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

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

Loading…
Cancel
Save