|
|
|
|
@ -79,6 +79,64 @@ public class Services { |
|
|
|
|
return try await _runRequest(request) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Runs a request using a traditional URLRequest |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - request: the URLRequest to run |
|
|
|
|
/// - apiCallId: the id of the ApiCall to delete in case of success, or to schedule for a rerun in case of failure |
|
|
|
|
fileprivate func _runSyncPostRequest<T: SyncedStorable>( |
|
|
|
|
_ request: URLRequest, type: T.Type) async throws { |
|
|
|
|
let debugURL = request.url?.absoluteString ?? "" |
|
|
|
|
// print("Run \(request.httpMethod ?? "") \(debugURL)") |
|
|
|
|
let task: (Data, URLResponse) = try await URLSession.shared.data(for: request) |
|
|
|
|
print("sync POST \(String(describing: T.self)) => \(String(data: task.0, encoding: .utf8) ?? "")") |
|
|
|
|
|
|
|
|
|
var rescheduleApiCalls: Bool = false |
|
|
|
|
|
|
|
|
|
if let response = task.1 as? HTTPURLResponse { |
|
|
|
|
let statusCode = response.statusCode |
|
|
|
|
print("\(debugURL) ended, status code = \(statusCode)") |
|
|
|
|
switch statusCode { |
|
|
|
|
case 200..<300: // success |
|
|
|
|
|
|
|
|
|
let decoded: BatchResponse<T> = try self._decode(data: task.0) |
|
|
|
|
for result in decoded.results { |
|
|
|
|
switch result.status { |
|
|
|
|
case 200..<300: |
|
|
|
|
|
|
|
|
|
try await StoreCenter.main.deleteApiCallById(type: T.self, id: result.apiCallId) |
|
|
|
|
default: |
|
|
|
|
rescheduleApiCalls = true |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
default: // error |
|
|
|
|
Logger.log( |
|
|
|
|
"Failed Run \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") |
|
|
|
|
let errorString: String = String(data: task.0, encoding: .utf8) ?? "" |
|
|
|
|
var errorMessage = ErrorMessage(error: errorString, domain: "") |
|
|
|
|
|
|
|
|
|
if let message = self.errorMessageFromResponse(data: task.0) { |
|
|
|
|
errorMessage = message |
|
|
|
|
} |
|
|
|
|
try await StoreCenter.main.rescheduleApiCalls(type: T.self) |
|
|
|
|
|
|
|
|
|
// StoreCenter.main.logFailedAPICall( |
|
|
|
|
// apiCall.id, request: request, collectionName: T.resourceName(), |
|
|
|
|
// error: errorMessage.message) |
|
|
|
|
|
|
|
|
|
throw ServiceError.responseError(response: errorMessage.error) |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
let message: String = "Unexpected and unmanaged URL Response \(task.1)" |
|
|
|
|
StoreCenter.main.log(message: message) |
|
|
|
|
Logger.w(message) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if rescheduleApiCalls { |
|
|
|
|
try await StoreCenter.main.rescheduleApiCalls(type: T.self) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Runs a request using a traditional URLRequest |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - request: the URLRequest to run |
|
|
|
|
@ -112,7 +170,7 @@ public class Services { |
|
|
|
|
errorMessage = message |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
try await StoreCenter.main.rescheduleApiCalls(id: apiCall.id, type: T.self) |
|
|
|
|
try await StoreCenter.main.rescheduleApiCalls(type: T.self) |
|
|
|
|
StoreCenter.main.logFailedAPICall( |
|
|
|
|
apiCall.id, request: request, collectionName: T.resourceName(), |
|
|
|
|
error: errorMessage.message) |
|
|
|
|
@ -265,7 +323,7 @@ public class Services { |
|
|
|
|
/// Returns the URLRequest for an ApiCall |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - apiCall: An ApiCall instance to configure the returned request |
|
|
|
|
fileprivate func _syncRequest<T: SyncedStorable>(from apiCall: ApiCall<T>) throws -> URLRequest { |
|
|
|
|
fileprivate func _syncGetRequest<T: SyncedStorable>(from apiCall: ApiCall<T>) throws -> URLRequest { |
|
|
|
|
|
|
|
|
|
var urlString = baseURL + "data/" |
|
|
|
|
if let urlParameters = apiCall.formattedURLParameters() { |
|
|
|
|
@ -277,39 +335,57 @@ public class Services { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
var request = URLRequest(url: url) |
|
|
|
|
if apiCall.method == .get { |
|
|
|
|
request.httpMethod = HTTPMethod.get.rawValue |
|
|
|
|
} else { |
|
|
|
|
request.httpMethod = HTTPMethod.post.rawValue |
|
|
|
|
request.httpMethod = HTTPMethod.get.rawValue |
|
|
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type") |
|
|
|
|
|
|
|
|
|
if self._isTokenRequired(type: T.self, method: apiCall.method) { |
|
|
|
|
let token = try self.keychainStore.getValue() |
|
|
|
|
request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return request |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns the URLRequest for an ApiCall |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - apiCall: An ApiCall instance to configure the returned request |
|
|
|
|
fileprivate func _syncPostRequest<T: SyncedStorable>(from apiCalls: [ApiCall<T>]) 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 = HTTPMethod.post.rawValue |
|
|
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type") |
|
|
|
|
let token = try self.keychainStore.getValue() |
|
|
|
|
request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") |
|
|
|
|
|
|
|
|
|
if let body = apiCall.body { |
|
|
|
|
if let data = body.data(using: .utf8) { |
|
|
|
|
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) |
|
|
|
|
let modelName = String(describing: T.self) |
|
|
|
|
let payload = SyncPayload( |
|
|
|
|
operation: apiCall.method.rawValue, |
|
|
|
|
modelName: modelName, |
|
|
|
|
data: object, |
|
|
|
|
storeId: object.getStoreId(), |
|
|
|
|
deviceId: StoreCenter.main.deviceId()) |
|
|
|
|
request.httpBody = try JSON.encoder.encode(payload) |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if self._isTokenRequired(type: T.self, method: apiCall.method) { |
|
|
|
|
let token = try self.keychainStore.getValue() |
|
|
|
|
request.addValue("Token \(token)", forHTTPHeaderField: "Authorization") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let payload = SyncPayload(operations: operations, |
|
|
|
|
deviceId: StoreCenter.main.deviceId()) |
|
|
|
|
request.httpBody = try JSON.encoder.encode(payload) |
|
|
|
|
|
|
|
|
|
return request |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Starts a request to retrieve the synchronization updates |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - since: The date from which updates are retrieved |
|
|
|
|
@ -388,44 +464,41 @@ public class Services { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Executes a POST request |
|
|
|
|
public func post<T: Storable>(_ instance: T) async throws -> T { |
|
|
|
|
|
|
|
|
|
let method: HTTPMethod = .post |
|
|
|
|
let payload = SyncPayload( |
|
|
|
|
operation: method.rawValue, |
|
|
|
|
modelName: String(describing: T.self), |
|
|
|
|
data: instance, |
|
|
|
|
deviceId: StoreCenter.main.deviceId()) |
|
|
|
|
let syncRequest = try self._baseSyncRequest(method: .post, payload: payload) |
|
|
|
|
return try await self._runRequest(syncRequest) |
|
|
|
|
|
|
|
|
|
// var postRequest = try self._postRequest(type: T.self) |
|
|
|
|
// postRequest.httpBody = try jsonEncoder.encode(instance) |
|
|
|
|
// return try await self._runRequest(postRequest) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Executes a PUT request |
|
|
|
|
public func put<T: SyncedStorable>(_ instance: T) async throws -> T { |
|
|
|
|
|
|
|
|
|
let method: HTTPMethod = .put |
|
|
|
|
let payload = SyncPayload( |
|
|
|
|
operation: method.rawValue, |
|
|
|
|
modelName: String(describing: T.self), |
|
|
|
|
data: instance, |
|
|
|
|
deviceId: StoreCenter.main.deviceId()) |
|
|
|
|
let syncRequest = try self._baseSyncRequest(method: .post, payload: payload) |
|
|
|
|
return try await self._runRequest(syncRequest) |
|
|
|
|
// public func post<T: Storable>(_ instance: T) async throws -> T { |
|
|
|
|
// |
|
|
|
|
// let method: HTTPMethod = .post |
|
|
|
|
// let payload = SyncPayload( |
|
|
|
|
// operation: method.rawValue, |
|
|
|
|
// modelName: String(describing: T.self), |
|
|
|
|
// data: instance, |
|
|
|
|
// deviceId: StoreCenter.main.deviceId()) |
|
|
|
|
// let syncRequest = try self._baseSyncRequest(method: .post, payload: payload) |
|
|
|
|
// return try await self._runRequest(syncRequest) |
|
|
|
|
// } |
|
|
|
|
// |
|
|
|
|
// /// Executes a PUT request |
|
|
|
|
// public func put<T: SyncedStorable>(_ instance: T) async throws -> T { |
|
|
|
|
// |
|
|
|
|
// let method: HTTPMethod = .put |
|
|
|
|
// let payload = SyncPayload( |
|
|
|
|
// operation: method.rawValue, |
|
|
|
|
// modelName: String(describing: T.self), |
|
|
|
|
// data: instance, |
|
|
|
|
// deviceId: StoreCenter.main.deviceId()) |
|
|
|
|
// let syncRequest = try self._baseSyncRequest(method: .post, payload: payload) |
|
|
|
|
// return try await self._runRequest(syncRequest) |
|
|
|
|
// } |
|
|
|
|
|
|
|
|
|
// var postRequest = try self._putRequest(type: T.self, id: instance.stringId) |
|
|
|
|
// postRequest.httpBody = try jsonEncoder.encode(instance) |
|
|
|
|
// return try await self._runRequest(postRequest) |
|
|
|
|
/// Executes an ApiCall |
|
|
|
|
func runGetApiCall<T: SyncedStorable, V: Decodable>(_ apiCall: ApiCall<T>) async throws -> V { |
|
|
|
|
let request = try self._syncGetRequest(from: apiCall) |
|
|
|
|
return try await self._runRequest(request, apiCall: apiCall) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Executes an ApiCall |
|
|
|
|
func runApiCall<T: SyncedStorable, V: Decodable>(_ apiCall: ApiCall<T>) async throws -> V { |
|
|
|
|
let request = try self._syncRequest(from: apiCall) |
|
|
|
|
// print("HTTP \(request.httpMethod ?? "") : id = \(apiCall.dataId)") |
|
|
|
|
return try await self._runRequest(request, apiCall: apiCall) |
|
|
|
|
func runApiCalls<T: SyncedStorable>(_ apiCalls: [ApiCall<T>]) async throws { |
|
|
|
|
let request = try self._syncPostRequest(from: apiCalls) |
|
|
|
|
try await self._runSyncPostRequest(request, type: T.self) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns the URLRequest for an ApiCall |
|
|
|
|
@ -649,11 +722,26 @@ public class Services { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
struct SyncPayload<T: Encodable>: Encodable { |
|
|
|
|
var operations: [Operation<T>] |
|
|
|
|
var deviceId: String? |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
struct Operation<T: Encodable>: Encodable { |
|
|
|
|
var apiCallId: String |
|
|
|
|
var operation: String |
|
|
|
|
var modelName: String |
|
|
|
|
var data: T |
|
|
|
|
var storeId: String? |
|
|
|
|
var deviceId: String? |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
struct BatchResponse<T: Decodable>: Decodable { |
|
|
|
|
var results: [OperationResult<T>] |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
struct OperationResult<T: Decodable>: Decodable { |
|
|
|
|
var apiCallId: String |
|
|
|
|
var status: Int |
|
|
|
|
var data: T |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
struct ErrorMessage { |
|
|
|
|
|