From afe5203df9adb68d78fd42a2f54660647f878d8b Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Tue, 20 Dec 2022 21:58:06 +0100 Subject: [PATCH 01/73] LocalStore --- Sources/ParseSwift/Objects/ParseObject.swift | 9 + Sources/ParseSwift/Parse.swift | 2 + Sources/ParseSwift/ParseConstants.swift | 1 + Sources/ParseSwift/Storage/LocalStorage.swift | 170 ++++++++++++++++++ .../ParseSwift/Storage/ParseFileManager.swift | 27 +++ .../ParseSwift/Types/ParseConfiguration.swift | 36 ++++ Sources/ParseSwift/Types/Query.swift | 37 +++- Sources/ParseSwift/Types/QueryWhere.swift | 10 ++ 8 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 Sources/ParseSwift/Storage/LocalStorage.swift diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 84db2f227..0ca0fbe60 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -1209,10 +1209,12 @@ extension ParseObject { #if compiler(>=5.5.2) && canImport(_Concurrency) Task { do { + try? saveLocally(method: method) let object = try await command(method: method, ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig, options: options, callbackQueue: callbackQueue) + completion(.success(object)) } catch { let defaultError = ParseError(code: .unknownError, @@ -1224,6 +1226,7 @@ extension ParseObject { } } #else + try? saveLocally(method: method) command(method: method, ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig, options: options, @@ -1249,6 +1252,7 @@ extension ParseObject { #if compiler(>=5.5.2) && canImport(_Concurrency) Task { do { + try? saveLocally(method: method) let object = try await command(method: method, options: options, callbackQueue: callbackQueue) @@ -1263,6 +1267,7 @@ extension ParseObject { } } #else + try? saveLocally(method: method) command(method: method, options: options, callbackQueue: callbackQueue, @@ -1287,6 +1292,7 @@ extension ParseObject { #if compiler(>=5.5.2) && canImport(_Concurrency) Task { do { + try? saveLocally(method: method) let object = try await command(method: method, options: options, callbackQueue: callbackQueue) @@ -1301,6 +1307,7 @@ extension ParseObject { } } #else + try? saveLocally(method: method) command(method: method, options: options, callbackQueue: callbackQueue, @@ -1325,6 +1332,7 @@ extension ParseObject { #if compiler(>=5.5.2) && canImport(_Concurrency) Task { do { + try? saveLocally(method: method) let object = try await command(method: method, options: options, callbackQueue: callbackQueue) @@ -1339,6 +1347,7 @@ extension ParseObject { } } #else + try? saveLocally(method: method) command(method: method, options: options, callbackQueue: callbackQueue, diff --git a/Sources/ParseSwift/Parse.swift b/Sources/ParseSwift/Parse.swift index cdf961312..e96e596b7 100644 --- a/Sources/ParseSwift/Parse.swift +++ b/Sources/ParseSwift/Parse.swift @@ -17,6 +17,7 @@ internal func initialize(applicationId: String, masterKey: String? = nil, serverURL: URL, liveQueryServerURL: URL? = nil, + offlinePolicy: ParseConfiguration.OfflinePolicy = .disabled, requiringCustomObjectIds: Bool = false, usingTransactions: Bool = false, usingEqualQueryConstraint: Bool = false, @@ -39,6 +40,7 @@ internal func initialize(applicationId: String, masterKey: masterKey, serverURL: serverURL, liveQueryServerURL: liveQueryServerURL, + offlinePolicy: offlinePolicy, requiringCustomObjectIds: requiringCustomObjectIds, usingTransactions: usingTransactions, usingEqualQueryConstraint: usingEqualQueryConstraint, diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift index 8f54a5856..009e22d59 100644 --- a/Sources/ParseSwift/ParseConstants.swift +++ b/Sources/ParseSwift/ParseConstants.swift @@ -15,6 +15,7 @@ enum ParseConstants { static let fileManagementPrivateDocumentsDirectory = "Private Documents/" static let fileManagementLibraryDirectory = "Library/" static let fileDownloadsDirectory = "Downloads" + static let fileObjectsDirectory = "Objects" static let bundlePrefix = "com.parse.ParseSwift" static let batchLimit = 50 static let includeAllKey = "*" diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift new file mode 100644 index 000000000..56a23ed74 --- /dev/null +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -0,0 +1,170 @@ +// +// LocalStorage.swift +// +// +// Created by Damian Van de Kauter on 03/12/2022. +// + +import Foundation + +internal struct LocalStorage { + + static func save(_ object: T, + queryIdentifier: String?) throws { + let fileManager = FileManager.default + let objectData = try ParseCoding.jsonEncoder().encode(object) + + guard let objectId = object.objectId else { + throw ParseError(code: .unknownError, message: "Object has no valid objectId") + } + + let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: object.className) + let objectPath = objectsDirectoryPath.appendingPathComponent(objectId) + + if fileManager.fileExists(atPath: objectPath.path) { + try objectData.write(to: objectPath) + } else { + fileManager.createFile(atPath: objectPath.path, contents: objectData, attributes: nil) + } + + if let queryIdentifier = queryIdentifier { + try self.saveQueryObjects([object], queryIdentifier: queryIdentifier) + } + } + + static func save(_ objects: [T], + queryIdentifier: String?) throws { + let fileManager = FileManager.default + + var successObjects: [T] = [] + for object in objects { + let objectData = try ParseCoding.jsonEncoder().encode(object) + guard let objectId = object.objectId else { + throw ParseError(code: .unknownError, message: "Object has no valid objectId") + } + + let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: object.className) + let objectPath = objectsDirectoryPath.appendingPathComponent(objectId) + + if fileManager.fileExists(atPath: objectPath.path) { + try objectData.write(to: objectPath) + } else { + fileManager.createFile(atPath: objectPath.path, contents: objectData, attributes: nil) + } + + successObjects.append(object) + } + + if let queryIdentifier = queryIdentifier { + try self.saveQueryObjects(successObjects, queryIdentifier: queryIdentifier) + } + } + + static func get(_ type: U.Type, + queryIdentifier: String) throws -> [U]? { + guard let queryObjects = try getQueryObjects()[queryIdentifier] else { return nil } + + var allObjects: [U] = [] + for queryObject in queryObjects { + let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: queryObject.className) + let objectPath = objectsDirectoryPath.appendingPathComponent(queryObject.objectId) + + let objectData = try Data(contentsOf: objectPath) + if let object = try? ParseCoding.jsonDecoder().decode(U.self, from: objectData) { + allObjects.append(object) + } + } + + return (allObjects.isEmpty ? nil : allObjects) + } + + static func saveQueryObjects(_ objects: [T], + queryIdentifier: String) throws { + var queryObjects = try getQueryObjects() + queryObjects[queryIdentifier] = try objects.map({ try QueryObject($0) }) + } + + static func getQueryObjects() throws -> [String : [QueryObject]] { + let fileManager = FileManager.default + + let objectsDirectoryPath = try ParseFileManager.objectsDirectory() + let queryObjectsPath = objectsDirectoryPath.appendingPathComponent("QueryObjects.json") + + if fileManager.fileExists(atPath: queryObjectsPath.path) { + let jsonData = try Data(contentsOf: queryObjectsPath) + let queryObjects = try? ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) + return queryObjects ?? [:] + } else { + return [:] + } + } +} + +internal struct QueryObject: Codable { + let objectId: String + let className: String + + init(_ object : T) throws { + guard let objectId = object.objectId else { + throw ParseError(code: .unknownError, message: "Object has no valid objectId") + } + self.objectId = objectId + self.className = object.className + } +} + +internal extension ParseObject { + + func saveLocally(method: Method, + queryIdentifier: String? = nil) throws { + switch method { + case .save: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + } + case .create: + if Parse.configuration.offlinePolicy.canCreate { + if Parse.configuration.isRequiringCustomObjectIds { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + } else { + throw ParseError(code: .unknownError, message: "Enable custom objectIds") + } + } + case .replace: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + } + case .update: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + } + } + } +} + +internal extension Sequence where Element: ParseObject { + + func saveLocally(method: Method, + queryIdentifier: String? = nil) throws { + let objects = map { $0 } + + switch method { + case .save: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + } + case .create: + if Parse.configuration.offlinePolicy.canCreate { + try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + } + case .replace: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + } + case .update: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + } + } + } +} diff --git a/Sources/ParseSwift/Storage/ParseFileManager.swift b/Sources/ParseSwift/Storage/ParseFileManager.swift index bde16c2ee..81fc3609b 100644 --- a/Sources/ParseSwift/Storage/ParseFileManager.swift +++ b/Sources/ParseSwift/Storage/ParseFileManager.swift @@ -227,6 +227,33 @@ public extension ParseFileManager { .appendingPathComponent(ParseConstants.fileDownloadsDirectory, isDirectory: true) } + + /** + The default directory for all `ParseObject`'s. + - returns: The objects directory. + - throws: An error of type `ParseError`. + */ + static func objectsDirectory(className: String? = nil) throws -> URL { + guard let fileManager = ParseFileManager(), + let defaultDirectoryPath = fileManager.defaultDataDirectoryPath else { + throw ParseError(code: .unknownError, message: "Cannot create ParseFileManager") + } + let objectsDirectory = defaultDirectoryPath + .appendingPathComponent(ParseConstants.fileObjectsDirectory, + isDirectory: true) + try fileManager.createDirectoryIfNeeded(objectsDirectory.path) + + if let className = className { + let classDirectory = objectsDirectory + .appendingPathComponent(className, + isDirectory: true) + try fileManager.createDirectoryIfNeeded(classDirectory.path) + + return classDirectory + } else { + return objectsDirectory + } + } /** Check if a file exists in the Swift SDK download directory. diff --git a/Sources/ParseSwift/Types/ParseConfiguration.swift b/Sources/ParseSwift/Types/ParseConfiguration.swift index 1fdf8f92c..cba96f339 100644 --- a/Sources/ParseSwift/Types/ParseConfiguration.swift +++ b/Sources/ParseSwift/Types/ParseConfiguration.swift @@ -39,6 +39,9 @@ public struct ParseConfiguration { /// The live query server URL to connect to Parse Server. public internal(set) var liveQuerysServerURL: URL? + + /// Determines wheter or not objects need to be saved locally. + public internal(set) var offlinePolicy: OfflinePolicy /// Requires `objectId`'s to be created on the client. public internal(set) var isRequiringCustomObjectIds = false @@ -123,6 +126,7 @@ public struct ParseConfiguration { specified when using the SDK on a server. - parameter serverURL: The server URL to connect to Parse Server. - parameter liveQueryServerURL: The live query server URL to connect to Parse Server. + - parameter OfflinePolicy: When enabled, objects will be stored locally for offline usage. - parameter requiringCustomObjectIds: Requires `objectId`'s to be created on the client side for each object. Must be enabled on the server to work. - parameter usingTransactions: Use transactions when saving/updating multiple objects. @@ -166,6 +170,7 @@ public struct ParseConfiguration { webhookKey: String? = nil, serverURL: URL, liveQueryServerURL: URL? = nil, + offlinePolicy: OfflinePolicy = .disabled, requiringCustomObjectIds: Bool = false, usingTransactions: Bool = false, usingEqualQueryConstraint: Bool = false, @@ -187,6 +192,7 @@ public struct ParseConfiguration { self.masterKey = masterKey self.serverURL = serverURL self.liveQuerysServerURL = liveQueryServerURL + self.offlinePolicy = offlinePolicy self.isRequiringCustomObjectIds = requiringCustomObjectIds self.isUsingTransactions = usingTransactions self.isUsingEqualQueryConstraint = usingEqualQueryConstraint @@ -389,4 +395,34 @@ public struct ParseConfiguration { authentication: authentication) self.isMigratingFromObjcSDK = migratingFromObjcSDK } + + public enum OfflinePolicy { + + /** + When using the `create` Policy, you can get, create and save objects when offline. + - warning: Using this Policy requires you to enable `allowingCustomObjectIds`. + */ + case create + + /** + When using the `save` Policy, you can get and save objects when offline. + */ + case save + + /** + When using the `disabled` Policy, offline usage is disabled. + */ + case disabled + } +} + +extension ParseConfiguration.OfflinePolicy { + + var canCreate: Bool { + return self == .create + } + + var enabled: Bool { + return self == .create || self == .save + } } diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index acb1e6c79..21c57d7ed 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -20,6 +20,7 @@ public struct Query: ParseTypeable where T: ParseObject { internal var keys: Set? internal var include: Set? internal var order: [Order]? + internal var useLocalStore: Bool = false internal var isCount: Bool? internal var explain: Bool? internal var hint: AnyCodable? @@ -498,7 +499,23 @@ extension Query: Queryable { if limit == 0 { return [ResultType]() } - return try findCommand().execute(options: options) + if useLocalStore { + do { + let objects = try findCommand().execute(options: options) + try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) + + return objects + } catch let parseError { + if parseError.equalsTo(.connectionFailed), + let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: self.where.queryIdentifier) { + return localObjects + } else { + throw parseError + } + } + } else { + return try findCommand().execute(options: options) + } } /** @@ -548,7 +565,23 @@ extension Query: Queryable { do { try findCommand().executeAsync(options: options, callbackQueue: callbackQueue) { result in - completion(result) + if useLocalStore { + switch result { + case .success(let objects): + try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) + + completion(result) + case .failure(let failure): + if failure.equalsTo(.connectionFailed), + let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: self.where.queryIdentifier) { + completion(.success(localObjects)) + } else { + completion(.failure(failure)) + } + } + } else { + completion(result) + } } } catch { let parseError = ParseError(code: .unknownError, diff --git a/Sources/ParseSwift/Types/QueryWhere.swift b/Sources/ParseSwift/Types/QueryWhere.swift index 54d15a202..ee0d24878 100644 --- a/Sources/ParseSwift/Types/QueryWhere.swift +++ b/Sources/ParseSwift/Types/QueryWhere.swift @@ -32,6 +32,16 @@ public struct QueryWhere: ParseTypeable { } } } + + internal var queryIdentifier: String { + guard let jsonData = try? ParseCoding.jsonEncoder().encode(self), + let descriptionString = String(data: jsonData, encoding: .utf8) else { + return "" + } + return descriptionString.replacingOccurrences(of: "[^A-Za-z0-9]+", + with: "", + options: [.regularExpression]) + } } public extension QueryWhere { From 36920d410a26a2c6ed52da6254f5321eee5e7d79 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Tue, 20 Dec 2022 22:00:11 +0100 Subject: [PATCH 02/73] FindExplain not working (yet) --- Sources/ParseSwift/Types/Query.swift | 67 +++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 21c57d7ed..c00b0ab95 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -538,10 +538,33 @@ extension Query: Queryable { if limit == 0 { return [U]() } - if !usingMongoDB { - return try findExplainCommand().execute(options: options) + if useLocalStore { + do { + if !usingMongoDB { + let objects = try findExplainCommand().execute(options: options) + try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) + + return objects + } else { + let objects = try findExplainMongoCommand().execute(options: options) + try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) + + return objects + } + } catch let parseError { + if parseError.equalsTo(.connectionFailed), + let localObjects = try? LocalStorage.get(U.self, queryIdentifier: self.where.queryIdentifier) { + return localObjects + } else { + throw parseError + } + } } else { - return try findExplainMongoCommand().execute(options: options) + if !usingMongoDB { + return try findExplainCommand().execute(options: options) + } else { + return try findExplainMongoCommand().execute(options: options) + } } } @@ -621,7 +644,24 @@ extension Query: Queryable { do { try findExplainCommand().executeAsync(options: options, callbackQueue: callbackQueue) { result in - completion(result) + if useLocalStore { + switch result { + case .success(let objects): + try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) + + completion(result) + case .failure(let failure): + if failure.equalsTo(.connectionFailed), + let localObjects = LocalStorage.get([U].self, + queryIdentifier: self.where.queryIdentifier) { + completion(.success(localObjects)) + } else { + completion(.failure(failure)) + } + } + } else { + completion(result) + } } } catch { let parseError = ParseError(code: .unknownError, @@ -634,7 +674,24 @@ extension Query: Queryable { do { try findExplainMongoCommand().executeAsync(options: options, callbackQueue: callbackQueue) { result in - completion(result) + if useLocalStore { + switch result { + case .success(let objects): + try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) + + completion(result) + case .failure(let failure): + if failure.equalsTo(.connectionFailed), + let localObjects = LocalStorage.get([U].self, + queryIdentifier: self.where.queryIdentifier) { + completion(.success(localObjects)) + } else { + completion(.failure(failure)) + } + } + } else { + completion(result) + } } } catch { let parseError = ParseError(code: .unknownError, From 22fb1346b0bf8258810786d618094da784810cfe Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Tue, 20 Dec 2022 22:02:41 +0100 Subject: [PATCH 03/73] Revert "FindExplain not working (yet)" This reverts commit 36920d410a26a2c6ed52da6254f5321eee5e7d79. --- Sources/ParseSwift/Types/Query.swift | 67 +++------------------------- 1 file changed, 5 insertions(+), 62 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index c00b0ab95..21c57d7ed 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -538,33 +538,10 @@ extension Query: Queryable { if limit == 0 { return [U]() } - if useLocalStore { - do { - if !usingMongoDB { - let objects = try findExplainCommand().execute(options: options) - try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) - - return objects - } else { - let objects = try findExplainMongoCommand().execute(options: options) - try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) - - return objects - } - } catch let parseError { - if parseError.equalsTo(.connectionFailed), - let localObjects = try? LocalStorage.get(U.self, queryIdentifier: self.where.queryIdentifier) { - return localObjects - } else { - throw parseError - } - } + if !usingMongoDB { + return try findExplainCommand().execute(options: options) } else { - if !usingMongoDB { - return try findExplainCommand().execute(options: options) - } else { - return try findExplainMongoCommand().execute(options: options) - } + return try findExplainMongoCommand().execute(options: options) } } @@ -644,24 +621,7 @@ extension Query: Queryable { do { try findExplainCommand().executeAsync(options: options, callbackQueue: callbackQueue) { result in - if useLocalStore { - switch result { - case .success(let objects): - try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) - - completion(result) - case .failure(let failure): - if failure.equalsTo(.connectionFailed), - let localObjects = LocalStorage.get([U].self, - queryIdentifier: self.where.queryIdentifier) { - completion(.success(localObjects)) - } else { - completion(.failure(failure)) - } - } - } else { - completion(result) - } + completion(result) } } catch { let parseError = ParseError(code: .unknownError, @@ -674,24 +634,7 @@ extension Query: Queryable { do { try findExplainMongoCommand().executeAsync(options: options, callbackQueue: callbackQueue) { result in - if useLocalStore { - switch result { - case .success(let objects): - try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) - - completion(result) - case .failure(let failure): - if failure.equalsTo(.connectionFailed), - let localObjects = LocalStorage.get([U].self, - queryIdentifier: self.where.queryIdentifier) { - completion(.success(localObjects)) - } else { - completion(.failure(failure)) - } - } - } else { - completion(result) - } + completion(result) } } catch { let parseError = ParseError(code: .unknownError, From 40f21ab0b13087128a3ec5d9cce42c775fa0a3f3 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 19:24:18 +0100 Subject: [PATCH 04/73] Fix --- Sources/ParseSwift/Parse.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/ParseSwift/Parse.swift b/Sources/ParseSwift/Parse.swift index e96e596b7..555361798 100644 --- a/Sources/ParseSwift/Parse.swift +++ b/Sources/ParseSwift/Parse.swift @@ -228,6 +228,7 @@ public func initialize( masterKey: String? = nil, serverURL: URL, liveQueryServerURL: URL? = nil, + offlinePolicy: ParseConfiguration.OfflinePolicy = .disabled, requiringCustomObjectIds: Bool = false, usingTransactions: Bool = false, usingEqualQueryConstraint: Bool = false, @@ -250,6 +251,7 @@ public func initialize( masterKey: masterKey, serverURL: serverURL, liveQueryServerURL: liveQueryServerURL, + offlinePolicy: offlinePolicy, requiringCustomObjectIds: requiringCustomObjectIds, usingTransactions: usingTransactions, usingEqualQueryConstraint: usingEqualQueryConstraint, From ad7e586a7db7d2911de790faea919d8d03c0ad93 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 19:38:42 +0100 Subject: [PATCH 05/73] Fix, Print --- Sources/ParseSwift/Storage/LocalStorage.swift | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 56a23ed74..6de62e203 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -11,6 +11,7 @@ internal struct LocalStorage { static func save(_ object: T, queryIdentifier: String?) throws { + print("[LocalStorage] save object") let fileManager = FileManager.default let objectData = try ParseCoding.jsonEncoder().encode(object) @@ -20,6 +21,7 @@ internal struct LocalStorage { let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: object.className) let objectPath = objectsDirectoryPath.appendingPathComponent(objectId) + print("[LocalStorage] objectPath: \(objectPath)") if fileManager.fileExists(atPath: objectPath.path) { try objectData.write(to: objectPath) @@ -34,6 +36,7 @@ internal struct LocalStorage { static func save(_ objects: [T], queryIdentifier: String?) throws { + print("[LocalStorage] save objects") let fileManager = FileManager.default var successObjects: [T] = [] @@ -45,6 +48,7 @@ internal struct LocalStorage { let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: object.className) let objectPath = objectsDirectoryPath.appendingPathComponent(objectId) + print("[LocalStorage] objectPath: \(objectPath)") if fileManager.fileExists(atPath: objectPath.path) { try objectData.write(to: objectPath) @@ -80,8 +84,21 @@ internal struct LocalStorage { static func saveQueryObjects(_ objects: [T], queryIdentifier: String) throws { + let fileManager = FileManager.default + + let objectsDirectoryPath = try ParseFileManager.objectsDirectory() + let queryObjectsPath = objectsDirectoryPath.appendingPathComponent("QueryObjects.json") + var queryObjects = try getQueryObjects() queryObjects[queryIdentifier] = try objects.map({ try QueryObject($0) }) + + let jsonData = try ParseCoding.jsonEncoder().encode(queryObjects) + + if fileManager.fileExists(atPath: queryObjectsPath.path) { + try jsonData.write(to: queryObjectsPath) + } else { + fileManager.createFile(atPath: queryObjectsPath.path, contents: jsonData, attributes: nil) + } } static func getQueryObjects() throws -> [String : [QueryObject]] { @@ -92,8 +109,7 @@ internal struct LocalStorage { if fileManager.fileExists(atPath: queryObjectsPath.path) { let jsonData = try Data(contentsOf: queryObjectsPath) - let queryObjects = try? ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) - return queryObjects ?? [:] + return try ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) } else { return [:] } From ccb65b3480b29c8158c857eff1814ba7579a7ccc Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 19:44:11 +0100 Subject: [PATCH 06/73] Fix --- Sources/ParseSwift/Storage/LocalStorage.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 6de62e203..1ece0c971 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -29,6 +29,7 @@ internal struct LocalStorage { fileManager.createFile(atPath: objectPath.path, contents: objectData, attributes: nil) } + print("[LocalStorage] queryIdentifier: \(String(describing: queryIdentifier))") if let queryIdentifier = queryIdentifier { try self.saveQueryObjects([object], queryIdentifier: queryIdentifier) } @@ -59,6 +60,7 @@ internal struct LocalStorage { successObjects.append(object) } + print("[LocalStorage] queryIdentifier: \(String(describing: queryIdentifier))") if let queryIdentifier = queryIdentifier { try self.saveQueryObjects(successObjects, queryIdentifier: queryIdentifier) } @@ -66,6 +68,8 @@ internal struct LocalStorage { static func get(_ type: U.Type, queryIdentifier: String) throws -> [U]? { + print("[LocalStorage] get objects") + print("[LocalStorage] queryIdentifier: \(String(describing: queryIdentifier))") guard let queryObjects = try getQueryObjects()[queryIdentifier] else { return nil } var allObjects: [U] = [] From 81fccd6dcaf34e23ac6ef60e77dafffbc6a05ed3 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 19:54:06 +0100 Subject: [PATCH 07/73] useLocalStore fix --- Sources/ParseSwift/Types/Query.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 21c57d7ed..0b643f2d9 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -436,6 +436,17 @@ public struct Query: ParseTypeable where T: ParseObject { mutableQuery.order = keys return mutableQuery } + + /** + Sort the results of the query based on the `Order` enum. + - parameter keys: An array of keys to order by. + - returns: The mutated instance of query for easy chaining. + */ + public func useLocalStore(_ state: Bool = true) -> Query { + var mutableQuery = self + mutableQuery.useLocalStore = state + return mutableQuery + } /** A variadic list of selected fields to receive updates on when the `Query` is used as a From fffd3580e7573cae6408c7634430410be99a4866 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 20:02:27 +0100 Subject: [PATCH 08/73] Filename change --- Sources/ParseSwift/ParseConstants.swift | 1 + Sources/ParseSwift/Storage/LocalStorage.swift | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift index 009e22d59..a86679262 100644 --- a/Sources/ParseSwift/ParseConstants.swift +++ b/Sources/ParseSwift/ParseConstants.swift @@ -16,6 +16,7 @@ enum ParseConstants { static let fileManagementLibraryDirectory = "Library/" static let fileDownloadsDirectory = "Downloads" static let fileObjectsDirectory = "Objects" + static let queryObjectsFile = "QueryObjects" static let bundlePrefix = "com.parse.ParseSwift" static let batchLimit = 50 static let includeAllKey = "*" diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 1ece0c971..01e8d81ae 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -91,7 +91,7 @@ internal struct LocalStorage { let fileManager = FileManager.default let objectsDirectoryPath = try ParseFileManager.objectsDirectory() - let queryObjectsPath = objectsDirectoryPath.appendingPathComponent("QueryObjects.json") + let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile) var queryObjects = try getQueryObjects() queryObjects[queryIdentifier] = try objects.map({ try QueryObject($0) }) @@ -109,7 +109,7 @@ internal struct LocalStorage { let fileManager = FileManager.default let objectsDirectoryPath = try ParseFileManager.objectsDirectory() - let queryObjectsPath = objectsDirectoryPath.appendingPathComponent("QueryObjects.json") + let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile) if fileManager.fileExists(atPath: queryObjectsPath.path) { let jsonData = try Data(contentsOf: queryObjectsPath) From 30a309b89fd71fbdec5c60b9bc57c7165b7b570d Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 20:10:46 +0100 Subject: [PATCH 09/73] Test --- Sources/ParseSwift/Types/Query.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 0b643f2d9..df24dbf9a 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -583,12 +583,12 @@ extension Query: Queryable { completion(result) case .failure(let failure): - if failure.equalsTo(.connectionFailed), - let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: self.where.queryIdentifier) { - completion(.success(localObjects)) - } else { + //if failure.equalsTo(.connectionFailed), + let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: self.where.queryIdentifier) //{ + completion(.success(localObjects ?? [])) + /*} else { completion(.failure(failure)) - } + }*/ } } else { completion(result) From cd32e3084e4ce28c3ccded3329a2a31f6a05ea07 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 20:16:39 +0100 Subject: [PATCH 10/73] Changed queryIdentifier --- Sources/ParseSwift/Types/Query.swift | 10 ++++++++++ Sources/ParseSwift/Types/QueryWhere.swift | 10 ---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index df24dbf9a..f6c5255cd 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -45,6 +45,16 @@ public struct Query: ParseTypeable where T: ParseObject { public var className: String { Self.className } + + internal var queryIdentifier: String { + guard let jsonData = try? ParseCoding.jsonEncoder().encode(self), + let descriptionString = String(data: jsonData, encoding: .utf8) else { + return className + } + return descriptionString.replacingOccurrences(of: "[^A-Za-z0-9]+", + with: "", + options: [.regularExpression]) + } struct AggregateBody: Codable where T: ParseObject { let pipeline: [[String: AnyCodable]]? diff --git a/Sources/ParseSwift/Types/QueryWhere.swift b/Sources/ParseSwift/Types/QueryWhere.swift index ee0d24878..54d15a202 100644 --- a/Sources/ParseSwift/Types/QueryWhere.swift +++ b/Sources/ParseSwift/Types/QueryWhere.swift @@ -32,16 +32,6 @@ public struct QueryWhere: ParseTypeable { } } } - - internal var queryIdentifier: String { - guard let jsonData = try? ParseCoding.jsonEncoder().encode(self), - let descriptionString = String(data: jsonData, encoding: .utf8) else { - return "" - } - return descriptionString.replacingOccurrences(of: "[^A-Za-z0-9]+", - with: "", - options: [.regularExpression]) - } } public extension QueryWhere { From 9b0cea9383170a3e4b0f9066608da1a7f2a5cedc Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 20:18:14 +0100 Subject: [PATCH 11/73] Fix --- Sources/ParseSwift/Types/Query.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index f6c5255cd..0138cb5b3 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -523,12 +523,12 @@ extension Query: Queryable { if useLocalStore { do { let objects = try findCommand().execute(options: options) - try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) + try? LocalStorage.save(objects, queryIdentifier: queryIdentifier) return objects } catch let parseError { if parseError.equalsTo(.connectionFailed), - let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: self.where.queryIdentifier) { + let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { return localObjects } else { throw parseError @@ -589,12 +589,12 @@ extension Query: Queryable { if useLocalStore { switch result { case .success(let objects): - try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) + try? LocalStorage.save(objects, queryIdentifier: queryIdentifier) completion(result) case .failure(let failure): //if failure.equalsTo(.connectionFailed), - let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: self.where.queryIdentifier) //{ + let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) //{ completion(.success(localObjects ?? [])) /*} else { completion(.failure(failure)) From c455849f236cdae1acfd7a13471c4e54de4f563c Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 20:20:53 +0100 Subject: [PATCH 12/73] Classname --- Sources/ParseSwift/Types/Query.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 0138cb5b3..e676dcbe6 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -51,9 +51,9 @@ public struct Query: ParseTypeable where T: ParseObject { let descriptionString = String(data: jsonData, encoding: .utf8) else { return className } - return descriptionString.replacingOccurrences(of: "[^A-Za-z0-9]+", - with: "", - options: [.regularExpression]) + return className + descriptionString.replacingOccurrences(of: "[^A-Za-z0-9]+", + with: "", + options: [.regularExpression]) } struct AggregateBody: Codable where T: ParseObject { From 6157b30391940e8e9e8d71dc9950a1999f8270f7 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 20:45:38 +0100 Subject: [PATCH 13/73] QueryDate, print --- Sources/ParseSwift/Storage/LocalStorage.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 01e8d81ae..1c0f479b3 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -74,11 +74,15 @@ internal struct LocalStorage { var allObjects: [U] = [] for queryObject in queryObjects { + print("[LocalStorage] \(queryObject.objectId)") let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: queryObject.className) let objectPath = objectsDirectoryPath.appendingPathComponent(queryObject.objectId) + print("[LocalStorage] \(objectPath)") let objectData = try Data(contentsOf: objectPath) + print("[LocalStorage] objectData: \(objectData)") if let object = try? ParseCoding.jsonDecoder().decode(U.self, from: objectData) { + print("[LocalStorage] object: \(object)") allObjects.append(object) } } @@ -123,6 +127,7 @@ internal struct LocalStorage { internal struct QueryObject: Codable { let objectId: String let className: String + let queryDate: Date init(_ object : T) throws { guard let objectId = object.objectId else { @@ -130,6 +135,7 @@ internal struct QueryObject: Codable { } self.objectId = objectId self.className = object.className + self.queryDate = Date() } } From 35e1c5fefe17bb956594b3a2a77e79bb60d34ea5 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 20:53:59 +0100 Subject: [PATCH 14/73] print --- Sources/ParseSwift/Storage/LocalStorage.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 1c0f479b3..85aa37500 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -71,6 +71,7 @@ internal struct LocalStorage { print("[LocalStorage] get objects") print("[LocalStorage] queryIdentifier: \(String(describing: queryIdentifier))") guard let queryObjects = try getQueryObjects()[queryIdentifier] else { return nil } + print("[LocalStorage] queryObjects: \(queryObjects)") var allObjects: [U] = [] for queryObject in queryObjects { @@ -114,9 +115,11 @@ internal struct LocalStorage { let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile) + print("[LocalStorage] queryObjectsPath: \(queryObjectsPath)") if fileManager.fileExists(atPath: queryObjectsPath.path) { let jsonData = try Data(contentsOf: queryObjectsPath) + print("[LocalStorage] jsonData: \(jsonData)") return try ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) } else { return [:] From cab5ae7a37e6be2387b48cae9b37d4eeaabef691 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 21:00:58 +0100 Subject: [PATCH 15/73] Print decode error --- Sources/ParseSwift/Storage/LocalStorage.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 85aa37500..5b4108371 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -120,7 +120,12 @@ internal struct LocalStorage { if fileManager.fileExists(atPath: queryObjectsPath.path) { let jsonData = try Data(contentsOf: queryObjectsPath) print("[LocalStorage] jsonData: \(jsonData)") - return try ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) + do { + return try ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) + } catch { + print("[LocalStorage] error: \(error)") + throw error + } } else { return [:] } From a5e575045cb9083007dfc6b6611c1202f8770476 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 21:05:54 +0100 Subject: [PATCH 16/73] queryIdentifiers --- Sources/ParseSwift/Storage/LocalStorage.swift | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 5b4108371..e031bdbdf 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -70,7 +70,9 @@ internal struct LocalStorage { queryIdentifier: String) throws -> [U]? { print("[LocalStorage] get objects") print("[LocalStorage] queryIdentifier: \(String(describing: queryIdentifier))") - guard let queryObjects = try getQueryObjects()[queryIdentifier] else { return nil } + let queryIdentifiers = try getQueryObjects() + print("[LocalStorage] queryIdentifiers: \(queryIdentifiers)") + guard let queryObjects = queryIdentifiers[queryIdentifier] else { return nil } print("[LocalStorage] queryObjects: \(queryObjects)") var allObjects: [U] = [] @@ -115,17 +117,10 @@ internal struct LocalStorage { let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile) - print("[LocalStorage] queryObjectsPath: \(queryObjectsPath)") if fileManager.fileExists(atPath: queryObjectsPath.path) { let jsonData = try Data(contentsOf: queryObjectsPath) - print("[LocalStorage] jsonData: \(jsonData)") - do { - return try ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) - } catch { - print("[LocalStorage] error: \(error)") - throw error - } + return try ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) } else { return [:] } From e9e8237810c6e7d7049469338274ce686df85625 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 21:54:20 +0100 Subject: [PATCH 17/73] Sets fix --- Sources/ParseSwift/Types/Query.swift | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index e676dcbe6..cefdf1adf 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -47,13 +47,29 @@ public struct Query: ParseTypeable where T: ParseObject { } internal var queryIdentifier: String { - guard let jsonData = try? ParseCoding.jsonEncoder().encode(self), + var mutableQuery = self + mutableQuery.keys = nil + mutableQuery.include = nil + mutableQuery.excludeKeys = nil + + guard let jsonData = try? ParseCoding.jsonEncoder().encode(mutableQuery), let descriptionString = String(data: jsonData, encoding: .utf8) else { return className } - return className + descriptionString.replacingOccurrences(of: "[^A-Za-z0-9]+", - with: "", - options: [.regularExpression]) + + let sortedSets = ( + (keys?.sorted(by: { $0 < $1 }) ?? []) + + (include?.sorted(by: { $0 < $1 }) ?? []) + + (excludeKeys?.sorted(by: { $0 < $1 }) ?? []) + ).joined(separator: "") //Sets need to be sorted to maintain the same queryIdentifier + + return ( + className + + sortedSets + + descriptionString.replacingOccurrences(of: "[^A-Za-z0-9]+", + with: "", + options: [.regularExpression]) + ) } struct AggregateBody: Codable where T: ParseObject { From 7d3f12c3f7b09a9fa383ce7fab6ac6babce504ce Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 21:56:43 +0100 Subject: [PATCH 18/73] Fix --- Sources/ParseSwift/Types/Query.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index cefdf1adf..e17456250 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -66,10 +66,10 @@ public struct Query: ParseTypeable where T: ParseObject { return ( className + sortedSets + - descriptionString.replacingOccurrences(of: "[^A-Za-z0-9]+", - with: "", - options: [.regularExpression]) - ) + descriptionString + ).replacingOccurrences(of: "[^A-Za-z0-9]+", + with: "", + options: [.regularExpression]) } struct AggregateBody: Codable where T: ParseObject { From ecfeec853ddf8d6b1102ba500036468debfe3928 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 22:01:56 +0100 Subject: [PATCH 19/73] Cleaned --- Sources/ParseSwift/Storage/LocalStorage.swift | 16 ++-------------- Sources/ParseSwift/Types/Query.swift | 10 +++++----- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index e031bdbdf..d8582b707 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -11,7 +11,6 @@ internal struct LocalStorage { static func save(_ object: T, queryIdentifier: String?) throws { - print("[LocalStorage] save object") let fileManager = FileManager.default let objectData = try ParseCoding.jsonEncoder().encode(object) @@ -21,7 +20,6 @@ internal struct LocalStorage { let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: object.className) let objectPath = objectsDirectoryPath.appendingPathComponent(objectId) - print("[LocalStorage] objectPath: \(objectPath)") if fileManager.fileExists(atPath: objectPath.path) { try objectData.write(to: objectPath) @@ -29,7 +27,6 @@ internal struct LocalStorage { fileManager.createFile(atPath: objectPath.path, contents: objectData, attributes: nil) } - print("[LocalStorage] queryIdentifier: \(String(describing: queryIdentifier))") if let queryIdentifier = queryIdentifier { try self.saveQueryObjects([object], queryIdentifier: queryIdentifier) } @@ -37,7 +34,6 @@ internal struct LocalStorage { static func save(_ objects: [T], queryIdentifier: String?) throws { - print("[LocalStorage] save objects") let fileManager = FileManager.default var successObjects: [T] = [] @@ -49,7 +45,6 @@ internal struct LocalStorage { let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: object.className) let objectPath = objectsDirectoryPath.appendingPathComponent(objectId) - print("[LocalStorage] objectPath: \(objectPath)") if fileManager.fileExists(atPath: objectPath.path) { try objectData.write(to: objectPath) @@ -60,7 +55,6 @@ internal struct LocalStorage { successObjects.append(object) } - print("[LocalStorage] queryIdentifier: \(String(describing: queryIdentifier))") if let queryIdentifier = queryIdentifier { try self.saveQueryObjects(successObjects, queryIdentifier: queryIdentifier) } @@ -70,22 +64,16 @@ internal struct LocalStorage { queryIdentifier: String) throws -> [U]? { print("[LocalStorage] get objects") print("[LocalStorage] queryIdentifier: \(String(describing: queryIdentifier))") - let queryIdentifiers = try getQueryObjects() - print("[LocalStorage] queryIdentifiers: \(queryIdentifiers)") - guard let queryObjects = queryIdentifiers[queryIdentifier] else { return nil } - print("[LocalStorage] queryObjects: \(queryObjects)") + guard let queryObjects = try getQueryObjects()[queryIdentifier] else { return nil } var allObjects: [U] = [] for queryObject in queryObjects { - print("[LocalStorage] \(queryObject.objectId)") + print("[LocalStorage] get id: \(queryObject.objectId)") let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: queryObject.className) let objectPath = objectsDirectoryPath.appendingPathComponent(queryObject.objectId) - print("[LocalStorage] \(objectPath)") let objectData = try Data(contentsOf: objectPath) - print("[LocalStorage] objectData: \(objectData)") if let object = try? ParseCoding.jsonDecoder().decode(U.self, from: objectData) { - print("[LocalStorage] object: \(object)") allObjects.append(object) } } diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index e17456250..d064c711c 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -609,12 +609,12 @@ extension Query: Queryable { completion(result) case .failure(let failure): - //if failure.equalsTo(.connectionFailed), - let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) //{ - completion(.success(localObjects ?? [])) - /*} else { + if failure.equalsTo(.connectionFailed), + let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { + completion(.success(localObjects)) + } else { completion(.failure(failure)) - }*/ + } } } else { completion(result) From 0a375fbe5cebe7432e3e4655b375b3af8dcafd99 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 22:25:35 +0100 Subject: [PATCH 20/73] sorted Sets names --- Sources/ParseSwift/Types/Query.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index d064c711c..4d90b66c6 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -57,10 +57,14 @@ public struct Query: ParseTypeable where T: ParseObject { return className } + let sortedKeys = (keys?.count == 0 ? [] : ["keys"]) + (keys?.sorted(by: { $0 < $1 }) ?? []) + let sortedInclude = (include?.count == 0 ? [] : ["include"]) + (include?.sorted(by: { $0 < $1 }) ?? []) + let sortedExcludeKeys = (excludeKeys?.count == 0 ? [] : ["excludeKeys"]) + (excludeKeys?.sorted(by: { $0 < $1 }) ?? []) + let sortedSets = ( - (keys?.sorted(by: { $0 < $1 }) ?? []) + - (include?.sorted(by: { $0 < $1 }) ?? []) + - (excludeKeys?.sorted(by: { $0 < $1 }) ?? []) + sortedKeys + + sortedInclude + + sortedExcludeKeys ).joined(separator: "") //Sets need to be sorted to maintain the same queryIdentifier return ( From 3b2469c13933d3b2bf9513f6d9a7034ad27e3add Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 22:26:12 +0100 Subject: [PATCH 21/73] Comment --- Sources/ParseSwift/Types/Query.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 4d90b66c6..23a28f74b 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -57,6 +57,7 @@ public struct Query: ParseTypeable where T: ParseObject { return className } + //Sets need to be sorted to maintain the same queryIdentifier let sortedKeys = (keys?.count == 0 ? [] : ["keys"]) + (keys?.sorted(by: { $0 < $1 }) ?? []) let sortedInclude = (include?.count == 0 ? [] : ["include"]) + (include?.sorted(by: { $0 < $1 }) ?? []) let sortedExcludeKeys = (excludeKeys?.count == 0 ? [] : ["excludeKeys"]) + (excludeKeys?.sorted(by: { $0 < $1 }) ?? []) @@ -65,7 +66,7 @@ public struct Query: ParseTypeable where T: ParseObject { sortedKeys + sortedInclude + sortedExcludeKeys - ).joined(separator: "") //Sets need to be sorted to maintain the same queryIdentifier + ).joined(separator: "") return ( className + From 1746e2974bf4aa75c457b08e821c6190512a5a87 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 22:27:34 +0100 Subject: [PATCH 22/73] Fix: added fields set --- Sources/ParseSwift/Types/Query.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 23a28f74b..dca903d78 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -51,6 +51,7 @@ public struct Query: ParseTypeable where T: ParseObject { mutableQuery.keys = nil mutableQuery.include = nil mutableQuery.excludeKeys = nil + mutableQuery.fields = nil guard let jsonData = try? ParseCoding.jsonEncoder().encode(mutableQuery), let descriptionString = String(data: jsonData, encoding: .utf8) else { @@ -61,11 +62,13 @@ public struct Query: ParseTypeable where T: ParseObject { let sortedKeys = (keys?.count == 0 ? [] : ["keys"]) + (keys?.sorted(by: { $0 < $1 }) ?? []) let sortedInclude = (include?.count == 0 ? [] : ["include"]) + (include?.sorted(by: { $0 < $1 }) ?? []) let sortedExcludeKeys = (excludeKeys?.count == 0 ? [] : ["excludeKeys"]) + (excludeKeys?.sorted(by: { $0 < $1 }) ?? []) + let sortedFieldsKeys = (fields?.count == 0 ? [] : ["fields"]) + (fields?.sorted(by: { $0 < $1 }) ?? []) let sortedSets = ( sortedKeys + sortedInclude + - sortedExcludeKeys + sortedExcludeKeys + + sortedFieldsKeys ).joined(separator: "") return ( From 2d6a9da21be355c6926bd75447c971592f6cb2a6 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 22:32:04 +0100 Subject: [PATCH 23/73] Print responseError --- Sources/ParseSwift/Extensions/URLSession.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/ParseSwift/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift index 60ff25e92..e18eb75f1 100644 --- a/Sources/ParseSwift/Extensions/URLSession.swift +++ b/Sources/ParseSwift/Extensions/URLSession.swift @@ -176,6 +176,8 @@ internal extension URLSession { ) { dataTask(with: request) { (responseData, urlResponse, responseError) in + print("responseError") + print(responseError) guard let httpResponse = urlResponse as? HTTPURLResponse else { completion(self.makeResult(request: request, responseData: responseData, From faba3289535d370c68318c461aa17048c25129a9 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 22:35:07 +0100 Subject: [PATCH 24/73] Removed print --- Sources/ParseSwift/Extensions/URLSession.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/ParseSwift/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift index e18eb75f1..60ff25e92 100644 --- a/Sources/ParseSwift/Extensions/URLSession.swift +++ b/Sources/ParseSwift/Extensions/URLSession.swift @@ -176,8 +176,6 @@ internal extension URLSession { ) { dataTask(with: request) { (responseData, urlResponse, responseError) in - print("responseError") - print(responseError) guard let httpResponse = urlResponse as? HTTPURLResponse else { completion(self.makeResult(request: request, responseData: responseData, From aa645984f801c1346212f559dee8d7c77c5fc8ba Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 22:40:13 +0100 Subject: [PATCH 25/73] Created no internet connection error --- Sources/ParseSwift/Extensions/URLSession.swift | 9 +++++++-- Sources/ParseSwift/Types/ParseError.swift | 5 +++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Sources/ParseSwift/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift index 60ff25e92..53f7e959e 100644 --- a/Sources/ParseSwift/Extensions/URLSession.swift +++ b/Sources/ParseSwift/Extensions/URLSession.swift @@ -66,11 +66,16 @@ internal extension URLSession { responseError: Error?, mapper: @escaping (Data) throws -> U) -> Result { if let responseError = responseError { - guard let parseError = responseError as? ParseError else { + if responseError._code == NSURLErrorNotConnectedToInternet { return .failure(ParseError(code: .unknownError, message: "Unable to connect with parse-server: \(responseError)")) + } else { + guard let parseError = responseError as? ParseError else { + return .failure(ParseError(code: .notConnectedToInternet, + message: "Unable to connect with the internet: \(responseError)")) + } + return .failure(parseError) } - return .failure(parseError) } guard let response = urlResponse else { guard let parseError = responseError as? ParseError else { diff --git a/Sources/ParseSwift/Types/ParseError.swift b/Sources/ParseSwift/Types/ParseError.swift index 2667cde98..b2b999f47 100644 --- a/Sources/ParseSwift/Types/ParseError.swift +++ b/Sources/ParseSwift/Types/ParseError.swift @@ -348,6 +348,11 @@ public struct ParseError: ParseTypeable, Swift.Error { a non-2XX status code. */ case xDomainRequest = 602 + + /** + Error code indicating that the device is not connected to the internet. + */ + case notConnectedToInternet = 1009 /** Error code indicating any other custom error sent from the Parse Server. From f96380a7e8c746cf77918d52a215f51f2ab98c75 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 22:42:04 +0100 Subject: [PATCH 26/73] Compare error --- Sources/ParseSwift/Types/Query.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index dca903d78..788f8c905 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -551,7 +551,7 @@ extension Query: Queryable { return objects } catch let parseError { - if parseError.equalsTo(.connectionFailed), + if parseError.equalsTo(.notConnectedToInternet) || parseError.equalsTo(.connectionFailed), let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { return localObjects } else { @@ -617,7 +617,7 @@ extension Query: Queryable { completion(result) case .failure(let failure): - if failure.equalsTo(.connectionFailed), + if failure.equalsTo(.notConnectedToInternet) || failure.equalsTo(.connectionFailed), let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { completion(.success(localObjects)) } else { From 3acf50cc6f3a5415fe44939ce9510251e2067853 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 22:45:05 +0100 Subject: [PATCH 27/73] Fix --- Sources/ParseSwift/Extensions/URLSession.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ParseSwift/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift index 53f7e959e..30de02965 100644 --- a/Sources/ParseSwift/Extensions/URLSession.swift +++ b/Sources/ParseSwift/Extensions/URLSession.swift @@ -67,12 +67,12 @@ internal extension URLSession { mapper: @escaping (Data) throws -> U) -> Result { if let responseError = responseError { if responseError._code == NSURLErrorNotConnectedToInternet { - return .failure(ParseError(code: .unknownError, - message: "Unable to connect with parse-server: \(responseError)")) + return .failure(ParseError(code: .notConnectedToInternet, + message: "Unable to connect with the internet: \(responseError)")) } else { guard let parseError = responseError as? ParseError else { - return .failure(ParseError(code: .notConnectedToInternet, - message: "Unable to connect with the internet: \(responseError)")) + return .failure(ParseError(code: .unknownError, + message: "Unable to connect with parse-server: \(responseError)")) } return .failure(parseError) } From 087400074dcd51cebdad4eb3fb9f8da88281593b Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 23:01:54 +0100 Subject: [PATCH 28/73] Try new error code for fix iOS --- Sources/ParseSwift/Extensions/URLSession.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ParseSwift/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift index 30de02965..216d72a75 100644 --- a/Sources/ParseSwift/Extensions/URLSession.swift +++ b/Sources/ParseSwift/Extensions/URLSession.swift @@ -66,7 +66,7 @@ internal extension URLSession { responseError: Error?, mapper: @escaping (Data) throws -> U) -> Result { if let responseError = responseError { - if responseError._code == NSURLErrorNotConnectedToInternet { + if let err = responseError as? URLError, err.code == URLError.Code.notConnectedToInternet { return .failure(ParseError(code: .notConnectedToInternet, message: "Unable to connect with the internet: \(responseError)")) } else { From 517499c4bbc478fd0d5e7ac77599d7238a94016e Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 23:05:17 +0100 Subject: [PATCH 29/73] Connection not allowed --- Sources/ParseSwift/Extensions/URLSession.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ParseSwift/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift index 216d72a75..204a578c6 100644 --- a/Sources/ParseSwift/Extensions/URLSession.swift +++ b/Sources/ParseSwift/Extensions/URLSession.swift @@ -66,7 +66,8 @@ internal extension URLSession { responseError: Error?, mapper: @escaping (Data) throws -> U) -> Result { if let responseError = responseError { - if let err = responseError as? URLError, err.code == URLError.Code.notConnectedToInternet { + if let urlError = responseError as? URLError, + urlError.code == URLError.Code.notConnectedToInternet || urlError.code == URLError.Code.dataNotAllowed { return .failure(ParseError(code: .notConnectedToInternet, message: "Unable to connect with the internet: \(responseError)")) } else { From f0e9cee20761aaf2de4a285ef412296c199bb650 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Thu, 22 Dec 2022 19:20:20 +0100 Subject: [PATCH 30/73] Function changes --- Sources/ParseSwift/Storage/LocalStorage.swift | 74 +++++++++++-------- Sources/ParseSwift/Types/Query.swift | 4 +- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index d8582b707..f7cb3b972 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -12,6 +12,7 @@ internal struct LocalStorage { static func save(_ object: T, queryIdentifier: String?) throws { let fileManager = FileManager.default + print("[LocalStorage] save object") let objectData = try ParseCoding.jsonEncoder().encode(object) guard let objectId = object.objectId else { @@ -35,6 +36,7 @@ internal struct LocalStorage { static func save(_ objects: [T], queryIdentifier: String?) throws { let fileManager = FileManager.default + print("[LocalStorage] save objects") var successObjects: [T] = [] for object in objects { @@ -132,26 +134,32 @@ internal struct QueryObject: Codable { internal extension ParseObject { - func saveLocally(method: Method, + func saveLocally(method: Method? = nil, queryIdentifier: String? = nil) throws { - switch method { - case .save: - if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(self, queryIdentifier: queryIdentifier) - } - case .create: - if Parse.configuration.offlinePolicy.canCreate { - if Parse.configuration.isRequiringCustomObjectIds { + if let method = method { + switch method { + case .save: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + } + case .create: + if Parse.configuration.offlinePolicy.canCreate { + if Parse.configuration.isRequiringCustomObjectIds { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + } else { + throw ParseError(code: .unknownError, message: "Enable custom objectIds") + } + } + case .replace: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + } + case .update: + if Parse.configuration.offlinePolicy.enabled { try LocalStorage.save(self, queryIdentifier: queryIdentifier) - } else { - throw ParseError(code: .unknownError, message: "Enable custom objectIds") } } - case .replace: - if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(self, queryIdentifier: queryIdentifier) - } - case .update: + } else { if Parse.configuration.offlinePolicy.enabled { try LocalStorage.save(self, queryIdentifier: queryIdentifier) } @@ -161,24 +169,30 @@ internal extension ParseObject { internal extension Sequence where Element: ParseObject { - func saveLocally(method: Method, + func saveLocally(method: Method? = nil, queryIdentifier: String? = nil) throws { let objects = map { $0 } - switch method { - case .save: - if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(objects, queryIdentifier: queryIdentifier) - } - case .create: - if Parse.configuration.offlinePolicy.canCreate { - try LocalStorage.save(objects, queryIdentifier: queryIdentifier) - } - case .replace: - if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + if let method = method { + switch method { + case .save: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + } + case .create: + if Parse.configuration.offlinePolicy.canCreate { + try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + } + case .replace: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + } + case .update: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + } } - case .update: + } else { if Parse.configuration.offlinePolicy.enabled { try LocalStorage.save(objects, queryIdentifier: queryIdentifier) } diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 788f8c905..8853c559e 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -547,7 +547,7 @@ extension Query: Queryable { if useLocalStore { do { let objects = try findCommand().execute(options: options) - try? LocalStorage.save(objects, queryIdentifier: queryIdentifier) + try? objects.saveLocally(queryIdentifier: queryIdentifier) return objects } catch let parseError { @@ -613,7 +613,7 @@ extension Query: Queryable { if useLocalStore { switch result { case .success(let objects): - try? LocalStorage.save(objects, queryIdentifier: queryIdentifier) + try? objects.saveLocally(queryIdentifier: queryIdentifier) completion(result) case .failure(let failure): From 6b32d79507526e41d122d93c074043e05ec1ea9b Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Thu, 22 Dec 2022 19:54:38 +0100 Subject: [PATCH 31/73] FindAll --- Sources/ParseSwift/Types/Query.swift | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 8853c559e..62b895ca6 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -747,16 +747,30 @@ extension Query: Queryable { finished = true } } catch { - let defaultError = ParseError(code: .unknownError, - message: error.localizedDescription) - let parseError = error as? ParseError ?? defaultError - callbackQueue.async { - completion(.failure(parseError)) + if let urlError = error as? URLError, + urlError.code == URLError.Code.notConnectedToInternet || urlError.code == URLError.Code.dataNotAllowed, let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { + completion(.success(localObjects)) + } else { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError + + if parseError.equalsTo(.notConnectedToInternet) || parseError.equalsTo(.connectionFailed), + let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { + completion(.success(localObjects)) + } else { + callbackQueue.async { + completion(.failure(parseError)) + } + } } return } } - + + if useLocalStore { + try? results.saveLocally(queryIdentifier: queryIdentifier) + } callbackQueue.async { completion(.success(results)) } From 05d3821c843cadbb2041ee24ba29ae9953ad8610 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Thu, 22 Dec 2022 20:20:32 +0100 Subject: [PATCH 32/73] First --- Sources/ParseSwift/Storage/LocalStorage.swift | 28 +++++++---- Sources/ParseSwift/Types/Query.swift | 46 ++++++++++++++++--- 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index f7cb3b972..202c0ffac 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -33,7 +33,7 @@ internal struct LocalStorage { } } - static func save(_ objects: [T], + static func saveAll(_ objects: [T], queryIdentifier: String?) throws { let fileManager = FileManager.default print("[LocalStorage] save objects") @@ -63,14 +63,24 @@ internal struct LocalStorage { } static func get(_ type: U.Type, + queryIdentifier: String) throws -> U? { + guard let queryObjects = try getQueryObjects()[queryIdentifier], + let queryObject = queryObjects.first else { return nil } + + let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: queryObject.className) + let objectPath = objectsDirectoryPath.appendingPathComponent(queryObject.objectId) + + let objectData = try Data(contentsOf: objectPath) + + return try ParseCoding.jsonDecoder().decode(U.self, from: objectData) + } + + static func getAll(_ type: U.Type, queryIdentifier: String) throws -> [U]? { - print("[LocalStorage] get objects") - print("[LocalStorage] queryIdentifier: \(String(describing: queryIdentifier))") guard let queryObjects = try getQueryObjects()[queryIdentifier] else { return nil } var allObjects: [U] = [] for queryObject in queryObjects { - print("[LocalStorage] get id: \(queryObject.objectId)") let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: queryObject.className) let objectPath = objectsDirectoryPath.appendingPathComponent(queryObject.objectId) @@ -177,24 +187,24 @@ internal extension Sequence where Element: ParseObject { switch method { case .save: if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } case .create: if Parse.configuration.offlinePolicy.canCreate { - try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } case .replace: if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } case .update: if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } } } else { if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } } } diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 62b895ca6..1f4337386 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -552,7 +552,7 @@ extension Query: Queryable { return objects } catch let parseError { if parseError.equalsTo(.notConnectedToInternet) || parseError.equalsTo(.connectionFailed), - let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { + let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) { return localObjects } else { throw parseError @@ -618,7 +618,7 @@ extension Query: Queryable { completion(result) case .failure(let failure): if failure.equalsTo(.notConnectedToInternet) || failure.equalsTo(.connectionFailed), - let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { + let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) { completion(.success(localObjects)) } else { completion(.failure(failure)) @@ -748,7 +748,7 @@ extension Query: Queryable { } } catch { if let urlError = error as? URLError, - urlError.code == URLError.Code.notConnectedToInternet || urlError.code == URLError.Code.dataNotAllowed, let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { + urlError.code == URLError.Code.notConnectedToInternet || urlError.code == URLError.Code.dataNotAllowed, let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) { completion(.success(localObjects)) } else { let defaultError = ParseError(code: .unknownError, @@ -756,7 +756,7 @@ extension Query: Queryable { let parseError = error as? ParseError ?? defaultError if parseError.equalsTo(.notConnectedToInternet) || parseError.equalsTo(.connectionFailed), - let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { + let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) { completion(.success(localObjects)) } else { callbackQueue.async { @@ -791,7 +791,23 @@ extension Query: Queryable { throw ParseError(code: .objectNotFound, message: "Object not found on the server.") } - return try firstCommand().execute(options: options) + if useLocalStore { + do { + let objects = try firstCommand().execute(options: options) + try? objects.saveLocally(queryIdentifier: queryIdentifier) + + return objects + } catch let parseError { + if parseError.equalsTo(.notConnectedToInternet) || parseError.equalsTo(.connectionFailed), + let localObject = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { + return localObject + } else { + throw parseError + } + } + } else { + return try firstCommand().execute(options: options) + } } /** @@ -846,8 +862,24 @@ extension Query: Queryable { } do { try firstCommand().executeAsync(options: options, - callbackQueue: callbackQueue) { result in - completion(result) + callbackQueue: callbackQueue) { result in + if useLocalStore { + switch result { + case .success(let object): + try? object.saveLocally(queryIdentifier: queryIdentifier) + + completion(result) + case .failure(let failure): + if failure.equalsTo(.notConnectedToInternet) || failure.equalsTo(.connectionFailed), + let localObject = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { + completion(.success(localObject)) + } else { + completion(.failure(failure)) + } + } + } else { + completion(result) + } } } catch { let parseError = ParseError(code: .unknownError, From 0e4410937dea98548409683ad3487c6659bb7d79 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Thu, 22 Dec 2022 20:38:33 +0100 Subject: [PATCH 33/73] Save object --- Sources/ParseSwift/Objects/ParseObject+async.swift | 4 ++++ Sources/ParseSwift/Objects/ParseObject.swift | 14 ++++++-------- Sources/ParseSwift/Storage/LocalStorage.swift | 8 ++++---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseObject+async.swift b/Sources/ParseSwift/Objects/ParseObject+async.swift index de7f30c4b..f8be75483 100644 --- a/Sources/ParseSwift/Objects/ParseObject+async.swift +++ b/Sources/ParseSwift/Objects/ParseObject+async.swift @@ -378,6 +378,8 @@ or disable transactions for this call. case .update: command = try self.updateCommand() } + + try? saveLocally(method: method) return try await command .executeAsync(options: options, callbackQueue: callbackQueue, @@ -387,6 +389,8 @@ or disable transactions for this call. let defaultError = ParseError(code: .unknownError, message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError + + try? saveLocally(method: method, error: parseError) throw parseError } } diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 0ca0fbe60..91948a9ff 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -1209,7 +1209,6 @@ extension ParseObject { #if compiler(>=5.5.2) && canImport(_Concurrency) Task { do { - try? saveLocally(method: method) let object = try await command(method: method, ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig, options: options, @@ -1226,7 +1225,6 @@ extension ParseObject { } } #else - try? saveLocally(method: method) command(method: method, ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig, options: options, @@ -1252,7 +1250,6 @@ extension ParseObject { #if compiler(>=5.5.2) && canImport(_Concurrency) Task { do { - try? saveLocally(method: method) let object = try await command(method: method, options: options, callbackQueue: callbackQueue) @@ -1267,7 +1264,6 @@ extension ParseObject { } } #else - try? saveLocally(method: method) command(method: method, options: options, callbackQueue: callbackQueue, @@ -1292,7 +1288,6 @@ extension ParseObject { #if compiler(>=5.5.2) && canImport(_Concurrency) Task { do { - try? saveLocally(method: method) let object = try await command(method: method, options: options, callbackQueue: callbackQueue) @@ -1307,7 +1302,6 @@ extension ParseObject { } } #else - try? saveLocally(method: method) command(method: method, options: options, callbackQueue: callbackQueue, @@ -1332,7 +1326,6 @@ extension ParseObject { #if compiler(>=5.5.2) && canImport(_Concurrency) Task { do { - try? saveLocally(method: method) let object = try await command(method: method, options: options, callbackQueue: callbackQueue) @@ -1347,7 +1340,6 @@ extension ParseObject { } } #else - try? saveLocally(method: method) command(method: method, options: options, callbackQueue: callbackQueue, @@ -1374,6 +1366,8 @@ extension ParseObject { case .update: command = try self.updateCommand() } + + try? saveLocally(method: method) command .executeAsync(options: options, callbackQueue: callbackQueue, @@ -1384,12 +1378,16 @@ extension ParseObject { let defaultError = ParseError(code: .unknownError, message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError + + try? saveLocally(method: method, error: parseError) callbackQueue.async { completion(.failure(parseError)) } } return } + + try? saveLocally(method: method, error: parseError) callbackQueue.async { completion(.failure(parseError)) } diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 202c0ffac..bff698c69 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -12,7 +12,6 @@ internal struct LocalStorage { static func save(_ object: T, queryIdentifier: String?) throws { let fileManager = FileManager.default - print("[LocalStorage] save object") let objectData = try ParseCoding.jsonEncoder().encode(object) guard let objectId = object.objectId else { @@ -36,7 +35,6 @@ internal struct LocalStorage { static func saveAll(_ objects: [T], queryIdentifier: String?) throws { let fileManager = FileManager.default - print("[LocalStorage] save objects") var successObjects: [T] = [] for object in objects { @@ -145,7 +143,8 @@ internal struct QueryObject: Codable { internal extension ParseObject { func saveLocally(method: Method? = nil, - queryIdentifier: String? = nil) throws { + queryIdentifier: String? = nil, + error: ParseError? = nil) throws { if let method = method { switch method { case .save: @@ -180,7 +179,8 @@ internal extension ParseObject { internal extension Sequence where Element: ParseObject { func saveLocally(method: Method? = nil, - queryIdentifier: String? = nil) throws { + queryIdentifier: String? = nil, + error: ParseError? = nil) throws { let objects = map { $0 } if let method = method { From 471863c064bd123263509b5c2045b129c9c1c750 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Thu, 22 Dec 2022 23:13:57 +0100 Subject: [PATCH 34/73] saveLocally --- Sources/ParseSwift/Objects/ParseObject+async.swift | 4 ++++ Sources/ParseSwift/Objects/ParseObject.swift | 2 ++ Sources/ParseSwift/Types/ParseError.swift | 12 ++++++++++++ 3 files changed, 18 insertions(+) diff --git a/Sources/ParseSwift/Objects/ParseObject+async.swift b/Sources/ParseSwift/Objects/ParseObject+async.swift index f8be75483..6c0ed95eb 100644 --- a/Sources/ParseSwift/Objects/ParseObject+async.swift +++ b/Sources/ParseSwift/Objects/ParseObject+async.swift @@ -462,11 +462,15 @@ internal extension Sequence where Element: ParseObject { childFiles: childFiles) returnBatch.append(contentsOf: saved) } + + try? saveLocally(method: method) return returnBatch } catch { let defaultError = ParseError(code: .unknownError, message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError + + try? saveLocally(method: method, error: parseError) throw parseError } } diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 91948a9ff..b3150afb6 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -800,10 +800,12 @@ transactions for this call. case .success(let saved): returnBatch.append(contentsOf: saved) if completed == (batches.count - 1) { + try? saveLocally(method: method) completion(.success(returnBatch)) } completed += 1 case .failure(let error): + try? saveLocally(method: method, error: error) completion(.failure(error)) return } diff --git a/Sources/ParseSwift/Types/ParseError.swift b/Sources/ParseSwift/Types/ParseError.swift index b2b999f47..1ce4d4c9b 100644 --- a/Sources/ParseSwift/Types/ParseError.swift +++ b/Sources/ParseSwift/Types/ParseError.swift @@ -563,3 +563,15 @@ public extension Error { containedIn(errorCodes) } } + +internal extension Error { + + /** + Validates if the given `ParseError` codes contains the error codes for no internet connection. + + - returns: A boolean indicating whether or not the `Error` is an internet connection error. + */ + var hasNoInternetConnection: Bool { + return self.equalsTo(.notConnectedToInternet) || self.equalsTo(.connectionFailed) + } +} From 56c63432b6a5255389d77d12a714cde91cb0786f Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Thu, 22 Dec 2022 23:15:11 +0100 Subject: [PATCH 35/73] hasNoInternetConnection --- Sources/ParseSwift/Types/Query.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 1f4337386..01ca9f26e 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -551,7 +551,7 @@ extension Query: Queryable { return objects } catch let parseError { - if parseError.equalsTo(.notConnectedToInternet) || parseError.equalsTo(.connectionFailed), + if parseError.hasNoInternetConnection, let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) { return localObjects } else { @@ -617,7 +617,7 @@ extension Query: Queryable { completion(result) case .failure(let failure): - if failure.equalsTo(.notConnectedToInternet) || failure.equalsTo(.connectionFailed), + if failure.hasNoInternetConnection, let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) { completion(.success(localObjects)) } else { @@ -755,7 +755,7 @@ extension Query: Queryable { message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError - if parseError.equalsTo(.notConnectedToInternet) || parseError.equalsTo(.connectionFailed), + if parseError.hasNoInternetConnection, let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) { completion(.success(localObjects)) } else { @@ -798,7 +798,7 @@ extension Query: Queryable { return objects } catch let parseError { - if parseError.equalsTo(.notConnectedToInternet) || parseError.equalsTo(.connectionFailed), + if parseError.hasNoInternetConnection, let localObject = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { return localObject } else { @@ -870,7 +870,7 @@ extension Query: Queryable { completion(result) case .failure(let failure): - if failure.equalsTo(.notConnectedToInternet) || failure.equalsTo(.connectionFailed), + if failure.hasNoInternetConnection, let localObject = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { completion(.success(localObject)) } else { From 6ac8a93332806e9ada17a0eb8600730ffbfdb10e Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 23 Dec 2022 00:55:45 +0100 Subject: [PATCH 36/73] FetchObject --- Sources/ParseSwift/ParseConstants.swift | 3 +- Sources/ParseSwift/Storage/LocalStorage.swift | 60 ++++++++++++++++++- Sources/ParseSwift/Types/Query.swift | 2 +- 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift index a86679262..15b56842c 100644 --- a/Sources/ParseSwift/ParseConstants.swift +++ b/Sources/ParseSwift/ParseConstants.swift @@ -16,6 +16,7 @@ enum ParseConstants { static let fileManagementLibraryDirectory = "Library/" static let fileDownloadsDirectory = "Downloads" static let fileObjectsDirectory = "Objects" + static let fetchObjectsFile = "FetchObjects" static let queryObjectsFile = "QueryObjects" static let bundlePrefix = "com.parse.ParseSwift" static let batchLimit = 50 @@ -37,7 +38,7 @@ enum ParseConstants { #endif } -enum Method: String { +enum Method: String, Codable { case save, create, replace, update } diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index bff698c69..0b1710a33 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -91,6 +91,39 @@ internal struct LocalStorage { return (allObjects.isEmpty ? nil : allObjects) } + static func saveFetchObjects(_ objects: [T], + method: Method) throws { + let fileManager = FileManager.default + + let objectsDirectoryPath = try ParseFileManager.objectsDirectory() + let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile) + + var fetchObjects = try getFetchObjects() + fetchObjects.append(contentsOf: try objects.map({ try FetchObject($0, method: method) })) + + let jsonData = try ParseCoding.jsonEncoder().encode(fetchObjects) + + if fileManager.fileExists(atPath: fetchObjectsPath.path) { + try jsonData.write(to: fetchObjectsPath) + } else { + fileManager.createFile(atPath: fetchObjectsPath.path, contents: jsonData, attributes: nil) + } + } + + static func getFetchObjects() throws -> [FetchObject] { + let fileManager = FileManager.default + + let objectsDirectoryPath = try ParseFileManager.objectsDirectory() + let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile) + + if fileManager.fileExists(atPath: fetchObjectsPath.path) { + let jsonData = try Data(contentsOf: fetchObjectsPath) + return try ParseCoding.jsonDecoder().decode([FetchObject].self, from: jsonData) + } else { + return [] + } + } + static func saveQueryObjects(_ objects: [T], queryIdentifier: String) throws { let fileManager = FileManager.default @@ -125,6 +158,23 @@ internal struct LocalStorage { } } +internal struct FetchObject: Codable { + let objectId: String + let className: String + let updatedAt: Date + let method: Method + + init(_ object : T, method: Method) throws { + guard let objectId = object.objectId else { + throw ParseError(code: .missingObjectId, message: "Object has no valid objectId") + } + self.objectId = objectId + self.className = object.className + self.updatedAt = object.updatedAt ?? Date() + self.method = method + } +} + internal struct QueryObject: Codable { let objectId: String let className: String @@ -132,7 +182,7 @@ internal struct QueryObject: Codable { init(_ object : T) throws { guard let objectId = object.objectId else { - throw ParseError(code: .unknownError, message: "Object has no valid objectId") + throw ParseError(code: .missingObjectId, message: "Object has no valid objectId") } self.objectId = objectId self.className = object.className @@ -173,6 +223,10 @@ internal extension ParseObject { try LocalStorage.save(self, queryIdentifier: queryIdentifier) } } + + if let method = method, let error = error, error.hasNoInternetConnection { + try LocalStorage.saveFetchObjects([self], method: method) + } } } @@ -207,5 +261,9 @@ internal extension Sequence where Element: ParseObject { try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } } + + if let method = method, let error = error, error.hasNoInternetConnection { + try LocalStorage.saveFetchObjects(objects, method: method) + } } } diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 01ca9f26e..baec0e92e 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -862,7 +862,7 @@ extension Query: Queryable { } do { try firstCommand().executeAsync(options: options, - callbackQueue: callbackQueue) { result in + callbackQueue: callbackQueue) { result in if useLocalStore { switch result { case .success(let object): From 6d7d95df4615c7e7e0bc54113d324d76a08263e2 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 23 Dec 2022 01:08:00 +0100 Subject: [PATCH 37/73] HiddenFile --- Sources/ParseSwift/Storage/LocalStorage.swift | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 0b1710a33..d27c3efa1 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -96,7 +96,7 @@ internal struct LocalStorage { let fileManager = FileManager.default let objectsDirectoryPath = try ParseFileManager.objectsDirectory() - let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile) + let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile) var fetchObjects = try getFetchObjects() fetchObjects.append(contentsOf: try objects.map({ try FetchObject($0, method: method) })) @@ -114,7 +114,7 @@ internal struct LocalStorage { let fileManager = FileManager.default let objectsDirectoryPath = try ParseFileManager.objectsDirectory() - let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile) + let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile) if fileManager.fileExists(atPath: fetchObjectsPath.path) { let jsonData = try Data(contentsOf: fetchObjectsPath) @@ -129,7 +129,7 @@ internal struct LocalStorage { let fileManager = FileManager.default let objectsDirectoryPath = try ParseFileManager.objectsDirectory() - let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile) + let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile.hiddenFile) var queryObjects = try getQueryObjects() queryObjects[queryIdentifier] = try objects.map({ try QueryObject($0) }) @@ -147,7 +147,7 @@ internal struct LocalStorage { let fileManager = FileManager.default let objectsDirectoryPath = try ParseFileManager.objectsDirectory() - let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile) + let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile.hiddenFile) if fileManager.fileExists(atPath: queryObjectsPath.path) { let jsonData = try Data(contentsOf: queryObjectsPath) @@ -158,6 +158,16 @@ internal struct LocalStorage { } } +fileprivate extension String { + + /** + Creates a hidden file + */ + var hiddenFile: Self { + return "." + self + } +} + internal struct FetchObject: Codable { let objectId: String let className: String From 1059b3bab99ecb4757e48e53956b009bbdeca25d Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 23 Dec 2022 11:43:00 +0100 Subject: [PATCH 38/73] uniqueObjectsById, Create fetchobjects when policy enabled --- Sources/ParseSwift/Storage/LocalStorage.swift | 58 ++++++++++++++++--- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index d27c3efa1..28b1741a8 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -118,7 +118,7 @@ internal struct LocalStorage { if fileManager.fileExists(atPath: fetchObjectsPath.path) { let jsonData = try Data(contentsOf: fetchObjectsPath) - return try ParseCoding.jsonDecoder().decode([FetchObject].self, from: jsonData) + return try ParseCoding.jsonDecoder().decode([FetchObject].self, from: jsonData).uniqueObjectsById } else { return [] } @@ -210,11 +210,19 @@ internal extension ParseObject { case .save: if Parse.configuration.offlinePolicy.enabled { try LocalStorage.save(self, queryIdentifier: queryIdentifier) + + if let error = error, error.hasNoInternetConnection { + try LocalStorage.saveFetchObjects([self], method: method) + } } case .create: if Parse.configuration.offlinePolicy.canCreate { if Parse.configuration.isRequiringCustomObjectIds { try LocalStorage.save(self, queryIdentifier: queryIdentifier) + + if let error = error, error.hasNoInternetConnection { + try LocalStorage.saveFetchObjects([self], method: method) + } } else { throw ParseError(code: .unknownError, message: "Enable custom objectIds") } @@ -222,10 +230,18 @@ internal extension ParseObject { case .replace: if Parse.configuration.offlinePolicy.enabled { try LocalStorage.save(self, queryIdentifier: queryIdentifier) + + if let error = error, error.hasNoInternetConnection { + try LocalStorage.saveFetchObjects([self], method: method) + } } case .update: if Parse.configuration.offlinePolicy.enabled { try LocalStorage.save(self, queryIdentifier: queryIdentifier) + + if let error = error, error.hasNoInternetConnection { + try LocalStorage.saveFetchObjects([self], method: method) + } } } } else { @@ -233,10 +249,6 @@ internal extension ParseObject { try LocalStorage.save(self, queryIdentifier: queryIdentifier) } } - - if let method = method, let error = error, error.hasNoInternetConnection { - try LocalStorage.saveFetchObjects([self], method: method) - } } } @@ -252,18 +264,38 @@ internal extension Sequence where Element: ParseObject { case .save: if Parse.configuration.offlinePolicy.enabled { try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) + + if let error = error, error.hasNoInternetConnection { + try LocalStorage.saveFetchObjects(objects, method: method) + } } case .create: if Parse.configuration.offlinePolicy.canCreate { - try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) + if Parse.configuration.isRequiringCustomObjectIds { + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) + + if let error = error, error.hasNoInternetConnection { + try LocalStorage.saveFetchObjects(objects, method: method) + } + } else { + throw ParseError(code: .unknownError, message: "Enable custom objectIds") + } } case .replace: if Parse.configuration.offlinePolicy.enabled { try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) + + if let error = error, error.hasNoInternetConnection { + try LocalStorage.saveFetchObjects(objects, method: method) + } } case .update: if Parse.configuration.offlinePolicy.enabled { try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) + + if let error = error, error.hasNoInternetConnection { + try LocalStorage.saveFetchObjects(objects, method: method) + } } } } else { @@ -271,9 +303,19 @@ internal extension Sequence where Element: ParseObject { try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } } + } +} + +fileprivate extension Sequence where Element == FetchObject { + + var uniqueObjectsById: [Element] { + let objects = map { $0 }.sorted(by: { $0.updatedAt > $1.updatedAt }) - if let method = method, let error = error, error.hasNoInternetConnection { - try LocalStorage.saveFetchObjects(objects, method: method) + var uniqueObjects: [Element] = [] + for object in objects { + uniqueObjects.append(objects.first(where: { $0.objectId == object.objectId }) ?? object) } + + return uniqueObjects.isEmpty ? objects : uniqueObjects } } From 799ed424c63e6c46a9411b145f2f1c90615c734d Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 23 Dec 2022 11:45:19 +0100 Subject: [PATCH 39/73] Cleanup --- Sources/ParseSwift/Storage/LocalStorage.swift | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 28b1741a8..ef1fe35bc 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -8,10 +8,10 @@ import Foundation internal struct LocalStorage { + static let fileManager = FileManager.default static func save(_ object: T, queryIdentifier: String?) throws { - let fileManager = FileManager.default let objectData = try ParseCoding.jsonEncoder().encode(object) guard let objectId = object.objectId else { @@ -34,8 +34,6 @@ internal struct LocalStorage { static func saveAll(_ objects: [T], queryIdentifier: String?) throws { - let fileManager = FileManager.default - var successObjects: [T] = [] for object in objects { let objectData = try ParseCoding.jsonEncoder().encode(object) @@ -93,8 +91,6 @@ internal struct LocalStorage { static func saveFetchObjects(_ objects: [T], method: Method) throws { - let fileManager = FileManager.default - let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile) @@ -111,8 +107,6 @@ internal struct LocalStorage { } static func getFetchObjects() throws -> [FetchObject] { - let fileManager = FileManager.default - let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile) @@ -126,8 +120,6 @@ internal struct LocalStorage { static func saveQueryObjects(_ objects: [T], queryIdentifier: String) throws { - let fileManager = FileManager.default - let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile.hiddenFile) @@ -144,8 +136,6 @@ internal struct LocalStorage { } static func getQueryObjects() throws -> [String : [QueryObject]] { - let fileManager = FileManager.default - let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile.hiddenFile) From 345993ad152cd56af714a55f514855f33acd29ea Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 23 Dec 2022 11:47:20 +0100 Subject: [PATCH 40/73] Comment --- Sources/ParseSwift/Storage/ParseFileManager.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ParseSwift/Storage/ParseFileManager.swift b/Sources/ParseSwift/Storage/ParseFileManager.swift index 81fc3609b..b8b3fab85 100644 --- a/Sources/ParseSwift/Storage/ParseFileManager.swift +++ b/Sources/ParseSwift/Storage/ParseFileManager.swift @@ -230,6 +230,7 @@ public extension ParseFileManager { /** The default directory for all `ParseObject`'s. + - parameter className: An optional value, that if set returns the objects directory for a specific class - returns: The objects directory. - throws: An error of type `ParseError`. */ From 4fb6971e9fc808473adb6f0ba45f734647721bd8 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 23 Dec 2022 11:48:30 +0100 Subject: [PATCH 41/73] Moved some code --- Sources/ParseSwift/Storage/LocalStorage.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index ef1fe35bc..4a9dcf49b 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -148,16 +148,6 @@ internal struct LocalStorage { } } -fileprivate extension String { - - /** - Creates a hidden file - */ - var hiddenFile: Self { - return "." + self - } -} - internal struct FetchObject: Codable { let objectId: String let className: String @@ -296,6 +286,16 @@ internal extension Sequence where Element: ParseObject { } } +fileprivate extension String { + + /** + Creates a hidden file + */ + var hiddenFile: Self { + return "." + self + } +} + fileprivate extension Sequence where Element == FetchObject { var uniqueObjectsById: [Element] { From 158a151a6b7469490e4e98d4519d4e1ece2d95cd Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 23 Dec 2022 11:57:48 +0100 Subject: [PATCH 42/73] Make sure new FetchObjects are unique --- Sources/ParseSwift/Storage/LocalStorage.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 4a9dcf49b..4b1b1bc9f 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -96,6 +96,7 @@ internal struct LocalStorage { var fetchObjects = try getFetchObjects() fetchObjects.append(contentsOf: try objects.map({ try FetchObject($0, method: method) })) + fetchObjects = fetchObjects.uniqueObjectsById let jsonData = try ParseCoding.jsonEncoder().encode(fetchObjects) @@ -298,6 +299,9 @@ fileprivate extension String { fileprivate extension Sequence where Element == FetchObject { + /** + Returns a unique array of `FetchObject`'s where each element is the most recent version of itself. + */ var uniqueObjectsById: [Element] { let objects = map { $0 }.sorted(by: { $0.updatedAt > $1.updatedAt }) From f3566eb286156664dec2de8f068b0ce9364384ef Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Sat, 24 Dec 2022 01:04:11 +0100 Subject: [PATCH 43/73] Fetching Local Store --- .../Objects/ParseObject+async.swift | 48 ++++-- Sources/ParseSwift/Objects/ParseObject.swift | 68 +++++++- Sources/ParseSwift/Storage/LocalStorage.swift | 152 +++++++++++++++++- 3 files changed, 243 insertions(+), 25 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseObject+async.swift b/Sources/ParseSwift/Objects/ParseObject+async.swift index 6c0ed95eb..8b0e8e111 100644 --- a/Sources/ParseSwift/Objects/ParseObject+async.swift +++ b/Sources/ParseSwift/Objects/ParseObject+async.swift @@ -42,9 +42,11 @@ public extension ParseObject { - throws: An error of type `ParseError`. */ @discardableResult func save(ignoringCustomObjectIdConfig: Bool = false, + ignoringLocalStore: Bool = false, options: API.Options = []) async throws -> Self { try await withCheckedThrowingContinuation { continuation in self.save(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig, + ignoringLocalStore: ignoringLocalStore, options: options, completion: continuation.resume) } @@ -56,9 +58,11 @@ public extension ParseObject { - returns: Returns the saved `ParseObject`. - throws: An error of type `ParseError`. */ - @discardableResult func create(options: API.Options = []) async throws -> Self { + @discardableResult func create(ignoringLocalStore: Bool = false, + options: API.Options = []) async throws -> Self { try await withCheckedThrowingContinuation { continuation in - self.create(options: options, + self.create(ignoringLocalStore: ignoringLocalStore, + options: options, completion: continuation.resume) } } @@ -69,9 +73,11 @@ public extension ParseObject { - returns: Returns the saved `ParseObject`. - throws: An error of type `ParseError`. */ - @discardableResult func replace(options: API.Options = []) async throws -> Self { + @discardableResult func replace(ignoringLocalStore: Bool = false, + options: API.Options = []) async throws -> Self { try await withCheckedThrowingContinuation { continuation in - self.replace(options: options, + self.replace(ignoringLocalStore: ignoringLocalStore, + options: options, completion: continuation.resume) } } @@ -81,10 +87,12 @@ public extension ParseObject { - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns the saved `ParseObject`. - throws: An error of type `ParseError`. - */ - @discardableResult internal func update(options: API.Options = []) async throws -> Self { + */ + @discardableResult internal func update(ignoringLocalStore: Bool = false, + options: API.Options = []) async throws -> Self { try await withCheckedThrowingContinuation { continuation in - self.update(options: options, + self.update(ignoringLocalStore: ignoringLocalStore, + options: options, completion: continuation.resume) } } @@ -159,11 +167,13 @@ public extension Sequence where Element: ParseObject { @discardableResult func saveAll(batchLimit limit: Int? = nil, transaction: Bool = configuration.isUsingTransactions, ignoringCustomObjectIdConfig: Bool = false, + ignoringLocalStore: Bool = false, options: API.Options = []) async throws -> [(Result)] { try await withCheckedThrowingContinuation { continuation in self.saveAll(batchLimit: limit, transaction: transaction, ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig, + ignoringLocalStore: ignoringLocalStore, options: options, completion: continuation.resume) } @@ -188,10 +198,12 @@ public extension Sequence where Element: ParseObject { */ @discardableResult func createAll(batchLimit limit: Int? = nil, transaction: Bool = configuration.isUsingTransactions, + ignoringLocalStore: Bool = false, options: API.Options = []) async throws -> [(Result)] { try await withCheckedThrowingContinuation { continuation in self.createAll(batchLimit: limit, transaction: transaction, + ignoringLocalStore: ignoringLocalStore, options: options, completion: continuation.resume) } @@ -216,10 +228,12 @@ public extension Sequence where Element: ParseObject { */ @discardableResult func replaceAll(batchLimit limit: Int? = nil, transaction: Bool = configuration.isUsingTransactions, + ignoringLocalStore: Bool = false, options: API.Options = []) async throws -> [(Result)] { try await withCheckedThrowingContinuation { continuation in self.replaceAll(batchLimit: limit, transaction: transaction, + ignoringLocalStore: ignoringLocalStore, options: options, completion: continuation.resume) } @@ -244,10 +258,12 @@ public extension Sequence where Element: ParseObject { */ internal func updateAll(batchLimit limit: Int? = nil, transaction: Bool = configuration.isUsingTransactions, + ignoringLocalStore: Bool = false, options: API.Options = []) async throws -> [(Result)] { try await withCheckedThrowingContinuation { continuation in self.updateAll(batchLimit: limit, transaction: transaction, + ignoringLocalStore: ignoringLocalStore, options: options, completion: continuation.resume) } @@ -363,6 +379,7 @@ or disable transactions for this call. func command(method: Method, ignoringCustomObjectIdConfig: Bool = false, + ignoringLocalStore: Bool = false, options: API.Options, callbackQueue: DispatchQueue) async throws -> Self { let (savedChildObjects, savedChildFiles) = try await self.ensureDeepSave(options: options) @@ -379,7 +396,9 @@ or disable transactions for this call. command = try self.updateCommand() } - try? saveLocally(method: method) + if !ignoringLocalStore { + try? saveLocally(method: method) + } return try await command .executeAsync(options: options, callbackQueue: callbackQueue, @@ -390,7 +409,9 @@ or disable transactions for this call. message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError - try? saveLocally(method: method, error: parseError) + if !ignoringLocalStore { + try? saveLocally(method: method, error: parseError) + } throw parseError } } @@ -402,6 +423,7 @@ internal extension Sequence where Element: ParseObject { batchLimit limit: Int?, transaction: Bool, ignoringCustomObjectIdConfig: Bool = false, + ignoringLocalStore: Bool = false, options: API.Options, callbackQueue: DispatchQueue) async throws -> [(Result)] { var options = options @@ -463,14 +485,18 @@ internal extension Sequence where Element: ParseObject { returnBatch.append(contentsOf: saved) } - try? saveLocally(method: method) + if !ignoringLocalStore { + try? saveLocally(method: method) + } return returnBatch } catch { let defaultError = ParseError(code: .unknownError, message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError - try? saveLocally(method: method, error: parseError) + if !ignoringLocalStore { + try? saveLocally(method: method, error: parseError) + } throw parseError } } diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index b3150afb6..7c8aa016a 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -478,6 +478,8 @@ transactions for this call. - parameter ignoringCustomObjectIdConfig: Ignore checking for `objectId` when `ParseConfiguration.isRequiringCustomObjectIds = true` to allow for mixed `objectId` environments. Defaults to false. + - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use + since default use should be set via policy on initialize. Defaults to false. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. @@ -501,6 +503,7 @@ transactions for this call. batchLimit limit: Int? = nil, transaction: Bool = configuration.isUsingTransactions, ignoringCustomObjectIdConfig: Bool = false, + ignoringLocalStore: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void @@ -513,6 +516,7 @@ transactions for this call. batchLimit: limit, transaction: transaction, ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue) completion(.success(objects)) @@ -530,6 +534,7 @@ transactions for this call. batchLimit: limit, transaction: transaction, ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue, completion: completion) @@ -543,6 +548,8 @@ transactions for this call. Defaults to 50. - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that prevents the transaction from completing, then none of the objects are committed to the Parse Server database. + - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use + since default use should be set via policy on initialize. Defaults to false. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. @@ -556,6 +563,7 @@ transactions for this call. func createAll( // swiftlint:disable:this function_body_length cyclomatic_complexity batchLimit limit: Int? = nil, transaction: Bool = configuration.isUsingTransactions, + ignoringLocalStore: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void @@ -567,6 +575,7 @@ transactions for this call. let objects = try await batchCommand(method: method, batchLimit: limit, transaction: transaction, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue) completion(.success(objects)) @@ -583,6 +592,7 @@ transactions for this call. batchCommand(method: method, batchLimit: limit, transaction: transaction, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue, completion: completion) @@ -596,6 +606,8 @@ transactions for this call. Defaults to 50. - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that prevents the transaction from completing, then none of the objects are committed to the Parse Server database. + - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use + since default use should be set via policy on initialize. Defaults to false. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. @@ -609,6 +621,7 @@ transactions for this call. func replaceAll( // swiftlint:disable:this function_body_length cyclomatic_complexity batchLimit limit: Int? = nil, transaction: Bool = configuration.isUsingTransactions, + ignoringLocalStore: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void @@ -620,6 +633,7 @@ transactions for this call. let objects = try await batchCommand(method: method, batchLimit: limit, transaction: transaction, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue) completion(.success(objects)) @@ -636,6 +650,7 @@ transactions for this call. batchCommand(method: method, batchLimit: limit, transaction: transaction, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue, completion: completion) @@ -649,6 +664,8 @@ transactions for this call. Defaults to 50. - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that prevents the transaction from completing, then none of the objects are committed to the Parse Server database. + - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use + since default use should be set via policy on initialize. Defaults to false. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. @@ -662,6 +679,7 @@ transactions for this call. internal func updateAll( // swiftlint:disable:this function_body_length cyclomatic_complexity batchLimit limit: Int? = nil, transaction: Bool = configuration.isUsingTransactions, + ignoringLocalStore: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void @@ -673,6 +691,7 @@ transactions for this call. let objects = try await batchCommand(method: method, batchLimit: limit, transaction: transaction, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue) completion(.success(objects)) @@ -689,6 +708,7 @@ transactions for this call. batchCommand(method: method, batchLimit: limit, transaction: transaction, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue, completion: completion) @@ -699,6 +719,7 @@ transactions for this call. batchLimit limit: Int?, transaction: Bool, ignoringCustomObjectIdConfig: Bool = false, + ignoringLocalStore: Bool = false, options: API.Options, callbackQueue: DispatchQueue, completion: @escaping (Result<[(Result)], ParseError>) -> Void) { @@ -800,12 +821,16 @@ transactions for this call. case .success(let saved): returnBatch.append(contentsOf: saved) if completed == (batches.count - 1) { - try? saveLocally(method: method) + if !ignoringLocalStore { + try? saveLocally(method: method) + } completion(.success(returnBatch)) } completed += 1 case .failure(let error): - try? saveLocally(method: method, error: error) + if !ignoringLocalStore { + try? saveLocally(method: method, error: error) + } completion(.failure(error)) return } @@ -1187,6 +1212,8 @@ extension ParseObject { - parameter ignoringCustomObjectIdConfig: Ignore checking for `objectId` when `ParseConfiguration.isRequiringCustomObjectIds = true` to allow for mixed `objectId` environments. Defaults to false. + - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use + since default use should be set via policy on initialize. Defaults to false. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. @@ -1203,6 +1230,7 @@ extension ParseObject { */ public func save( ignoringCustomObjectIdConfig: Bool = false, + ignoringLocalStore: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void @@ -1213,6 +1241,7 @@ extension ParseObject { do { let object = try await command(method: method, ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue) @@ -1229,6 +1258,7 @@ extension ParseObject { #else command(method: method, ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue, completion: completion) @@ -1237,13 +1267,16 @@ extension ParseObject { /** Creates the `ParseObject` *asynchronously* and executes the given callback block. - + + - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use + since default use should be set via policy on initialize. Defaults to false. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result)`. */ public func create( + ignoringLocalStore: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void @@ -1253,6 +1286,7 @@ extension ParseObject { Task { do { let object = try await command(method: method, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue) completion(.success(object)) @@ -1267,6 +1301,7 @@ extension ParseObject { } #else command(method: method, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue, completion: completion) @@ -1275,13 +1310,16 @@ extension ParseObject { /** Replaces the `ParseObject` *asynchronously* and executes the given callback block. - + + - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use + since default use should be set via policy on initialize. Defaults to false. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result)`. */ public func replace( + ignoringLocalStore: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void @@ -1291,6 +1329,7 @@ extension ParseObject { Task { do { let object = try await command(method: method, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue) completion(.success(object)) @@ -1305,6 +1344,7 @@ extension ParseObject { } #else command(method: method, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue, completion: completion) @@ -1313,13 +1353,16 @@ extension ParseObject { /** Updates the `ParseObject` *asynchronously* and executes the given callback block. - + + - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use + since default use should be set via policy on initialize. Defaults to false. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result)`. */ func update( + ignoringLocalStore: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void @@ -1329,6 +1372,7 @@ extension ParseObject { Task { do { let object = try await command(method: method, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue) completion(.success(object)) @@ -1343,6 +1387,7 @@ extension ParseObject { } #else command(method: method, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue, completion: completion) @@ -1351,6 +1396,7 @@ extension ParseObject { func command(method: Method, ignoringCustomObjectIdConfig: Bool = false, + ignoringLocalStore: Bool = false, options: API.Options, callbackQueue: DispatchQueue, completion: @escaping (Result) -> Void) { @@ -1369,7 +1415,9 @@ extension ParseObject { command = try self.updateCommand() } - try? saveLocally(method: method) + if !ignoringLocalStore { + try? saveLocally(method: method) + } command .executeAsync(options: options, callbackQueue: callbackQueue, @@ -1381,7 +1429,9 @@ extension ParseObject { message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError - try? saveLocally(method: method, error: parseError) + if !ignoringLocalStore { + try? saveLocally(method: method, error: parseError) + } callbackQueue.async { completion(.failure(parseError)) } @@ -1389,7 +1439,9 @@ extension ParseObject { return } - try? saveLocally(method: method, error: parseError) + if !ignoringLocalStore { + try? saveLocally(method: method, error: parseError) + } callbackQueue.async { completion(.failure(parseError)) } diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 4b1b1bc9f..1da9dff69 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -7,6 +7,21 @@ import Foundation +public extension ParseObject { + + /** + Fetch all local objects. + + - returns: If objects are more recent on the database, it will replace the local objects and return them. + + - note: You will need to run this on every `ParseObject` that needs to fetch it's local objects + after creating offline objects. + */ + @discardableResult static func fetchLocalStore(_ type: T.Type) async throws -> [T]? { + return try await LocalStorage.fetchLocalObjects(type) + } +} + internal struct LocalStorage { static let fileManager = FileManager.default @@ -147,6 +162,94 @@ internal struct LocalStorage { return [:] } } + + /** + Fetch all local objects. + + - returns: If objects are more recent on the database, it will replace the local objects and return them. + */ + @discardableResult static func fetchLocalObjects(_ type: T.Type) async throws -> [T]? { + let fetchObjects = try getFetchObjects() + if fetchObjects.isEmpty { + return nil + } + + var saveObjects = try fetchObjects + .filter({ $0.method == .save }) + .asParseObjects(type) + var createObjects = try fetchObjects + .filter({ $0.method == .create }) + .asParseObjects(type) + var replaceObjects = try fetchObjects + .filter({ $0.method == .replace }) + .asParseObjects(type) + var updateObjects = try fetchObjects + .filter({ $0.method == .update }) + .asParseObjects(type) + + var cloudObjects: [T] = [] + + if Parse.configuration.offlinePolicy.enabled { + try await self.fetchLocalStore(.save, objects: &saveObjects, cloudObjects: &cloudObjects) + } + + if Parse.configuration.offlinePolicy.canCreate { + if Parse.configuration.isRequiringCustomObjectIds { + try await self.fetchLocalStore(.create, objects: &createObjects, cloudObjects: &cloudObjects) + } else { + assertionFailure("Enable custom objectIds") + } + } + + if Parse.configuration.offlinePolicy.enabled { + try await self.fetchLocalStore(.replace, objects: &replaceObjects, cloudObjects: &cloudObjects) + } + + if Parse.configuration.offlinePolicy.enabled { + try await self.fetchLocalStore(.update, objects: &updateObjects, cloudObjects: &cloudObjects) + } + + if cloudObjects.isEmpty { + return nil + } else { + try self.saveAll(cloudObjects, queryIdentifier: nil) + return cloudObjects + } + } + + private static func fetchLocalStore(_ method: Method, objects: inout [T], cloudObjects: inout [T]) async throws { + let queryObjects = T.query() + .where(containedIn(key: "objectId", array: objects.map({ $0.objectId }))) + .useLocalStore(false) + let foundObjects = try? await queryObjects.find() + + for object in objects { + if let matchingObject = foundObjects?.first(where: { $0.objectId == object.objectId }) { + if let objectUpdatedAt = object.updatedAt { + if let matchingObjectUpdatedAt = matchingObject.updatedAt { + if objectUpdatedAt < matchingObjectUpdatedAt { + objects.removeAll(where: { $0.objectId == matchingObject.objectId }) + cloudObjects.append(matchingObject) + } + } + } else { + objects.removeAll(where: { $0.objectId == matchingObject.objectId }) + cloudObjects.append(matchingObject) + } + } + } + + switch method { + case .save: + try await objects.saveAll(ignoringLocalStore: true) + case .create: + try await objects.createAll(ignoringLocalStore: true) + case .replace: + try await objects.replaceAll(ignoringLocalStore: true) + case .update: + _ = try await objects.updateAll(ignoringLocalStore: true) + } + } } internal struct FetchObject: Codable { @@ -205,7 +308,7 @@ internal extension ParseObject { try LocalStorage.saveFetchObjects([self], method: method) } } else { - throw ParseError(code: .unknownError, message: "Enable custom objectIds") + assertionFailure("Enable custom objectIds") } } case .replace: @@ -259,7 +362,7 @@ internal extension Sequence where Element: ParseObject { try LocalStorage.saveFetchObjects(objects, method: method) } } else { - throw ParseError(code: .unknownError, message: "Enable custom objectIds") + assertionFailure("Enable custom objectIds") } } case .replace: @@ -303,13 +406,50 @@ fileprivate extension Sequence where Element == FetchObject { Returns a unique array of `FetchObject`'s where each element is the most recent version of itself. */ var uniqueObjectsById: [Element] { - let objects = map { $0 }.sorted(by: { $0.updatedAt > $1.updatedAt }) + let fetchObjects = map { $0 }.sorted(by: { $0.updatedAt > $1.updatedAt }) var uniqueObjects: [Element] = [] - for object in objects { - uniqueObjects.append(objects.first(where: { $0.objectId == object.objectId }) ?? object) + for fetchObject in fetchObjects { + uniqueObjects.append(fetchObjects.first(where: { $0.objectId == fetchObject.objectId }) ?? fetchObject) + } + + return uniqueObjects.isEmpty ? fetchObjects : uniqueObjects + } + + func asParseObjects(_ type: T.Type) throws -> [T] { + let fileManager = FileManager.default + + let fetchObjectIds = map { $0 }.filter({ $0.className == T.className }).map({ $0.objectId }) + + let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: T.className) + let directoryObjectIds = try fileManager.contentsOfDirectory(atPath: objectsDirectoryPath.path) + + var objects: [T] = [] + + for directoryObjectId in directoryObjectIds { + if fetchObjectIds.contains(directoryObjectId) { + if #available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *) { + let contentPath = objectsDirectoryPath.appending(component: directoryObjectId, directoryHint: .notDirectory) + + if fileManager.fileExists(atPath: contentPath.path) { + let jsonData = try Data(contentsOf: contentPath) + let object = try ParseCoding.jsonDecoder().decode(T.self, from: jsonData) + + objects.append(object) + } + } else { + let contentPath = objectsDirectoryPath.appendingPathComponent(directoryObjectId, isDirectory: false) + + if fileManager.fileExists(atPath: contentPath.path) { + let jsonData = try Data(contentsOf: contentPath) + let object = try ParseCoding.jsonDecoder().decode(T.self, from: jsonData) + + objects.append(object) + } + } + } } - return uniqueObjects.isEmpty ? objects : uniqueObjects + return objects } } From 5ba3892380cb39028d871388e0f9a0a6085767de Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Sun, 25 Dec 2022 12:06:53 +0100 Subject: [PATCH 44/73] Print --- Sources/ParseSwift/Objects/ParseObject+async.swift | 5 +++++ Sources/ParseSwift/Objects/ParseObject.swift | 1 + 2 files changed, 6 insertions(+) diff --git a/Sources/ParseSwift/Objects/ParseObject+async.swift b/Sources/ParseSwift/Objects/ParseObject+async.swift index 8b0e8e111..24c3804b2 100644 --- a/Sources/ParseSwift/Objects/ParseObject+async.swift +++ b/Sources/ParseSwift/Objects/ParseObject+async.swift @@ -383,6 +383,7 @@ or disable transactions for this call. options: API.Options, callbackQueue: DispatchQueue) async throws -> Self { let (savedChildObjects, savedChildFiles) = try await self.ensureDeepSave(options: options) + print("1") do { let command: API.Command! switch method { @@ -396,7 +397,9 @@ or disable transactions for this call. command = try self.updateCommand() } + print("2") if !ignoringLocalStore { + print("3") try? saveLocally(method: method) } return try await command @@ -405,11 +408,13 @@ or disable transactions for this call. childObjects: savedChildObjects, childFiles: savedChildFiles) } catch { + print("4") let defaultError = ParseError(code: .unknownError, message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError if !ignoringLocalStore { + print("5") try? saveLocally(method: method, error: parseError) } throw parseError diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 7c8aa016a..8c98d9635 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -1235,6 +1235,7 @@ extension ParseObject { callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void ) { + print("01") let method = Method.save #if compiler(>=5.5.2) && canImport(_Concurrency) Task { From 0e258f44f53e02ca59a7b49936080e7116e1adf3 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Sun, 25 Dec 2022 12:22:14 +0100 Subject: [PATCH 45/73] Error change --- Sources/ParseSwift/Storage/LocalStorage.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 1da9dff69..c7853a09d 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -30,7 +30,7 @@ internal struct LocalStorage { let objectData = try ParseCoding.jsonEncoder().encode(object) guard let objectId = object.objectId else { - throw ParseError(code: .unknownError, message: "Object has no valid objectId") + throw ParseError(code: .missingObjectId, message: "Object has no valid objectId") } let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: object.className) @@ -53,7 +53,7 @@ internal struct LocalStorage { for object in objects { let objectData = try ParseCoding.jsonEncoder().encode(object) guard let objectId = object.objectId else { - throw ParseError(code: .unknownError, message: "Object has no valid objectId") + throw ParseError(code: .missingObjectId, message: "Object has no valid objectId") } let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: object.className) From 2c0dbf1acee4e54af69fbcc90441147e0a870785 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Sun, 25 Dec 2022 16:20:11 +0100 Subject: [PATCH 46/73] Change function --- Sources/ParseSwift/Storage/LocalStorage.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index c7853a09d..bb005eb51 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -17,8 +17,8 @@ public extension ParseObject { - note: You will need to run this on every `ParseObject` that needs to fetch it's local objects after creating offline objects. */ - @discardableResult static func fetchLocalStore(_ type: T.Type) async throws -> [T]? { - return try await LocalStorage.fetchLocalObjects(type) + @discardableResult static func fetchLocalStore() async throws -> [T]? { + return try await LocalStorage.fetchLocalObjects() } } @@ -168,7 +168,7 @@ internal struct LocalStorage { - returns: If objects are more recent on the database, it will replace the local objects and return them. */ - @discardableResult static func fetchLocalObjects(_ type: T.Type) async throws -> [T]? { + @discardableResult static func fetchLocalObjects() async throws -> [T]? { let fetchObjects = try getFetchObjects() if fetchObjects.isEmpty { return nil @@ -176,16 +176,16 @@ internal struct LocalStorage { var saveObjects = try fetchObjects .filter({ $0.method == .save }) - .asParseObjects(type) + .asParseObjects(T.self) var createObjects = try fetchObjects .filter({ $0.method == .create }) - .asParseObjects(type) + .asParseObjects(T.self) var replaceObjects = try fetchObjects .filter({ $0.method == .replace }) - .asParseObjects(type) + .asParseObjects(T.self) var updateObjects = try fetchObjects .filter({ $0.method == .update }) - .asParseObjects(type) + .asParseObjects(T.self) var cloudObjects: [T] = [] From daa251d25f4eea5a8b341c13bdd8bcf90a137e96 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Sun, 25 Dec 2022 16:26:47 +0100 Subject: [PATCH 47/73] Revert --- Sources/ParseSwift/Storage/LocalStorage.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index bb005eb51..c7853a09d 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -17,8 +17,8 @@ public extension ParseObject { - note: You will need to run this on every `ParseObject` that needs to fetch it's local objects after creating offline objects. */ - @discardableResult static func fetchLocalStore() async throws -> [T]? { - return try await LocalStorage.fetchLocalObjects() + @discardableResult static func fetchLocalStore(_ type: T.Type) async throws -> [T]? { + return try await LocalStorage.fetchLocalObjects(type) } } @@ -168,7 +168,7 @@ internal struct LocalStorage { - returns: If objects are more recent on the database, it will replace the local objects and return them. */ - @discardableResult static func fetchLocalObjects() async throws -> [T]? { + @discardableResult static func fetchLocalObjects(_ type: T.Type) async throws -> [T]? { let fetchObjects = try getFetchObjects() if fetchObjects.isEmpty { return nil @@ -176,16 +176,16 @@ internal struct LocalStorage { var saveObjects = try fetchObjects .filter({ $0.method == .save }) - .asParseObjects(T.self) + .asParseObjects(type) var createObjects = try fetchObjects .filter({ $0.method == .create }) - .asParseObjects(T.self) + .asParseObjects(type) var replaceObjects = try fetchObjects .filter({ $0.method == .replace }) - .asParseObjects(T.self) + .asParseObjects(type) var updateObjects = try fetchObjects .filter({ $0.method == .update }) - .asParseObjects(T.self) + .asParseObjects(type) var cloudObjects: [T] = [] From 3bfce97c3d8e8437441c2d904500481643979ec3 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Sun, 25 Dec 2022 16:54:25 +0100 Subject: [PATCH 48/73] Lint fix --- Sources/ParseSwift/Storage/LocalStorage.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index c7853a09d..584be6a74 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -48,7 +48,7 @@ internal struct LocalStorage { } static func saveAll(_ objects: [T], - queryIdentifier: String?) throws { + queryIdentifier: String?) throws { var successObjects: [T] = [] for object in objects { let objectData = try ParseCoding.jsonEncoder().encode(object) @@ -87,7 +87,7 @@ internal struct LocalStorage { } static func getAll(_ type: U.Type, - queryIdentifier: String) throws -> [U]? { + queryIdentifier: String) throws -> [U]? { guard let queryObjects = try getQueryObjects()[queryIdentifier] else { return nil } var allObjects: [U] = [] From 04826e07c9e01842dfc78563268c58c0c9ce6422 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Sun, 25 Dec 2022 18:04:58 +0100 Subject: [PATCH 49/73] Remove files if corrupted, Remove files if empty, code clean --- Sources/ParseSwift/Storage/LocalStorage.swift | 84 ++++++++++++++----- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 584be6a74..191ff5dff 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -104,7 +104,7 @@ internal struct LocalStorage { return (allObjects.isEmpty ? nil : allObjects) } - static func saveFetchObjects(_ objects: [T], + static fileprivate func saveFetchObjects(_ objects: [T], method: Method) throws { let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile) @@ -113,53 +113,95 @@ internal struct LocalStorage { fetchObjects.append(contentsOf: try objects.map({ try FetchObject($0, method: method) })) fetchObjects = fetchObjects.uniqueObjectsById - let jsonData = try ParseCoding.jsonEncoder().encode(fetchObjects) + try self.writeFetchObjects(fetchObjects) + } + + static fileprivate func removeFetchObjects(_ objects: [T]) throws { + var fetchObjects = try getFetchObjects() + let objectIds = objects.compactMap({ $0.objectId }) + fetchObjects.removeAll(where: { removableObject in + objectIds.contains(where: { currentObjectId in + removableObject.objectId == currentObjectId + }) + }) + fetchObjects = fetchObjects.uniqueObjectsById - if fileManager.fileExists(atPath: fetchObjectsPath.path) { - try jsonData.write(to: fetchObjectsPath) - } else { - fileManager.createFile(atPath: fetchObjectsPath.path, contents: jsonData, attributes: nil) - } + try self.writeFetchObjects(fetchObjects) } - static func getFetchObjects() throws -> [FetchObject] { + static fileprivate func getFetchObjects() throws -> [FetchObject] { let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile) if fileManager.fileExists(atPath: fetchObjectsPath.path) { let jsonData = try Data(contentsOf: fetchObjectsPath) - return try ParseCoding.jsonDecoder().decode([FetchObject].self, from: jsonData).uniqueObjectsById + do { + return try ParseCoding.jsonDecoder().decode([FetchObject].self, from: jsonData).uniqueObjectsById + } catch { + try fileManager.removeItem(at: fetchObjectsPath) + return [] + } } else { return [] } } - static func saveQueryObjects(_ objects: [T], - queryIdentifier: String) throws { + static private func writeFetchObjects(_ fetchObjects: [FetchObject]) throws { let objectsDirectoryPath = try ParseFileManager.objectsDirectory() - let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile.hiddenFile) + let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile) + if fetchObjects.isEmpty { + try fileManager.removeItem(at: fetchObjectsPath) + } else { + let jsonData = try ParseCoding.jsonEncoder().encode(fetchObjects) + + if fileManager.fileExists(atPath: fetchObjectsPath.path) { + try jsonData.write(to: fetchObjectsPath) + } else { + fileManager.createFile(atPath: fetchObjectsPath.path, contents: jsonData, attributes: nil) + } + } + } + + static fileprivate func saveQueryObjects(_ objects: [T], + queryIdentifier: String) throws { var queryObjects = try getQueryObjects() queryObjects[queryIdentifier] = try objects.map({ try QueryObject($0) }) - let jsonData = try ParseCoding.jsonEncoder().encode(queryObjects) + try self.writeQueryObjects(queryObjects) + } + + static fileprivate func getQueryObjects() throws -> [String : [QueryObject]] { + let objectsDirectoryPath = try ParseFileManager.objectsDirectory() + let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile.hiddenFile) if fileManager.fileExists(atPath: queryObjectsPath.path) { - try jsonData.write(to: queryObjectsPath) + let jsonData = try Data(contentsOf: queryObjectsPath) + do { + return try ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) + } catch { + try fileManager.removeItem(at: queryObjectsPath) + return [:] + } } else { - fileManager.createFile(atPath: queryObjectsPath.path, contents: jsonData, attributes: nil) + return [:] } } - static func getQueryObjects() throws -> [String : [QueryObject]] { + static private func writeQueryObjects(_ queryObjects: [String : [QueryObject]]) throws { let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile.hiddenFile) - if fileManager.fileExists(atPath: queryObjectsPath.path) { - let jsonData = try Data(contentsOf: queryObjectsPath) - return try ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) + if queryObjects.isEmpty { + try fileManager.removeItem(at: queryObjectsPath) } else { - return [:] + let jsonData = try ParseCoding.jsonEncoder().encode(queryObjects) + + if fileManager.fileExists(atPath: queryObjectsPath.path) { + try jsonData.write(to: queryObjectsPath) + } else { + fileManager.createFile(atPath: queryObjectsPath.path, contents: jsonData, attributes: nil) + } } } @@ -249,6 +291,8 @@ internal struct LocalStorage { case .update: _ = try await objects.updateAll(ignoringLocalStore: true) } + + try self.removeFetchObjects(objects) } } From ca175d06ca6bee6d642cff9192b6a202a082b243 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Mon, 26 Dec 2022 22:33:20 +0100 Subject: [PATCH 50/73] Fixes --- Sources/ParseSwift/Storage/LocalStorage.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 191ff5dff..0545036cc 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -106,9 +106,6 @@ internal struct LocalStorage { static fileprivate func saveFetchObjects(_ objects: [T], method: Method) throws { - let objectsDirectoryPath = try ParseFileManager.objectsDirectory() - let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile) - var fetchObjects = try getFetchObjects() fetchObjects.append(contentsOf: try objects.map({ try FetchObject($0, method: method) })) fetchObjects = fetchObjects.uniqueObjectsById @@ -151,7 +148,7 @@ internal struct LocalStorage { let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile) if fetchObjects.isEmpty { - try fileManager.removeItem(at: fetchObjectsPath) + try? fileManager.removeItem(at: fetchObjectsPath) } else { let jsonData = try ParseCoding.jsonEncoder().encode(fetchObjects) @@ -193,7 +190,7 @@ internal struct LocalStorage { let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile.hiddenFile) if queryObjects.isEmpty { - try fileManager.removeItem(at: queryObjectsPath) + try? fileManager.removeItem(at: queryObjectsPath) } else { let jsonData = try ParseCoding.jsonEncoder().encode(queryObjects) From 455c78b762f3b8832fae5563029832cdbc7072d4 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Tue, 27 Dec 2022 21:13:03 +0100 Subject: [PATCH 51/73] Fix: don't create when an error --- Sources/ParseSwift/Storage/LocalStorage.swift | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 0545036cc..0be0270c0 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -388,19 +388,25 @@ internal extension Sequence where Element: ParseObject { switch method { case .save: if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) - - if let error = error, error.hasNoInternetConnection { - try LocalStorage.saveFetchObjects(objects, method: method) + if let error = error { + if error.hasNoInternetConnection { + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) + try LocalStorage.saveFetchObjects(objects, method: method) + } + } else { + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } } case .create: if Parse.configuration.offlinePolicy.canCreate { if Parse.configuration.isRequiringCustomObjectIds { - try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) - - if let error = error, error.hasNoInternetConnection { - try LocalStorage.saveFetchObjects(objects, method: method) + if let error = error { + if error.hasNoInternetConnection { + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) + try LocalStorage.saveFetchObjects(objects, method: method) + } + } else { + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } } else { assertionFailure("Enable custom objectIds") @@ -408,18 +414,24 @@ internal extension Sequence where Element: ParseObject { } case .replace: if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) - - if let error = error, error.hasNoInternetConnection { - try LocalStorage.saveFetchObjects(objects, method: method) + if let error = error { + if error.hasNoInternetConnection { + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) + try LocalStorage.saveFetchObjects(objects, method: method) + } + } else { + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } } case .update: if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) - - if let error = error, error.hasNoInternetConnection { - try LocalStorage.saveFetchObjects(objects, method: method) + if let error = error { + if error.hasNoInternetConnection { + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) + try LocalStorage.saveFetchObjects(objects, method: method) + } + } else { + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } } } From 65843f797380460f61c1ac8ed7813a99d683ed48 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Tue, 27 Dec 2022 21:22:00 +0100 Subject: [PATCH 52/73] Fix --- Sources/ParseSwift/Storage/LocalStorage.swift | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 0be0270c0..12a49d5df 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -334,19 +334,25 @@ internal extension ParseObject { switch method { case .save: if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(self, queryIdentifier: queryIdentifier) - - if let error = error, error.hasNoInternetConnection { - try LocalStorage.saveFetchObjects([self], method: method) + if let error = error { + if error.hasNoInternetConnection { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + try LocalStorage.saveFetchObjects([self], method: method) + } + } else { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) } } case .create: if Parse.configuration.offlinePolicy.canCreate { if Parse.configuration.isRequiringCustomObjectIds { - try LocalStorage.save(self, queryIdentifier: queryIdentifier) - - if let error = error, error.hasNoInternetConnection { - try LocalStorage.saveFetchObjects([self], method: method) + if let error = error { + if error.hasNoInternetConnection { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + try LocalStorage.saveFetchObjects([self], method: method) + } + } else { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) } } else { assertionFailure("Enable custom objectIds") @@ -354,18 +360,24 @@ internal extension ParseObject { } case .replace: if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(self, queryIdentifier: queryIdentifier) - - if let error = error, error.hasNoInternetConnection { - try LocalStorage.saveFetchObjects([self], method: method) + if let error = error { + if error.hasNoInternetConnection { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + try LocalStorage.saveFetchObjects([self], method: method) + } + } else { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) } } case .update: if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(self, queryIdentifier: queryIdentifier) - - if let error = error, error.hasNoInternetConnection { - try LocalStorage.saveFetchObjects([self], method: method) + if let error = error { + if error.hasNoInternetConnection { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + try LocalStorage.saveFetchObjects([self], method: method) + } + } else { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) } } } From 25b8ea9811716dacbe68a1441a50de987ccbcf7b Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Tue, 27 Dec 2022 21:31:20 +0100 Subject: [PATCH 53/73] Print --- Sources/ParseSwift/Storage/LocalStorage.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 12a49d5df..10f9c12ac 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -334,6 +334,8 @@ internal extension ParseObject { switch method { case .save: if Parse.configuration.offlinePolicy.enabled { + print("error") + print(error) if let error = error { if error.hasNoInternetConnection { try LocalStorage.save(self, queryIdentifier: queryIdentifier) From 46c0dd5a4b9410b35427b6b8eef9c42d96938fbf Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Tue, 27 Dec 2022 21:48:25 +0100 Subject: [PATCH 54/73] Fix where save was executed before result --- Sources/ParseSwift/Objects/ParseObject+async.swift | 12 ++++++------ Sources/ParseSwift/Objects/ParseObject.swift | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseObject+async.swift b/Sources/ParseSwift/Objects/ParseObject+async.swift index 24c3804b2..f8de12f39 100644 --- a/Sources/ParseSwift/Objects/ParseObject+async.swift +++ b/Sources/ParseSwift/Objects/ParseObject+async.swift @@ -396,17 +396,17 @@ or disable transactions for this call. case .update: command = try self.updateCommand() } - + let commandResult = try await command + .executeAsync(options: options, + callbackQueue: callbackQueue, + childObjects: savedChildObjects, + childFiles: savedChildFiles) print("2") if !ignoringLocalStore { print("3") try? saveLocally(method: method) } - return try await command - .executeAsync(options: options, - callbackQueue: callbackQueue, - childObjects: savedChildObjects, - childFiles: savedChildFiles) + return commandResult } catch { print("4") let defaultError = ParseError(code: .unknownError, diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 8c98d9635..b2e2a8bcd 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -1415,16 +1415,16 @@ extension ParseObject { case .update: command = try self.updateCommand() } - - if !ignoringLocalStore { - try? saveLocally(method: method) - } command .executeAsync(options: options, callbackQueue: callbackQueue, childObjects: savedChildObjects, childFiles: savedChildFiles, completion: completion) + + if !ignoringLocalStore { + try? saveLocally(method: method) + } } catch { let defaultError = ParseError(code: .unknownError, message: error.localizedDescription) From a0428e97c944f1111b2097213cda41369254900c Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Tue, 27 Dec 2022 21:59:18 +0100 Subject: [PATCH 55/73] Cleanup --- Sources/ParseSwift/Objects/ParseObject+async.swift | 5 ----- Sources/ParseSwift/Objects/ParseObject.swift | 1 - Sources/ParseSwift/Storage/LocalStorage.swift | 2 -- 3 files changed, 8 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseObject+async.swift b/Sources/ParseSwift/Objects/ParseObject+async.swift index f8de12f39..3c487f385 100644 --- a/Sources/ParseSwift/Objects/ParseObject+async.swift +++ b/Sources/ParseSwift/Objects/ParseObject+async.swift @@ -383,7 +383,6 @@ or disable transactions for this call. options: API.Options, callbackQueue: DispatchQueue) async throws -> Self { let (savedChildObjects, savedChildFiles) = try await self.ensureDeepSave(options: options) - print("1") do { let command: API.Command! switch method { @@ -401,20 +400,16 @@ or disable transactions for this call. callbackQueue: callbackQueue, childObjects: savedChildObjects, childFiles: savedChildFiles) - print("2") if !ignoringLocalStore { - print("3") try? saveLocally(method: method) } return commandResult } catch { - print("4") let defaultError = ParseError(code: .unknownError, message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError if !ignoringLocalStore { - print("5") try? saveLocally(method: method, error: parseError) } throw parseError diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index b2e2a8bcd..9a0c32cde 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -1235,7 +1235,6 @@ extension ParseObject { callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void ) { - print("01") let method = Method.save #if compiler(>=5.5.2) && canImport(_Concurrency) Task { diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 10f9c12ac..12a49d5df 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -334,8 +334,6 @@ internal extension ParseObject { switch method { case .save: if Parse.configuration.offlinePolicy.enabled { - print("error") - print(error) if let error = error { if error.hasNoInternetConnection { try LocalStorage.save(self, queryIdentifier: queryIdentifier) From 9bf6c06357ed7091532326eb22ca03ae0031fa9d Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Tue, 27 Dec 2022 23:20:02 +0100 Subject: [PATCH 56/73] Offline Mode playground --- .../16 - Offline Mode.xcplaygroundpage/Contents.swift | 7 +++++++ .../Contents.swift | 0 .../Contents.swift | 0 .../Contents.swift | 0 .../Contents.swift | 0 .../Contents.swift | 0 .../Contents.swift | 0 .../Contents.swift | 0 .../Contents.swift | 0 9 files changed, 7 insertions(+) create mode 100644 ParseSwift.playground/Pages/16 - Offline Mode.xcplaygroundpage/Contents.swift rename ParseSwift.playground/Pages/{16 - Analytics.xcplaygroundpage => 17 - Analytics.xcplaygroundpage}/Contents.swift (100%) rename ParseSwift.playground/Pages/{17 - SwiftUI - Finding Objects.xcplaygroundpage => 18 - SwiftUI - Finding Objects.xcplaygroundpage}/Contents.swift (100%) rename ParseSwift.playground/Pages/{18 - SwiftUI - Finding Objects With Custom ViewModel.xcplaygroundpage => 19 - SwiftUI - Finding Objects With Custom ViewModel.xcplaygroundpage}/Contents.swift (100%) rename ParseSwift.playground/Pages/{19 - SwiftUI - LiveQuery.xcplaygroundpage => 20 - SwiftUI - LiveQuery.xcplaygroundpage}/Contents.swift (100%) rename ParseSwift.playground/Pages/{20 - Cloud Schemas.xcplaygroundpage => 21 - Cloud Schemas.xcplaygroundpage}/Contents.swift (100%) rename ParseSwift.playground/Pages/{21 - Cloud Push Notifications.xcplaygroundpage => 22 - Cloud Push Notifications.xcplaygroundpage}/Contents.swift (100%) rename ParseSwift.playground/Pages/{22 - Cloud Hook Functions.xcplaygroundpage => 23 - Cloud Hook Functions.xcplaygroundpage}/Contents.swift (100%) rename ParseSwift.playground/Pages/{23 - Cloud Hook Triggers.xcplaygroundpage => 24 - Cloud Hook Triggers.xcplaygroundpage}/Contents.swift (100%) diff --git a/ParseSwift.playground/Pages/16 - Offline Mode.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/16 - Offline Mode.xcplaygroundpage/Contents.swift new file mode 100644 index 000000000..5275afb6c --- /dev/null +++ b/ParseSwift.playground/Pages/16 - Offline Mode.xcplaygroundpage/Contents.swift @@ -0,0 +1,7 @@ +//: [Previous](@previous) + +import Foundation + +var greeting = "Hello, playground" + +//: [Next](@next) diff --git a/ParseSwift.playground/Pages/16 - Analytics.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/17 - Analytics.xcplaygroundpage/Contents.swift similarity index 100% rename from ParseSwift.playground/Pages/16 - Analytics.xcplaygroundpage/Contents.swift rename to ParseSwift.playground/Pages/17 - Analytics.xcplaygroundpage/Contents.swift diff --git a/ParseSwift.playground/Pages/17 - SwiftUI - Finding Objects.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/18 - SwiftUI - Finding Objects.xcplaygroundpage/Contents.swift similarity index 100% rename from ParseSwift.playground/Pages/17 - SwiftUI - Finding Objects.xcplaygroundpage/Contents.swift rename to ParseSwift.playground/Pages/18 - SwiftUI - Finding Objects.xcplaygroundpage/Contents.swift diff --git a/ParseSwift.playground/Pages/18 - SwiftUI - Finding Objects With Custom ViewModel.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/19 - SwiftUI - Finding Objects With Custom ViewModel.xcplaygroundpage/Contents.swift similarity index 100% rename from ParseSwift.playground/Pages/18 - SwiftUI - Finding Objects With Custom ViewModel.xcplaygroundpage/Contents.swift rename to ParseSwift.playground/Pages/19 - SwiftUI - Finding Objects With Custom ViewModel.xcplaygroundpage/Contents.swift diff --git a/ParseSwift.playground/Pages/19 - SwiftUI - LiveQuery.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/20 - SwiftUI - LiveQuery.xcplaygroundpage/Contents.swift similarity index 100% rename from ParseSwift.playground/Pages/19 - SwiftUI - LiveQuery.xcplaygroundpage/Contents.swift rename to ParseSwift.playground/Pages/20 - SwiftUI - LiveQuery.xcplaygroundpage/Contents.swift diff --git a/ParseSwift.playground/Pages/20 - Cloud Schemas.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/21 - Cloud Schemas.xcplaygroundpage/Contents.swift similarity index 100% rename from ParseSwift.playground/Pages/20 - Cloud Schemas.xcplaygroundpage/Contents.swift rename to ParseSwift.playground/Pages/21 - Cloud Schemas.xcplaygroundpage/Contents.swift diff --git a/ParseSwift.playground/Pages/21 - Cloud Push Notifications.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/22 - Cloud Push Notifications.xcplaygroundpage/Contents.swift similarity index 100% rename from ParseSwift.playground/Pages/21 - Cloud Push Notifications.xcplaygroundpage/Contents.swift rename to ParseSwift.playground/Pages/22 - Cloud Push Notifications.xcplaygroundpage/Contents.swift diff --git a/ParseSwift.playground/Pages/22 - Cloud Hook Functions.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/23 - Cloud Hook Functions.xcplaygroundpage/Contents.swift similarity index 100% rename from ParseSwift.playground/Pages/22 - Cloud Hook Functions.xcplaygroundpage/Contents.swift rename to ParseSwift.playground/Pages/23 - Cloud Hook Functions.xcplaygroundpage/Contents.swift diff --git a/ParseSwift.playground/Pages/23 - Cloud Hook Triggers.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/24 - Cloud Hook Triggers.xcplaygroundpage/Contents.swift similarity index 100% rename from ParseSwift.playground/Pages/23 - Cloud Hook Triggers.xcplaygroundpage/Contents.swift rename to ParseSwift.playground/Pages/24 - Cloud Hook Triggers.xcplaygroundpage/Contents.swift From 5f455dcec8541c7ca438adacac0db50f056a159b Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 28 Dec 2022 20:51:53 +0100 Subject: [PATCH 57/73] Fix --- Sources/ParseSwift/Objects/ParseObject.swift | 24 +++++++++++++++---- Sources/ParseSwift/Storage/LocalStorage.swift | 2 +- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 9a0c32cde..93ad9c24d 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -1180,7 +1180,9 @@ extension ParseObject { */ @discardableResult public func save(ignoringCustomObjectIdConfig: Bool = false, + ignoringLocalStore: Bool = false, options: API.Options = []) throws -> Self { + let method = Method.save var childObjects: [String: PointerType]? var childFiles: [UUID: ParseFile]? var error: ParseError? @@ -1197,13 +1199,27 @@ extension ParseObject { group.wait() if let error = error { + if !ignoringLocalStore { + try? saveLocally(method: method, error: error) + } throw error } - return try saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig) - .execute(options: options, - childObjects: childObjects, - childFiles: childFiles) + do { + let commandResult = try saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig) + .execute(options: options, + childObjects: childObjects, + childFiles: childFiles) + if !ignoringLocalStore { + try? saveLocally(method: method) + } + return commandResult + } catch { + if !ignoringLocalStore { + try? saveLocally(method: method, error: error) + } + throw error + } } /** diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 12a49d5df..310cdaa29 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -329,7 +329,7 @@ internal extension ParseObject { func saveLocally(method: Method? = nil, queryIdentifier: String? = nil, - error: ParseError? = nil) throws { + error: Error? = nil) throws { if let method = method { switch method { case .save: From 9e090cca243a73084f89b1d2b1476daed659ba9c Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 28 Dec 2022 20:56:41 +0100 Subject: [PATCH 58/73] Offline mode playground --- .../Contents.swift | 136 +++++++++++++++++- 1 file changed, 135 insertions(+), 1 deletion(-) diff --git a/ParseSwift.playground/Pages/16 - Offline Mode.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/16 - Offline Mode.xcplaygroundpage/Contents.swift index 5275afb6c..03e8a17aa 100644 --- a/ParseSwift.playground/Pages/16 - Offline Mode.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/16 - Offline Mode.xcplaygroundpage/Contents.swift @@ -1,7 +1,141 @@ //: [Previous](@previous) +//: For this page, make sure your build target is set to ParseSwift (macOS) and targeting +//: `My Mac` or whatever the name of your mac is. Also be sure your `Playground Settings` +//: in the `File Inspector` is `Platform = macOS`. This is because +//: Keychain in iOS Playgrounds behaves differently. Every page in Playgrounds should +//: be set to build for `macOS` unless specified. + +import PlaygroundSupport import Foundation +import ParseSwift +PlaygroundPage.current.needsIndefiniteExecution = true + + +//: In order to enable offline mode you need to set offlinePolicy to either `create` or `save` +//: `save` will allow you to save and fetch objects. +//: `create` will allow you to create, save and fetch objects. +//: Note that `create` will require you to enable customObjectIds. +ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: URL(string: "http://localhost:1337/1")!, + offlinePolicy: .create, + requiringCustomObjectIds: true, + usingEqualQueryConstraint: false, + usingDataProtectionKeychain: false) + +struct GameScore: ParseObject { + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + var originalData: Data? + + //: Your own properties. + var points: Int? + var timeStamp: Date? = Date() + var oldScore: Int? + var isHighest: Bool? + + /*: + Optional - implement your own version of merge + for faster decoding after updating your `ParseObject`. + */ + func merge(with object: Self) throws -> Self { + var updated = try mergeParse(with: object) + if updated.shouldRestoreKey(\.points, + original: object) { + updated.points = object.points + } + if updated.shouldRestoreKey(\.timeStamp, + original: object) { + updated.timeStamp = object.timeStamp + } + if updated.shouldRestoreKey(\.oldScore, + original: object) { + updated.oldScore = object.oldScore + } + if updated.shouldRestoreKey(\.isHighest, + original: object) { + updated.isHighest = object.isHighest + } + return updated + } +} + +var score = GameScore() +score.points = 200 +score.oldScore = 10 +score.isHighest = true +do { + try score.save() +} catch { + print(error) +} + +//: If you want to use local objects when an internet connection failed, +//: you need to set useLocalStore() +let afterDate = Date().addingTimeInterval(-300) +var query = GameScore.query("points" > 50, + "createdAt" > afterDate) + .useLocalStore() + .order([.descending("points")]) + +//: Query asynchronously (preferred way) - Performs work on background +//: queue and returns to specified callbackQueue. +//: If no callbackQueue is specified it returns to main queue. +query.limit(2) + .order([.descending("points")]) + .find(callbackQueue: .main) { results in + switch results { + case .success(let scores): + + assert(scores.count >= 1) + scores.forEach { score in + guard let createdAt = score.createdAt else { fatalError() } + assert(createdAt.timeIntervalSince1970 > afterDate.timeIntervalSince1970, "date should be ok") + print("Found score: \(score)") + } + + case .failure(let error): + if error.equalsTo(.objectNotFound) { + assertionFailure("Object not found for this query") + } else { + assertionFailure("Error querying: \(error)") + } + } +} + +//: Query synchronously (not preferred - all operations on current queue). +let results = try query.find() +assert(results.count >= 1) +results.forEach { score in + guard let createdAt = score.createdAt else { fatalError() } + assert(createdAt.timeIntervalSince1970 > afterDate.timeIntervalSince1970, "date should be ok") + print("Found score: \(score)") +} + +//: Query first asynchronously (preferred way) - Performs work on background +//: queue and returns to specified callbackQueue. +//: If no callbackQueue is specified it returns to main queue. +query.first { results in + switch results { + case .success(let score): + + guard score.objectId != nil, + let createdAt = score.createdAt else { fatalError() } + assert(createdAt.timeIntervalSince1970 > afterDate.timeIntervalSince1970, "date should be ok") + print("Found score: \(score)") -var greeting = "Hello, playground" + case .failure(let error): + if error.containedIn([.objectNotFound, .invalidQuery]) { + assertionFailure("The query is invalid or the object is not found.") + } else { + assertionFailure("Error querying: \(error)") + } + } +} +PlaygroundPage.current.finishExecution() //: [Next](@next) From 7a2d85358868591502a814a4bcabcd67fcece1e6 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Thu, 29 Dec 2022 22:42:09 +0100 Subject: [PATCH 59/73] Linting --- .../Contents.swift | 1 - ParseSwift.xcodeproj/project.pbxproj | 10 ++ .../Objects/ParseObject+async.swift | 6 +- Sources/ParseSwift/Objects/ParseObject.swift | 8 +- Sources/ParseSwift/Storage/LocalStorage.swift | 150 +++++++++--------- .../ParseSwift/Storage/ParseFileManager.swift | 6 +- .../ParseSwift/Types/ParseConfiguration.swift | 14 +- Sources/ParseSwift/Types/ParseError.swift | 2 +- Sources/ParseSwift/Types/Query.swift | 26 +-- 9 files changed, 116 insertions(+), 107 deletions(-) diff --git a/ParseSwift.playground/Pages/16 - Offline Mode.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/16 - Offline Mode.xcplaygroundpage/Contents.swift index 03e8a17aa..120149d23 100644 --- a/ParseSwift.playground/Pages/16 - Offline Mode.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/16 - Offline Mode.xcplaygroundpage/Contents.swift @@ -11,7 +11,6 @@ import Foundation import ParseSwift PlaygroundPage.current.needsIndefiniteExecution = true - //: In order to enable offline mode you need to set offlinePolicy to either `create` or `save` //: `save` will allow you to save and fetch objects. //: `create` will allow you to create, save and fetch objects. diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index e1ad03430..3d8db50ac 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -973,6 +973,10 @@ 91F346C3269B88F7005727B6 /* ParseCloudViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F346C2269B88F7005727B6 /* ParseCloudViewModelTests.swift */; }; 91F346C4269B88F7005727B6 /* ParseCloudViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F346C2269B88F7005727B6 /* ParseCloudViewModelTests.swift */; }; 91F346C5269B88F7005727B6 /* ParseCloudViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91F346C2269B88F7005727B6 /* ParseCloudViewModelTests.swift */; }; + CBEF514C295E40CB0052E598 /* LocalStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEF514B295E40CB0052E598 /* LocalStorage.swift */; }; + CBEF514D295E40CB0052E598 /* LocalStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEF514B295E40CB0052E598 /* LocalStorage.swift */; }; + CBEF514E295E40CB0052E598 /* LocalStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEF514B295E40CB0052E598 /* LocalStorage.swift */; }; + CBEF514F295E40CB0052E598 /* LocalStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEF514B295E40CB0052E598 /* LocalStorage.swift */; }; F971F4F624DE381A006CB79B /* ParseEncoderExtraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F971F4F524DE381A006CB79B /* ParseEncoderExtraTests.swift */; }; F97B45CE24D9C6F200F4A88B /* ParseCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45B424D9C6F200F4A88B /* ParseCoding.swift */; }; F97B45CF24D9C6F200F4A88B /* ParseCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45B424D9C6F200F4A88B /* ParseCoding.swift */; }; @@ -1449,6 +1453,7 @@ 91F346B8269B766C005727B6 /* CloudViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudViewModel.swift; sourceTree = ""; }; 91F346BD269B77B5005727B6 /* CloudObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudObservable.swift; sourceTree = ""; }; 91F346C2269B88F7005727B6 /* ParseCloudViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseCloudViewModelTests.swift; sourceTree = ""; }; + CBEF514B295E40CB0052E598 /* LocalStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalStorage.swift; sourceTree = ""; }; F971F4F524DE381A006CB79B /* ParseEncoderExtraTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseEncoderExtraTests.swift; sourceTree = ""; }; F97B45B424D9C6F200F4A88B /* ParseCoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseCoding.swift; sourceTree = ""; }; F97B45B524D9C6F200F4A88B /* AnyDecodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyDecodable.swift; sourceTree = ""; }; @@ -2225,6 +2230,7 @@ F97B45CB24D9C6F200F4A88B /* Storage */ = { isa = PBXGroup; children = ( + CBEF514B295E40CB0052E598 /* LocalStorage.swift */, F97B465E24D9C7B500F4A88B /* KeychainStore.swift */, 70572670259033A700F0ADD5 /* ParseFileManager.swift */, F97B45CC24D9C6F200F4A88B /* ParseStorage.swift */, @@ -2815,6 +2821,7 @@ F97B462F24D9C74400F4A88B /* BatchUtils.swift in Sources */, 70385E802858EAA90084D306 /* ParseHookFunctionRequest.swift in Sources */, 70CE0AAD28595FDE00DAEA86 /* ParseHookRequestable+combine.swift in Sources */, + CBEF514C295E40CB0052E598 /* LocalStorage.swift in Sources */, 4A82B7F61F254CCE0063D731 /* Parse.swift in Sources */, F97B45EA24D9C6F200F4A88B /* ParseGeoPoint.swift in Sources */, F97B460224D9C6F200F4A88B /* NoBody.swift in Sources */, @@ -3129,6 +3136,7 @@ 4AFDA72A1F26DAE1002AE4FC /* Parse.swift in Sources */, 70385E812858EAA90084D306 /* ParseHookFunctionRequest.swift in Sources */, 70CE0AAE28595FDE00DAEA86 /* ParseHookRequestable+combine.swift in Sources */, + CBEF514D295E40CB0052E598 /* LocalStorage.swift in Sources */, F97B45EB24D9C6F200F4A88B /* ParseGeoPoint.swift in Sources */, F97B460324D9C6F200F4A88B /* NoBody.swift in Sources */, 703B093126BF42C2005A112F /* ParseAnonymous+combine.swift in Sources */, @@ -3576,6 +3584,7 @@ F97B465924D9C78C00F4A88B /* Remove.swift in Sources */, 70385E832858EAA90084D306 /* ParseHookFunctionRequest.swift in Sources */, 70CE0AB028595FDE00DAEA86 /* ParseHookRequestable+combine.swift in Sources */, + CBEF514F295E40CB0052E598 /* LocalStorage.swift in Sources */, 70110D5A2506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45F924D9C6F200F4A88B /* ParseError.swift in Sources */, 703B093326BF42C2005A112F /* ParseAnonymous+combine.swift in Sources */, @@ -3766,6 +3775,7 @@ F97B465824D9C78C00F4A88B /* Remove.swift in Sources */, 70385E822858EAA90084D306 /* ParseHookFunctionRequest.swift in Sources */, 70CE0AAF28595FDE00DAEA86 /* ParseHookRequestable+combine.swift in Sources */, + CBEF514E295E40CB0052E598 /* LocalStorage.swift in Sources */, 70110D592506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45F824D9C6F200F4A88B /* ParseError.swift in Sources */, 703B093226BF42C2005A112F /* ParseAnonymous+combine.swift in Sources */, diff --git a/Sources/ParseSwift/Objects/ParseObject+async.swift b/Sources/ParseSwift/Objects/ParseObject+async.swift index 3c487f385..c70616285 100644 --- a/Sources/ParseSwift/Objects/ParseObject+async.swift +++ b/Sources/ParseSwift/Objects/ParseObject+async.swift @@ -408,7 +408,7 @@ or disable transactions for this call. let defaultError = ParseError(code: .unknownError, message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError - + if !ignoringLocalStore { try? saveLocally(method: method, error: parseError) } @@ -484,7 +484,7 @@ internal extension Sequence where Element: ParseObject { childFiles: childFiles) returnBatch.append(contentsOf: saved) } - + if !ignoringLocalStore { try? saveLocally(method: method) } @@ -493,7 +493,7 @@ internal extension Sequence where Element: ParseObject { let defaultError = ParseError(code: .unknownError, message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError - + if !ignoringLocalStore { try? saveLocally(method: method, error: parseError) } diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 93ad9c24d..0225f517c 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -1260,7 +1260,7 @@ extension ParseObject { ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue) - + completion(.success(object)) } catch { let defaultError = ParseError(code: .unknownError, @@ -1436,7 +1436,7 @@ extension ParseObject { childObjects: savedChildObjects, childFiles: savedChildFiles, completion: completion) - + if !ignoringLocalStore { try? saveLocally(method: method) } @@ -1444,7 +1444,7 @@ extension ParseObject { let defaultError = ParseError(code: .unknownError, message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError - + if !ignoringLocalStore { try? saveLocally(method: method, error: parseError) } @@ -1454,7 +1454,7 @@ extension ParseObject { } return } - + if !ignoringLocalStore { try? saveLocally(method: method, error: parseError) } diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 310cdaa29..8af95e811 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -8,7 +8,7 @@ import Foundation public extension ParseObject { - + /** Fetch all local objects. @@ -17,36 +17,36 @@ public extension ParseObject { - note: You will need to run this on every `ParseObject` that needs to fetch it's local objects after creating offline objects. */ - @discardableResult static func fetchLocalStore(_ type: T.Type) async throws -> [T]? { + @discardableResult static func fetchLocalStore(_ type: T.Type) async throws -> [T]? { return try await LocalStorage.fetchLocalObjects(type) } } internal struct LocalStorage { static let fileManager = FileManager.default - + static func save(_ object: T, queryIdentifier: String?) throws { let objectData = try ParseCoding.jsonEncoder().encode(object) - + guard let objectId = object.objectId else { throw ParseError(code: .missingObjectId, message: "Object has no valid objectId") } - + let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: object.className) let objectPath = objectsDirectoryPath.appendingPathComponent(objectId) - + if fileManager.fileExists(atPath: objectPath.path) { try objectData.write(to: objectPath) } else { fileManager.createFile(atPath: objectPath.path, contents: objectData, attributes: nil) } - + if let queryIdentifier = queryIdentifier { try self.saveQueryObjects([object], queryIdentifier: queryIdentifier) } } - + static func saveAll(_ objects: [T], queryIdentifier: String?) throws { var successObjects: [T] = [] @@ -55,64 +55,64 @@ internal struct LocalStorage { guard let objectId = object.objectId else { throw ParseError(code: .missingObjectId, message: "Object has no valid objectId") } - + let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: object.className) let objectPath = objectsDirectoryPath.appendingPathComponent(objectId) - + if fileManager.fileExists(atPath: objectPath.path) { try objectData.write(to: objectPath) } else { fileManager.createFile(atPath: objectPath.path, contents: objectData, attributes: nil) } - + successObjects.append(object) } - + if let queryIdentifier = queryIdentifier { try self.saveQueryObjects(successObjects, queryIdentifier: queryIdentifier) } } - + static func get(_ type: U.Type, queryIdentifier: String) throws -> U? { guard let queryObjects = try getQueryObjects()[queryIdentifier], let queryObject = queryObjects.first else { return nil } - + let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: queryObject.className) let objectPath = objectsDirectoryPath.appendingPathComponent(queryObject.objectId) - + let objectData = try Data(contentsOf: objectPath) - + return try ParseCoding.jsonDecoder().decode(U.self, from: objectData) } - + static func getAll(_ type: U.Type, queryIdentifier: String) throws -> [U]? { guard let queryObjects = try getQueryObjects()[queryIdentifier] else { return nil } - + var allObjects: [U] = [] for queryObject in queryObjects { let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: queryObject.className) let objectPath = objectsDirectoryPath.appendingPathComponent(queryObject.objectId) - + let objectData = try Data(contentsOf: objectPath) if let object = try? ParseCoding.jsonDecoder().decode(U.self, from: objectData) { allObjects.append(object) } } - + return (allObjects.isEmpty ? nil : allObjects) } - + static fileprivate func saveFetchObjects(_ objects: [T], method: Method) throws { var fetchObjects = try getFetchObjects() fetchObjects.append(contentsOf: try objects.map({ try FetchObject($0, method: method) })) fetchObjects = fetchObjects.uniqueObjectsById - + try self.writeFetchObjects(fetchObjects) } - + static fileprivate func removeFetchObjects(_ objects: [T]) throws { var fetchObjects = try getFetchObjects() let objectIds = objects.compactMap({ $0.objectId }) @@ -122,14 +122,14 @@ internal struct LocalStorage { }) }) fetchObjects = fetchObjects.uniqueObjectsById - + try self.writeFetchObjects(fetchObjects) } - + static fileprivate func getFetchObjects() throws -> [FetchObject] { let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile) - + if fileManager.fileExists(atPath: fetchObjectsPath.path) { let jsonData = try Data(contentsOf: fetchObjectsPath) do { @@ -142,16 +142,16 @@ internal struct LocalStorage { return [] } } - + static private func writeFetchObjects(_ fetchObjects: [FetchObject]) throws { let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile) - + if fetchObjects.isEmpty { try? fileManager.removeItem(at: fetchObjectsPath) } else { let jsonData = try ParseCoding.jsonEncoder().encode(fetchObjects) - + if fileManager.fileExists(atPath: fetchObjectsPath.path) { try jsonData.write(to: fetchObjectsPath) } else { @@ -159,23 +159,23 @@ internal struct LocalStorage { } } } - + static fileprivate func saveQueryObjects(_ objects: [T], queryIdentifier: String) throws { var queryObjects = try getQueryObjects() queryObjects[queryIdentifier] = try objects.map({ try QueryObject($0) }) - + try self.writeQueryObjects(queryObjects) } - - static fileprivate func getQueryObjects() throws -> [String : [QueryObject]] { + + static fileprivate func getQueryObjects() throws -> [String: [QueryObject]] { let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile.hiddenFile) - + if fileManager.fileExists(atPath: queryObjectsPath.path) { let jsonData = try Data(contentsOf: queryObjectsPath) do { - return try ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) + return try ParseCoding.jsonDecoder().decode([String: [QueryObject]].self, from: jsonData) } catch { try fileManager.removeItem(at: queryObjectsPath) return [:] @@ -184,16 +184,16 @@ internal struct LocalStorage { return [:] } } - - static private func writeQueryObjects(_ queryObjects: [String : [QueryObject]]) throws { + + static private func writeQueryObjects(_ queryObjects: [String: [QueryObject]]) throws { let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile.hiddenFile) - + if queryObjects.isEmpty { try? fileManager.removeItem(at: queryObjectsPath) } else { let jsonData = try ParseCoding.jsonEncoder().encode(queryObjects) - + if fileManager.fileExists(atPath: queryObjectsPath.path) { try jsonData.write(to: queryObjectsPath) } else { @@ -201,18 +201,18 @@ internal struct LocalStorage { } } } - + /** Fetch all local objects. - returns: If objects are more recent on the database, it will replace the local objects and return them. */ - @discardableResult static func fetchLocalObjects(_ type: T.Type) async throws -> [T]? { + @discardableResult static func fetchLocalObjects(_ type: T.Type) async throws -> [T]? { let fetchObjects = try getFetchObjects() if fetchObjects.isEmpty { return nil } - + var saveObjects = try fetchObjects .filter({ $0.method == .save }) .asParseObjects(type) @@ -225,13 +225,13 @@ internal struct LocalStorage { var updateObjects = try fetchObjects .filter({ $0.method == .update }) .asParseObjects(type) - + var cloudObjects: [T] = [] - + if Parse.configuration.offlinePolicy.enabled { try await self.fetchLocalStore(.save, objects: &saveObjects, cloudObjects: &cloudObjects) } - + if Parse.configuration.offlinePolicy.canCreate { if Parse.configuration.isRequiringCustomObjectIds { try await self.fetchLocalStore(.create, objects: &createObjects, cloudObjects: &cloudObjects) @@ -239,15 +239,15 @@ internal struct LocalStorage { assertionFailure("Enable custom objectIds") } } - + if Parse.configuration.offlinePolicy.enabled { try await self.fetchLocalStore(.replace, objects: &replaceObjects, cloudObjects: &cloudObjects) } - + if Parse.configuration.offlinePolicy.enabled { try await self.fetchLocalStore(.update, objects: &updateObjects, cloudObjects: &cloudObjects) } - + if cloudObjects.isEmpty { return nil } else { @@ -255,13 +255,13 @@ internal struct LocalStorage { return cloudObjects } } - - private static func fetchLocalStore(_ method: Method, objects: inout [T], cloudObjects: inout [T]) async throws { + + private static func fetchLocalStore(_ method: Method, objects: inout [T], cloudObjects: inout [T]) async throws { let queryObjects = T.query() .where(containedIn(key: "objectId", array: objects.map({ $0.objectId }))) .useLocalStore(false) let foundObjects = try? await queryObjects.find() - + for object in objects { if let matchingObject = foundObjects?.first(where: { $0.objectId == object.objectId }) { if let objectUpdatedAt = object.updatedAt { @@ -277,7 +277,7 @@ internal struct LocalStorage { } } } - + switch method { case .save: try await objects.saveAll(ignoringLocalStore: true) @@ -288,7 +288,7 @@ internal struct LocalStorage { case .update: _ = try await objects.updateAll(ignoringLocalStore: true) } - + try self.removeFetchObjects(objects) } } @@ -298,8 +298,8 @@ internal struct FetchObject: Codable { let className: String let updatedAt: Date let method: Method - - init(_ object : T, method: Method) throws { + + init(_ object: T, method: Method) throws { guard let objectId = object.objectId else { throw ParseError(code: .missingObjectId, message: "Object has no valid objectId") } @@ -314,8 +314,8 @@ internal struct QueryObject: Codable { let objectId: String let className: String let queryDate: Date - - init(_ object : T) throws { + + init(_ object: T) throws { guard let objectId = object.objectId else { throw ParseError(code: .missingObjectId, message: "Object has no valid objectId") } @@ -326,7 +326,7 @@ internal struct QueryObject: Codable { } internal extension ParseObject { - + func saveLocally(method: Method? = nil, queryIdentifier: String? = nil, error: Error? = nil) throws { @@ -390,12 +390,12 @@ internal extension ParseObject { } internal extension Sequence where Element: ParseObject { - + func saveLocally(method: Method? = nil, queryIdentifier: String? = nil, error: ParseError? = nil) throws { let objects = map { $0 } - + if let method = method { switch method { case .save: @@ -456,7 +456,7 @@ internal extension Sequence where Element: ParseObject { } fileprivate extension String { - + /** Creates a hidden file */ @@ -466,55 +466,55 @@ fileprivate extension String { } fileprivate extension Sequence where Element == FetchObject { - + /** Returns a unique array of `FetchObject`'s where each element is the most recent version of itself. */ var uniqueObjectsById: [Element] { let fetchObjects = map { $0 }.sorted(by: { $0.updatedAt > $1.updatedAt }) - + var uniqueObjects: [Element] = [] for fetchObject in fetchObjects { uniqueObjects.append(fetchObjects.first(where: { $0.objectId == fetchObject.objectId }) ?? fetchObject) } - + return uniqueObjects.isEmpty ? fetchObjects : uniqueObjects } - - func asParseObjects(_ type: T.Type) throws -> [T] { + + func asParseObjects(_ type: T.Type) throws -> [T] { let fileManager = FileManager.default - + let fetchObjectIds = map { $0 }.filter({ $0.className == T.className }).map({ $0.objectId }) - + let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: T.className) let directoryObjectIds = try fileManager.contentsOfDirectory(atPath: objectsDirectoryPath.path) - + var objects: [T] = [] - + for directoryObjectId in directoryObjectIds { if fetchObjectIds.contains(directoryObjectId) { if #available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *) { let contentPath = objectsDirectoryPath.appending(component: directoryObjectId, directoryHint: .notDirectory) - + if fileManager.fileExists(atPath: contentPath.path) { let jsonData = try Data(contentsOf: contentPath) let object = try ParseCoding.jsonDecoder().decode(T.self, from: jsonData) - + objects.append(object) } } else { let contentPath = objectsDirectoryPath.appendingPathComponent(directoryObjectId, isDirectory: false) - + if fileManager.fileExists(atPath: contentPath.path) { let jsonData = try Data(contentsOf: contentPath) let object = try ParseCoding.jsonDecoder().decode(T.self, from: jsonData) - + objects.append(object) } } } } - + return objects } } diff --git a/Sources/ParseSwift/Storage/ParseFileManager.swift b/Sources/ParseSwift/Storage/ParseFileManager.swift index b8b3fab85..fe800abcf 100644 --- a/Sources/ParseSwift/Storage/ParseFileManager.swift +++ b/Sources/ParseSwift/Storage/ParseFileManager.swift @@ -227,7 +227,7 @@ public extension ParseFileManager { .appendingPathComponent(ParseConstants.fileDownloadsDirectory, isDirectory: true) } - + /** The default directory for all `ParseObject`'s. - parameter className: An optional value, that if set returns the objects directory for a specific class @@ -243,13 +243,13 @@ public extension ParseFileManager { .appendingPathComponent(ParseConstants.fileObjectsDirectory, isDirectory: true) try fileManager.createDirectoryIfNeeded(objectsDirectory.path) - + if let className = className { let classDirectory = objectsDirectory .appendingPathComponent(className, isDirectory: true) try fileManager.createDirectoryIfNeeded(classDirectory.path) - + return classDirectory } else { return objectsDirectory diff --git a/Sources/ParseSwift/Types/ParseConfiguration.swift b/Sources/ParseSwift/Types/ParseConfiguration.swift index cba96f339..5093fc013 100644 --- a/Sources/ParseSwift/Types/ParseConfiguration.swift +++ b/Sources/ParseSwift/Types/ParseConfiguration.swift @@ -39,7 +39,7 @@ public struct ParseConfiguration { /// The live query server URL to connect to Parse Server. public internal(set) var liveQuerysServerURL: URL? - + /// Determines wheter or not objects need to be saved locally. public internal(set) var offlinePolicy: OfflinePolicy @@ -395,20 +395,20 @@ public struct ParseConfiguration { authentication: authentication) self.isMigratingFromObjcSDK = migratingFromObjcSDK } - + public enum OfflinePolicy { - + /** When using the `create` Policy, you can get, create and save objects when offline. - warning: Using this Policy requires you to enable `allowingCustomObjectIds`. */ case create - + /** When using the `save` Policy, you can get and save objects when offline. */ case save - + /** When using the `disabled` Policy, offline usage is disabled. */ @@ -417,11 +417,11 @@ public struct ParseConfiguration { } extension ParseConfiguration.OfflinePolicy { - + var canCreate: Bool { return self == .create } - + var enabled: Bool { return self == .create || self == .save } diff --git a/Sources/ParseSwift/Types/ParseError.swift b/Sources/ParseSwift/Types/ParseError.swift index 1ce4d4c9b..387b4d7a8 100644 --- a/Sources/ParseSwift/Types/ParseError.swift +++ b/Sources/ParseSwift/Types/ParseError.swift @@ -348,7 +348,7 @@ public struct ParseError: ParseTypeable, Swift.Error { a non-2XX status code. */ case xDomainRequest = 602 - + /** Error code indicating that the device is not connected to the internet. */ diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index baec0e92e..e79f30b17 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -45,32 +45,32 @@ public struct Query: ParseTypeable where T: ParseObject { public var className: String { Self.className } - + internal var queryIdentifier: String { var mutableQuery = self mutableQuery.keys = nil mutableQuery.include = nil mutableQuery.excludeKeys = nil mutableQuery.fields = nil - + guard let jsonData = try? ParseCoding.jsonEncoder().encode(mutableQuery), let descriptionString = String(data: jsonData, encoding: .utf8) else { return className } - + //Sets need to be sorted to maintain the same queryIdentifier let sortedKeys = (keys?.count == 0 ? [] : ["keys"]) + (keys?.sorted(by: { $0 < $1 }) ?? []) let sortedInclude = (include?.count == 0 ? [] : ["include"]) + (include?.sorted(by: { $0 < $1 }) ?? []) let sortedExcludeKeys = (excludeKeys?.count == 0 ? [] : ["excludeKeys"]) + (excludeKeys?.sorted(by: { $0 < $1 }) ?? []) let sortedFieldsKeys = (fields?.count == 0 ? [] : ["fields"]) + (fields?.sorted(by: { $0 < $1 }) ?? []) - + let sortedSets = ( sortedKeys + sortedInclude + sortedExcludeKeys + sortedFieldsKeys ).joined(separator: "") - + return ( className + sortedSets + @@ -470,7 +470,7 @@ public struct Query: ParseTypeable where T: ParseObject { mutableQuery.order = keys return mutableQuery } - + /** Sort the results of the query based on the `Order` enum. - parameter keys: An array of keys to order by. @@ -548,7 +548,7 @@ extension Query: Queryable { do { let objects = try findCommand().execute(options: options) try? objects.saveLocally(queryIdentifier: queryIdentifier) - + return objects } catch let parseError { if parseError.hasNoInternetConnection, @@ -614,7 +614,7 @@ extension Query: Queryable { switch result { case .success(let objects): try? objects.saveLocally(queryIdentifier: queryIdentifier) - + completion(result) case .failure(let failure): if failure.hasNoInternetConnection, @@ -748,13 +748,13 @@ extension Query: Queryable { } } catch { if let urlError = error as? URLError, - urlError.code == URLError.Code.notConnectedToInternet || urlError.code == URLError.Code.dataNotAllowed, let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) { + urlError.code == URLError.Code.notConnectedToInternet || urlError.code == URLError.Code.dataNotAllowed, let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) { completion(.success(localObjects)) } else { let defaultError = ParseError(code: .unknownError, message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError - + if parseError.hasNoInternetConnection, let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) { completion(.success(localObjects)) @@ -767,7 +767,7 @@ extension Query: Queryable { return } } - + if useLocalStore { try? results.saveLocally(queryIdentifier: queryIdentifier) } @@ -795,7 +795,7 @@ extension Query: Queryable { do { let objects = try firstCommand().execute(options: options) try? objects.saveLocally(queryIdentifier: queryIdentifier) - + return objects } catch let parseError { if parseError.hasNoInternetConnection, @@ -867,7 +867,7 @@ extension Query: Queryable { switch result { case .success(let object): try? object.saveLocally(queryIdentifier: queryIdentifier) - + completion(result) case .failure(let failure): if failure.hasNoInternetConnection, From ce5760f987efef07f16178b81fa13772215ba280 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Thu, 29 Dec 2022 22:47:47 +0100 Subject: [PATCH 60/73] Linting --- Sources/ParseSwift/Storage/LocalStorage.swift | 40 ++++++++++--------- Sources/ParseSwift/Types/Query.swift | 23 +++++++---- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 8af95e811..66a6e14aa 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -105,7 +105,7 @@ internal struct LocalStorage { } static fileprivate func saveFetchObjects(_ objects: [T], - method: Method) throws { + method: Method) throws { var fetchObjects = try getFetchObjects() fetchObjects.append(contentsOf: try objects.map({ try FetchObject($0, method: method) })) fetchObjects = fetchObjects.uniqueObjectsById @@ -161,7 +161,7 @@ internal struct LocalStorage { } static fileprivate func saveQueryObjects(_ objects: [T], - queryIdentifier: String) throws { + queryIdentifier: String) throws { var queryObjects = try getQueryObjects() queryObjects[queryIdentifier] = try objects.map({ try QueryObject($0) }) @@ -256,7 +256,9 @@ internal struct LocalStorage { } } - private static func fetchLocalStore(_ method: Method, objects: inout [T], cloudObjects: inout [T]) async throws { + private static func fetchLocalStore(_ method: Method, + objects: inout [T], + cloudObjects: inout [T]) async throws { let queryObjects = T.query() .where(containedIn(key: "objectId", array: objects.map({ $0.objectId }))) .useLocalStore(false) @@ -491,26 +493,26 @@ fileprivate extension Sequence where Element == FetchObject { var objects: [T] = [] - for directoryObjectId in directoryObjectIds { - if fetchObjectIds.contains(directoryObjectId) { - if #available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *) { - let contentPath = objectsDirectoryPath.appending(component: directoryObjectId, directoryHint: .notDirectory) + for directoryObjectId in directoryObjectIds where fetchObjectIds.contains(directoryObjectId) { + if #available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *) { + let contentPath = objectsDirectoryPath.appending(component: directoryObjectId, + directoryHint: .notDirectory) - if fileManager.fileExists(atPath: contentPath.path) { - let jsonData = try Data(contentsOf: contentPath) - let object = try ParseCoding.jsonDecoder().decode(T.self, from: jsonData) + if fileManager.fileExists(atPath: contentPath.path) { + let jsonData = try Data(contentsOf: contentPath) + let object = try ParseCoding.jsonDecoder().decode(T.self, from: jsonData) - objects.append(object) - } - } else { - let contentPath = objectsDirectoryPath.appendingPathComponent(directoryObjectId, isDirectory: false) + objects.append(object) + } + } else { + let contentPath = objectsDirectoryPath.appendingPathComponent(directoryObjectId, + isDirectory: false) - if fileManager.fileExists(atPath: contentPath.path) { - let jsonData = try Data(contentsOf: contentPath) - let object = try ParseCoding.jsonDecoder().decode(T.self, from: jsonData) + if fileManager.fileExists(atPath: contentPath.path) { + let jsonData = try Data(contentsOf: contentPath) + let object = try ParseCoding.jsonDecoder().decode(T.self, from: jsonData) - objects.append(object) - } + objects.append(object) } } } diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index e79f30b17..d69726abd 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -59,10 +59,14 @@ public struct Query: ParseTypeable where T: ParseObject { } //Sets need to be sorted to maintain the same queryIdentifier - let sortedKeys = (keys?.count == 0 ? [] : ["keys"]) + (keys?.sorted(by: { $0 < $1 }) ?? []) - let sortedInclude = (include?.count == 0 ? [] : ["include"]) + (include?.sorted(by: { $0 < $1 }) ?? []) - let sortedExcludeKeys = (excludeKeys?.count == 0 ? [] : ["excludeKeys"]) + (excludeKeys?.sorted(by: { $0 < $1 }) ?? []) - let sortedFieldsKeys = (fields?.count == 0 ? [] : ["fields"]) + (fields?.sorted(by: { $0 < $1 }) ?? []) + let sortedKeys = ((keys?.count == 0 ? [] : ["keys"]) + + (keys?.sorted(by: { $0 < $1 }) ?? [])) + let sortedInclude = ((include?.count == 0 ? [] : ["include"]) + + (include?.sorted(by: { $0 < $1 }) ?? [])) + let sortedExcludeKeys = ((excludeKeys?.count == 0 ? [] : ["excludeKeys"]) + + (excludeKeys?.sorted(by: { $0 < $1 }) ?? [])) + let sortedFieldsKeys = ((fields?.count == 0 ? [] : ["fields"]) + + (fields?.sorted(by: { $0 < $1 }) ?? [])) let sortedSets = ( sortedKeys + @@ -618,7 +622,8 @@ extension Query: Queryable { completion(result) case .failure(let failure): if failure.hasNoInternetConnection, - let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) { + let localObjects = try? LocalStorage.getAll(ResultType.self, + queryIdentifier: queryIdentifier) { completion(.success(localObjects)) } else { completion(.failure(failure)) @@ -748,7 +753,10 @@ extension Query: Queryable { } } catch { if let urlError = error as? URLError, - urlError.code == URLError.Code.notConnectedToInternet || urlError.code == URLError.Code.dataNotAllowed, let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) { + (urlError.code == URLError.Code.notConnectedToInternet || + urlError.code == URLError.Code.dataNotAllowed), + let localObjects = try? LocalStorage.getAll(ResultType.self, + queryIdentifier: queryIdentifier) { completion(.success(localObjects)) } else { let defaultError = ParseError(code: .unknownError, @@ -756,7 +764,8 @@ extension Query: Queryable { let parseError = error as? ParseError ?? defaultError if parseError.hasNoInternetConnection, - let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) { + let localObjects = try? LocalStorage.getAll(ResultType.self, + queryIdentifier: queryIdentifier) { completion(.success(localObjects)) } else { callbackQueue.async { From 1f150efa6717af092787542eea333666ea4be53b Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Thu, 29 Dec 2022 23:01:48 +0100 Subject: [PATCH 61/73] Linting --- Sources/ParseSwift/Coding/AnyDecodable.swift | 3 ++- Sources/ParseSwift/Coding/AnyEncodable.swift | 5 ++--- Sources/ParseSwift/Coding/ParseEncoder.swift | 7 +++---- Sources/ParseSwift/Types/ParseACL.swift | 2 ++ .../ParseSwiftTests/ParseKeychainAccessGroupTests.swift | 9 ++++++--- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Sources/ParseSwift/Coding/AnyDecodable.swift b/Sources/ParseSwift/Coding/AnyDecodable.swift index e7bb1f77e..adf872734 100755 --- a/Sources/ParseSwift/Coding/AnyDecodable.swift +++ b/Sources/ParseSwift/Coding/AnyDecodable.swift @@ -35,7 +35,7 @@ struct AnyDecodable: Decodable { } } -protocol _AnyDecodable { +protocol _AnyDecodable { // swiftlint:disable:this type_name var value: Any { get } init(_ value: T?) } @@ -74,6 +74,7 @@ extension _AnyDecodable { } extension AnyDecodable: Equatable { + // swiftlint:disable:next cyclomatic_complexity static func == (lhs: AnyDecodable, rhs: AnyDecodable) -> Bool { switch (lhs.value, rhs.value) { #if canImport(Foundation) diff --git a/Sources/ParseSwift/Coding/AnyEncodable.swift b/Sources/ParseSwift/Coding/AnyEncodable.swift index f7738bee4..876f968f9 100755 --- a/Sources/ParseSwift/Coding/AnyEncodable.swift +++ b/Sources/ParseSwift/Coding/AnyEncodable.swift @@ -38,8 +38,7 @@ struct AnyEncodable: Encodable { } @usableFromInline -protocol _AnyEncodable { - +protocol _AnyEncodable { // swiftlint:disable:this type_name var value: Any { get } init(_ value: T?) } @@ -47,7 +46,6 @@ protocol _AnyEncodable { extension AnyEncodable: _AnyEncodable {} // MARK: - Encodable - extension _AnyEncodable { // swiftlint:disable:next cyclomatic_complexity function_body_length func encode(to encoder: Encoder) throws { @@ -110,6 +108,7 @@ extension _AnyEncodable { } #if canImport(Foundation) + // swiftlint:disable:next cyclomatic_complexity private func encode(nsnumber: NSNumber, into container: inout SingleValueEncodingContainer) throws { switch Character(Unicode.Scalar(UInt8(nsnumber.objCType.pointee))) { case "c", "C": diff --git a/Sources/ParseSwift/Coding/ParseEncoder.swift b/Sources/ParseSwift/Coding/ParseEncoder.swift index 75b92a2a6..db3ef42c6 100644 --- a/Sources/ParseSwift/Coding/ParseEncoder.swift +++ b/Sources/ParseSwift/Coding/ParseEncoder.swift @@ -5,8 +5,7 @@ // Created by Pranjal Satija on 7/20/20. // Copyright © 2020 Parse. All rights reserved. // - -//===----------------------------------------------------------------------===// +// ===----------------------------------------------------------------------===// // // This source file is part of the Swift.org open source project // @@ -16,10 +15,10 @@ // See https://swift.org/LICENSE.txt for license information // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // -//===----------------------------------------------------------------------===// - +// ===----------------------------------------------------------------------===// import Foundation +// swiftlint:disable type_name /// A marker protocol used to determine whether a value is a `String`-keyed `Dictionary` /// containing `Encodable` values (in which case it should be exempt from key conversion strategies). /// diff --git a/Sources/ParseSwift/Types/ParseACL.swift b/Sources/ParseSwift/Types/ParseACL.swift index 1fb34d44d..54798f5fd 100644 --- a/Sources/ParseSwift/Types/ParseACL.swift +++ b/Sources/ParseSwift/Types/ParseACL.swift @@ -8,6 +8,8 @@ import Foundation +// swiftlint:disable large_tuple + /** `ParseACL` is used to control which users can access or modify a particular `ParseObject`. Each `ParseObject` has its own ACL. You can grant read and write permissions separately diff --git a/Tests/ParseSwiftTests/ParseKeychainAccessGroupTests.swift b/Tests/ParseSwiftTests/ParseKeychainAccessGroupTests.swift index 3d4117f44..a26bb4b1d 100644 --- a/Tests/ParseSwiftTests/ParseKeychainAccessGroupTests.swift +++ b/Tests/ParseSwiftTests/ParseKeychainAccessGroupTests.swift @@ -11,6 +11,8 @@ import Foundation import XCTest @testable import ParseSwift +// swiftlint:disable unused_optional_binding function_body_length type_body_length + class ParseKeychainAccessGroupTests: XCTestCase { struct User: ParseUser { @@ -261,6 +263,7 @@ class ParseKeychainAccessGroupTests: XCTestCase { XCTAssertEqual(acl, otherAcl) } + // swiftlint:disable:next cyclomatic_complexity func testRemoveOldObjectsFromKeychain() throws { try userLogin() Config.current = .init(welcomeMessage: "yolo", winningNumber: 1) @@ -299,15 +302,15 @@ class ParseKeychainAccessGroupTests: XCTestCase { let deleted = KeychainStore.shared.removeOldObjects(accessGroup: ParseSwift.configuration.keychainAccessGroup) XCTAssertTrue(deleted) if let _: CurrentUserContainer = - try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) { + try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) { XCTFail("Should be nil") } if let _: CurrentConfigContainer = - try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentConfig) { + try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentConfig) { XCTFail("Should be nil") } if let _: DefaultACL = - try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.defaultACL) { + try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.defaultACL) { XCTFail("Should be nil") } guard let _: CurrentInstallationContainer = From ad3985a6e633714bb15c600e462f95ae40782d6a Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 30 Dec 2022 10:23:32 +0100 Subject: [PATCH 62/73] Fix --- Sources/ParseSwift/Storage/LocalStorage.swift | 24 +++++-------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 66a6e14aa..cfa7551b0 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -494,26 +494,14 @@ fileprivate extension Sequence where Element == FetchObject { var objects: [T] = [] for directoryObjectId in directoryObjectIds where fetchObjectIds.contains(directoryObjectId) { - if #available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *) { - let contentPath = objectsDirectoryPath.appending(component: directoryObjectId, - directoryHint: .notDirectory) + let contentPath = objectsDirectoryPath.appendingPathComponent(directoryObjectId, + isDirectory: false) - if fileManager.fileExists(atPath: contentPath.path) { - let jsonData = try Data(contentsOf: contentPath) - let object = try ParseCoding.jsonDecoder().decode(T.self, from: jsonData) + if fileManager.fileExists(atPath: contentPath.path) { + let jsonData = try Data(contentsOf: contentPath) + let object = try ParseCoding.jsonDecoder().decode(T.self, from: jsonData) - objects.append(object) - } - } else { - let contentPath = objectsDirectoryPath.appendingPathComponent(directoryObjectId, - isDirectory: false) - - if fileManager.fileExists(atPath: contentPath.path) { - let jsonData = try Data(contentsOf: contentPath) - let object = try ParseCoding.jsonDecoder().decode(T.self, from: jsonData) - - objects.append(object) - } + objects.append(object) } } From 8dbe69745d38db736c94752a15610283fe240266 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 30 Dec 2022 10:58:26 +0100 Subject: [PATCH 63/73] CodeCov --- Tests/ParseSwiftTests/ParseQueryAsyncTests.swift | 1 + Tests/ParseSwiftTests/ParseQueryCacheTests.swift | 1 + Tests/ParseSwiftTests/ParseQueryCombineTests.swift | 1 + Tests/ParseSwiftTests/ParseQueryTests.swift | 1 + Tests/ParseSwiftTests/ParseQueryViewModelTests.swift | 1 + 5 files changed, 5 insertions(+) diff --git a/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift b/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift index d0e239a1a..f8d2e8428 100644 --- a/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift @@ -59,6 +59,7 @@ class ParseQueryAsyncTests: XCTestCase { // swiftlint:disable:this type_body_len clientKey: "clientKey", masterKey: "masterKey", serverURL: url, + offlinePolicy: .save, usingPostForQuery: true, testing: true) } diff --git a/Tests/ParseSwiftTests/ParseQueryCacheTests.swift b/Tests/ParseSwiftTests/ParseQueryCacheTests.swift index 8bc24a04b..afebd2ac9 100644 --- a/Tests/ParseSwiftTests/ParseQueryCacheTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryCacheTests.swift @@ -65,6 +65,7 @@ class ParseQueryCacheTests: XCTestCase { // swiftlint:disable:this type_body_len clientKey: "clientKey", masterKey: "masterKey", serverURL: url, + offlinePolicy: .save, usingEqualQueryConstraint: false, usingPostForQuery: false, testing: true) diff --git a/Tests/ParseSwiftTests/ParseQueryCombineTests.swift b/Tests/ParseSwiftTests/ParseQueryCombineTests.swift index 9d4d5031c..a8854d12e 100644 --- a/Tests/ParseSwiftTests/ParseQueryCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryCombineTests.swift @@ -57,6 +57,7 @@ class ParseQueryCombineTests: XCTestCase { // swiftlint:disable:this type_body_l clientKey: "clientKey", masterKey: "masterKey", serverURL: url, + offlinePolicy: .save, usingPostForQuery: true, testing: true) } diff --git a/Tests/ParseSwiftTests/ParseQueryTests.swift b/Tests/ParseSwiftTests/ParseQueryTests.swift index 26aef6c82..3fcf6049d 100644 --- a/Tests/ParseSwiftTests/ParseQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryTests.swift @@ -63,6 +63,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length clientKey: "clientKey", masterKey: "masterKey", serverURL: url, + offlinePolicy: .save, usingEqualQueryConstraint: false, usingPostForQuery: true, testing: true) diff --git a/Tests/ParseSwiftTests/ParseQueryViewModelTests.swift b/Tests/ParseSwiftTests/ParseQueryViewModelTests.swift index 3d2ff27a9..566af6b04 100644 --- a/Tests/ParseSwiftTests/ParseQueryViewModelTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryViewModelTests.swift @@ -44,6 +44,7 @@ class ParseQueryViewModelTests: XCTestCase { clientKey: "clientKey", masterKey: "masterKey", serverURL: url, + offlinePolicy: .save, usingPostForQuery: true, testing: true) } From e82c4c9e89af7ce3d9e8261d61f2ba4728afcfea Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 30 Dec 2022 11:29:55 +0100 Subject: [PATCH 64/73] CodeCov: useLocalStore --- .../ParseQueryAsyncTests.swift | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift b/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift index f8d2e8428..7342191b2 100644 --- a/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift @@ -102,6 +102,37 @@ class ParseQueryAsyncTests: XCTestCase { // swiftlint:disable:this type_body_len } XCTAssert(object.hasSameObjectId(as: scoreOnServer)) } + + @MainActor + func testLocalFind() async throws { + + var scoreOnServer = GameScore(points: 10) + scoreOnServer.points = 11 + scoreOnServer.objectId = "yolo" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + + let results = QueryResponse(results: [scoreOnServer], count: 1) + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(results) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let query = GameScore.query + .useLocalStore() + + let found = try await query.find() + guard let object = found.first else { + XCTFail("Should have unwrapped") + return + } + XCTAssert(object.hasSameObjectId(as: scoreOnServer)) + } @MainActor func testWithCount() async throws { @@ -201,6 +232,35 @@ class ParseQueryAsyncTests: XCTestCase { // swiftlint:disable:this type_body_len } XCTAssert(object.hasSameObjectId(as: scoreOnServer)) } + + @MainActor + func testLocalFindAll() async throws { + + var scoreOnServer = GameScore(points: 10) + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + + let results = AnyResultsResponse(results: [scoreOnServer]) + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(results) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let found = try await GameScore.query + .useLocalStore() + .findAll() + guard let object = found.first else { + XCTFail("Should have unwrapped") + return + } + XCTAssert(object.hasSameObjectId(as: scoreOnServer)) + } @MainActor func testFindExplain() async throws { From 58e6a357755886c8dde4d6da88d665fc777ff7da Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 30 Dec 2022 12:00:22 +0100 Subject: [PATCH 65/73] tests: LocalStorage --- ParseSwift.xcodeproj/project.pbxproj | 8 ++ .../ParseLocalStorageTests.swift | 135 ++++++++++++++++++ .../ParseQueryAsyncTests.swift | 4 +- 3 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 Tests/ParseSwiftTests/ParseLocalStorageTests.swift diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index 3d8db50ac..6c2bee69c 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -977,6 +977,9 @@ CBEF514D295E40CB0052E598 /* LocalStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEF514B295E40CB0052E598 /* LocalStorage.swift */; }; CBEF514E295E40CB0052E598 /* LocalStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEF514B295E40CB0052E598 /* LocalStorage.swift */; }; CBEF514F295E40CB0052E598 /* LocalStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEF514B295E40CB0052E598 /* LocalStorage.swift */; }; + CBEF5152295EF5DA0052E598 /* ParseLocalStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEF5151295EF5DA0052E598 /* ParseLocalStorageTests.swift */; }; + CBEF5153295EF5E20052E598 /* ParseLocalStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEF5151295EF5DA0052E598 /* ParseLocalStorageTests.swift */; }; + CBEF5154295EF5E30052E598 /* ParseLocalStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBEF5151295EF5DA0052E598 /* ParseLocalStorageTests.swift */; }; F971F4F624DE381A006CB79B /* ParseEncoderExtraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F971F4F524DE381A006CB79B /* ParseEncoderExtraTests.swift */; }; F97B45CE24D9C6F200F4A88B /* ParseCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45B424D9C6F200F4A88B /* ParseCoding.swift */; }; F97B45CF24D9C6F200F4A88B /* ParseCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F97B45B424D9C6F200F4A88B /* ParseCoding.swift */; }; @@ -1454,6 +1457,7 @@ 91F346BD269B77B5005727B6 /* CloudObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudObservable.swift; sourceTree = ""; }; 91F346C2269B88F7005727B6 /* ParseCloudViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseCloudViewModelTests.swift; sourceTree = ""; }; CBEF514B295E40CB0052E598 /* LocalStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalStorage.swift; sourceTree = ""; }; + CBEF5151295EF5DA0052E598 /* ParseLocalStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseLocalStorageTests.swift; sourceTree = ""; }; F971F4F524DE381A006CB79B /* ParseEncoderExtraTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseEncoderExtraTests.swift; sourceTree = ""; }; F97B45B424D9C6F200F4A88B /* ParseCoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseCoding.swift; sourceTree = ""; }; F97B45B524D9C6F200F4A88B /* AnyDecodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyDecodable.swift; sourceTree = ""; }; @@ -1691,6 +1695,7 @@ 70385E6328563FD10084D306 /* ParsePushPayloadFirebaseTests.swift */, 70212D172855256F00386163 /* ParsePushTests.swift */, 917BA4252703DB4600F8D747 /* ParseQueryAsyncTests.swift */, + CBEF5151295EF5DA0052E598 /* ParseLocalStorageTests.swift */, 700AFE02289C3508006C1CD9 /* ParseQueryCacheTests.swift */, 7044C20525C5D6780011F6E7 /* ParseQueryCombineTests.swift */, 70C7DC1F24D20F180050419B /* ParseQueryTests.swift */, @@ -2932,6 +2937,7 @@ 703B092326BDFAB2005A112F /* ParseUserAsyncTests.swift in Sources */, 70E6B016286120E00043EC4A /* ParseHookFunctionTests.swift in Sources */, 70C5504625B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */, + CBEF5152295EF5DA0052E598 /* ParseLocalStorageTests.swift in Sources */, 70110D5C2506ED0E0091CC1D /* ParseInstallationTests.swift in Sources */, 70F03A562780E8E300E5AFB4 /* ParseGoogleCombineTests.swift in Sources */, 7C4C0947285EA60E00F202C6 /* ParseInstagramAsyncTests.swift in Sources */, @@ -3256,6 +3262,7 @@ 703B092526BDFAB2005A112F /* ParseUserAsyncTests.swift in Sources */, 70E6B018286120E00043EC4A /* ParseHookFunctionTests.swift in Sources */, 70C5504825B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */, + CBEF5154295EF5E30052E598 /* ParseLocalStorageTests.swift in Sources */, 709B98572556ECAA00507778 /* ParseACLTests.swift in Sources */, 70F03A582780E8E300E5AFB4 /* ParseGoogleCombineTests.swift in Sources */, 7C4C0949285EA60E00F202C6 /* ParseInstagramAsyncTests.swift in Sources */, @@ -3380,6 +3387,7 @@ 703B092426BDFAB2005A112F /* ParseUserAsyncTests.swift in Sources */, 70E6B017286120E00043EC4A /* ParseHookFunctionTests.swift in Sources */, 70C5504725B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */, + CBEF5153295EF5E20052E598 /* ParseLocalStorageTests.swift in Sources */, 70F2E2BC254F283000B2EA5C /* ParseObjectTests.swift in Sources */, 70F03A572780E8E300E5AFB4 /* ParseGoogleCombineTests.swift in Sources */, 7C4C0948285EA60E00F202C6 /* ParseInstagramAsyncTests.swift in Sources */, diff --git a/Tests/ParseSwiftTests/ParseLocalStorageTests.swift b/Tests/ParseSwiftTests/ParseLocalStorageTests.swift new file mode 100644 index 000000000..462643009 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseLocalStorageTests.swift @@ -0,0 +1,135 @@ +// +// ParseLocalStorageTests.swift +// ParseSwiftTests +// +// Created by Damian Van de Kauter on 30/12/2022. +// Copyright © 2022 Parse Community. All rights reserved. +// + +#if compiler(>=5.5.2) && canImport(_Concurrency) +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +import XCTest +@testable import ParseSwift + +final class ParseLocalStorageTests: XCTestCase { + struct GameScore: ParseObject { + //: These are required by ParseObject + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + var originalData: Data? + + //: Your own properties + var points: Int? + var player: String? + init() { } + //custom initializers + init (objectId: String?) { + self.objectId = objectId + } + init(points: Int) { + self.points = points + self.player = "Jen" + } + init(points: Int, name: String) { + self.points = points + self.player = name + } + } + + override func setUpWithError() throws { + try super.setUpWithError() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + offlinePolicy: .save, + usingPostForQuery: true, + testing: true) + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + MockURLProtocol.removeAll() + #if !os(Linux) && !os(Android) && !os(Windows) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func testSave() throws { + var score = GameScore(points: 10) + score.points = 11 + score.objectId = "yolo" + score.createdAt = Date() + score.updatedAt = score.createdAt + score.ACL = nil + + let query = GameScore.query("objectId" == score.objectId) + .useLocalStore() + XCTAssertNotEqual(query.queryIdentifier, "") + + try LocalStorage.save(score, queryIdentifier: query.queryIdentifier) + } + + func testSaveAll() throws { + var score1 = GameScore(points: 10) + score1.points = 11 + score1.objectId = "yolo1" + score1.createdAt = Date() + score1.updatedAt = score1.createdAt + score1.ACL = nil + + var score2 = GameScore(points: 10) + score2.points = 22 + score2.objectId = "yolo2" + score2.createdAt = Date() + score2.updatedAt = score2.createdAt + score2.ACL = nil + + let query = GameScore.query(containedIn(key: "objectId", array: [score1, score2].map({ $0.objectId }))) + .useLocalStore() + XCTAssertNotEqual(query.queryIdentifier, "") + + try LocalStorage.saveAll([score1, score2], queryIdentifier: query.queryIdentifier) + } + + func testGet() throws { + let query = GameScore.query("objectId" == "yolo") + .useLocalStore() + XCTAssertNotEqual(query.queryIdentifier, "") + + XCTAssertNoThrow(try LocalStorage.get(GameScore.self, queryIdentifier: query.queryIdentifier)) + } + + func testGetAll() throws { + var score1 = GameScore(points: 10) + score1.points = 11 + score1.objectId = "yolo1" + score1.createdAt = Date() + score1.updatedAt = score1.createdAt + score1.ACL = nil + + var score2 = GameScore(points: 10) + score2.points = 22 + score2.objectId = "yolo2" + score2.createdAt = Date() + score2.updatedAt = score2.createdAt + score2.ACL = nil + + let query = GameScore.query(containedIn(key: "objectId", array: ["yolo1", "yolo2"])) + .useLocalStore() + XCTAssertNotEqual(query.queryIdentifier, "") + + XCTAssertNoThrow(try LocalStorage.getAll(GameScore.self, queryIdentifier: query.queryIdentifier)) + } +} +#endif diff --git a/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift b/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift index 7342191b2..2ecfc7d56 100644 --- a/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryAsyncTests.swift @@ -102,7 +102,7 @@ class ParseQueryAsyncTests: XCTestCase { // swiftlint:disable:this type_body_len } XCTAssert(object.hasSameObjectId(as: scoreOnServer)) } - + @MainActor func testLocalFind() async throws { @@ -232,7 +232,7 @@ class ParseQueryAsyncTests: XCTestCase { // swiftlint:disable:this type_body_len } XCTAssert(object.hasSameObjectId(as: scoreOnServer)) } - + @MainActor func testLocalFindAll() async throws { From 345270a0de6141e4cef7c2f9e0a6fdcc3044c44e Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 30 Dec 2022 12:13:36 +0100 Subject: [PATCH 66/73] tests: testFetchLocalStore --- Tests/ParseSwiftTests/ParseLocalStorageTests.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tests/ParseSwiftTests/ParseLocalStorageTests.swift b/Tests/ParseSwiftTests/ParseLocalStorageTests.swift index 462643009..3fc9feb23 100644 --- a/Tests/ParseSwiftTests/ParseLocalStorageTests.swift +++ b/Tests/ParseSwiftTests/ParseLocalStorageTests.swift @@ -64,6 +64,11 @@ final class ParseLocalStorageTests: XCTestCase { #endif try ParseStorage.shared.deleteAll() } + + @MainActor + func testFetchLocalStore() async throws { + try await GameScore.fetchLocalStore(GameScore.self) + } func testSave() throws { var score = GameScore(points: 10) From 9c54f28dafbf5166e25776257e1baf9f2e193808 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 30 Dec 2022 16:16:10 +0100 Subject: [PATCH 67/73] Tests --- .../ParseLocalStorageTests.swift | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/Tests/ParseSwiftTests/ParseLocalStorageTests.swift b/Tests/ParseSwiftTests/ParseLocalStorageTests.swift index 3fc9feb23..ca9d144d9 100644 --- a/Tests/ParseSwiftTests/ParseLocalStorageTests.swift +++ b/Tests/ParseSwiftTests/ParseLocalStorageTests.swift @@ -58,13 +58,8 @@ final class ParseLocalStorageTests: XCTestCase { override func tearDownWithError() throws { try super.tearDownWithError() - MockURLProtocol.removeAll() - #if !os(Linux) && !os(Android) && !os(Windows) - try KeychainStore.shared.deleteAll() - #endif - try ParseStorage.shared.deleteAll() } - + @MainActor func testFetchLocalStore() async throws { try await GameScore.fetchLocalStore(GameScore.self) @@ -116,6 +111,14 @@ final class ParseLocalStorageTests: XCTestCase { } func testGetAll() throws { + let query = GameScore.query(containedIn(key: "objectId", array: ["yolo1", "yolo2"])) + .useLocalStore() + XCTAssertNotEqual(query.queryIdentifier, "") + + XCTAssertNoThrow(try LocalStorage.getAll(GameScore.self, queryIdentifier: query.queryIdentifier)) + } + + func testSaveLocally() throws { var score1 = GameScore(points: 10) score1.points = 11 score1.objectId = "yolo1" @@ -130,11 +133,18 @@ final class ParseLocalStorageTests: XCTestCase { score2.updatedAt = score2.createdAt score2.ACL = nil - let query = GameScore.query(containedIn(key: "objectId", array: ["yolo1", "yolo2"])) + let query1 = GameScore.query("objectId" == "yolo1") + .useLocalStore() + let query2 = GameScore.query("objectId" == ["yolo1", "yolo2"]) .useLocalStore() - XCTAssertNotEqual(query.queryIdentifier, "") - XCTAssertNoThrow(try LocalStorage.getAll(GameScore.self, queryIdentifier: query.queryIdentifier)) + try score1.saveLocally(method: .save, + queryIdentifier: query1.queryIdentifier, + error: ParseError(code: .notConnectedToInternet, message: "")) + + try [score1, score2].saveLocally(method: .save, + queryIdentifier: query2.queryIdentifier, + error: ParseError(code: .notConnectedToInternet, message: "")) } } #endif From a3729185856ef5b5e55aff1e8f39889ff9b4308e Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 30 Dec 2022 17:53:02 +0100 Subject: [PATCH 68/73] MockLocalStorage --- Sources/ParseSwift/Storage/LocalStorage.swift | 10 ++++++++ .../ParseLocalStorageTests.swift | 25 +++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index cfa7551b0..d3328b432 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -22,6 +22,8 @@ public extension ParseObject { } } +internal var MockLocalStorage: [any ParseObject]? + internal struct LocalStorage { static let fileManager = FileManager.default @@ -133,12 +135,15 @@ internal struct LocalStorage { if fileManager.fileExists(atPath: fetchObjectsPath.path) { let jsonData = try Data(contentsOf: fetchObjectsPath) do { + if MockLocalStorage != nil { return mockedFetchObjects } return try ParseCoding.jsonDecoder().decode([FetchObject].self, from: jsonData).uniqueObjectsById } catch { try fileManager.removeItem(at: fetchObjectsPath) + if MockLocalStorage != nil { return mockedFetchObjects } return [] } } else { + if MockLocalStorage != nil { return mockedFetchObjects } return [] } } @@ -160,6 +165,11 @@ internal struct LocalStorage { } } + static private var mockedFetchObjects: [FetchObject] { + guard let mockLocalStorage = MockLocalStorage else { return [] } + return mockLocalStorage.compactMap({ try? FetchObject($0, method: .save) }) + } + static fileprivate func saveQueryObjects(_ objects: [T], queryIdentifier: String) throws { var queryObjects = try getQueryObjects() diff --git a/Tests/ParseSwiftTests/ParseLocalStorageTests.swift b/Tests/ParseSwiftTests/ParseLocalStorageTests.swift index ca9d144d9..a1d148e4c 100644 --- a/Tests/ParseSwiftTests/ParseLocalStorageTests.swift +++ b/Tests/ParseSwiftTests/ParseLocalStorageTests.swift @@ -51,9 +51,26 @@ final class ParseLocalStorageTests: XCTestCase { clientKey: "clientKey", masterKey: "masterKey", serverURL: url, - offlinePolicy: .save, + offlinePolicy: .create, + requiringCustomObjectIds: true, usingPostForQuery: true, testing: true) + + var score1 = GameScore(points: 10) + score1.points = 11 + score1.objectId = "yolo1" + score1.createdAt = Date() + score1.updatedAt = score1.createdAt + score1.ACL = nil + + var score2 = GameScore(points: 10) + score2.points = 22 + score2.objectId = "yolo2" + score2.createdAt = Date() + score2.updatedAt = score2.createdAt + score2.ACL = nil + + MockLocalStorage = [score1, score2] } override func tearDownWithError() throws { @@ -62,7 +79,7 @@ final class ParseLocalStorageTests: XCTestCase { @MainActor func testFetchLocalStore() async throws { - try await GameScore.fetchLocalStore(GameScore.self) + try? await GameScore.fetchLocalStore(GameScore.self) } func testSave() throws { @@ -141,10 +158,14 @@ final class ParseLocalStorageTests: XCTestCase { try score1.saveLocally(method: .save, queryIdentifier: query1.queryIdentifier, error: ParseError(code: .notConnectedToInternet, message: "")) + try score1.saveLocally(method: .save, + queryIdentifier: query1.queryIdentifier) try [score1, score2].saveLocally(method: .save, queryIdentifier: query2.queryIdentifier, error: ParseError(code: .notConnectedToInternet, message: "")) + try [score1, score2].saveLocally(method: .save, + queryIdentifier: query2.queryIdentifier) } } #endif From 42cf041933d72c54d921fd153ae151c5c6bcd4af Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 30 Dec 2022 18:55:19 +0100 Subject: [PATCH 69/73] tests: Query local --- Sources/ParseSwift/Storage/LocalStorage.swift | 20 +++--- .../ParseLocalStorageTests.swift | 2 +- Tests/ParseSwiftTests/ParseQueryTests.swift | 70 +++++++++++++++++++ 3 files changed, 82 insertions(+), 10 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index d3328b432..827c6e7f6 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -290,15 +290,17 @@ internal struct LocalStorage { } } - switch method { - case .save: - try await objects.saveAll(ignoringLocalStore: true) - case .create: - try await objects.createAll(ignoringLocalStore: true) - case .replace: - try await objects.replaceAll(ignoringLocalStore: true) - case .update: - _ = try await objects.updateAll(ignoringLocalStore: true) + if MockLocalStorage == nil { + switch method { + case .save: + try await objects.saveAll(ignoringLocalStore: true) + case .create: + try await objects.createAll(ignoringLocalStore: true) + case .replace: + try await objects.replaceAll(ignoringLocalStore: true) + case .update: + _ = try await objects.updateAll(ignoringLocalStore: true) + } } try self.removeFetchObjects(objects) diff --git a/Tests/ParseSwiftTests/ParseLocalStorageTests.swift b/Tests/ParseSwiftTests/ParseLocalStorageTests.swift index a1d148e4c..b7a6ce5eb 100644 --- a/Tests/ParseSwiftTests/ParseLocalStorageTests.swift +++ b/Tests/ParseSwiftTests/ParseLocalStorageTests.swift @@ -79,7 +79,7 @@ final class ParseLocalStorageTests: XCTestCase { @MainActor func testFetchLocalStore() async throws { - try? await GameScore.fetchLocalStore(GameScore.self) + try await GameScore.fetchLocalStore(GameScore.self) } func testSave() throws { diff --git a/Tests/ParseSwiftTests/ParseQueryTests.swift b/Tests/ParseSwiftTests/ParseQueryTests.swift index 3fcf6049d..e44ae9758 100644 --- a/Tests/ParseSwiftTests/ParseQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryTests.swift @@ -463,6 +463,38 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } } + + func testLocalFind() { + var scoreOnServer = GameScore(points: 10) + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + + let results = QueryResponse(results: [scoreOnServer], count: 1) + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(results) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let query = GameScore.query() + .useLocalStore() + do { + + guard let score = try query.find(options: []).first else { + XCTFail("Should unwrap first object found") + return + } + XCTAssert(score.hasSameObjectId(as: scoreOnServer)) + } catch { + XCTFail(error.localizedDescription) + } + + } func testFindLimit() { let query = GameScore.query() @@ -633,6 +665,44 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } wait(for: [expectation], timeout: 20.0) } + + func testLocalFindAllAsync() { + var scoreOnServer = GameScore(points: 10) + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + + let results = AnyResultsResponse(results: [scoreOnServer]) + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(results) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + let query = GameScore.query() + .useLocalStore() + let expectation = XCTestExpectation(description: "Count object1") + query.findAll { result in + + switch result { + + case .success(let found): + guard let score = found.first else { + XCTFail("Should unwrap score count") + expectation.fulfill() + return + } + XCTAssert(score.hasSameObjectId(as: scoreOnServer)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation.fulfill() + } + wait(for: [expectation], timeout: 20.0) + } func testFindAllAsyncErrorSkip() { var scoreOnServer = GameScore(points: 10) From f51defdde64f678129882094aa5465246cced679 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Sat, 31 Dec 2022 12:36:37 +0100 Subject: [PATCH 70/73] codecov: Mock QueryObjects --- Sources/ParseSwift/Storage/LocalStorage.swift | 8 ++++++++ Tests/ParseSwiftTests/ParseQueryTests.swift | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 827c6e7f6..2ad1e9377 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -185,12 +185,15 @@ internal struct LocalStorage { if fileManager.fileExists(atPath: queryObjectsPath.path) { let jsonData = try Data(contentsOf: queryObjectsPath) do { + if MockLocalStorage != nil { return mockedQueryObjects } return try ParseCoding.jsonDecoder().decode([String: [QueryObject]].self, from: jsonData) } catch { try fileManager.removeItem(at: queryObjectsPath) + if MockLocalStorage != nil { return mockedQueryObjects } return [:] } } else { + if MockLocalStorage != nil { return mockedQueryObjects } return [:] } } @@ -212,6 +215,11 @@ internal struct LocalStorage { } } + static private var mockedQueryObjects: [String: [QueryObject]] { + guard let mockLocalStorage = MockLocalStorage else { return [:] } + return ["queryIdentifierMock": mockLocalStorage.compactMap({ try? QueryObject($0) })] + } + /** Fetch all local objects. diff --git a/Tests/ParseSwiftTests/ParseQueryTests.swift b/Tests/ParseSwiftTests/ParseQueryTests.swift index e44ae9758..43388b6b6 100644 --- a/Tests/ParseSwiftTests/ParseQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryTests.swift @@ -463,7 +463,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } } - + func testLocalFind() { var scoreOnServer = GameScore(points: 10) scoreOnServer.objectId = "yarr" @@ -665,7 +665,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } wait(for: [expectation], timeout: 20.0) } - + func testLocalFindAllAsync() { var scoreOnServer = GameScore(points: 10) scoreOnServer.objectId = "yarr" From 835a7782a3d7f24b9839891d5162c64d63d8f020 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Sat, 31 Dec 2022 13:03:35 +0100 Subject: [PATCH 71/73] Revert Mock --- Sources/ParseSwift/Storage/LocalStorage.swift | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 2ad1e9377..827c6e7f6 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -185,15 +185,12 @@ internal struct LocalStorage { if fileManager.fileExists(atPath: queryObjectsPath.path) { let jsonData = try Data(contentsOf: queryObjectsPath) do { - if MockLocalStorage != nil { return mockedQueryObjects } return try ParseCoding.jsonDecoder().decode([String: [QueryObject]].self, from: jsonData) } catch { try fileManager.removeItem(at: queryObjectsPath) - if MockLocalStorage != nil { return mockedQueryObjects } return [:] } } else { - if MockLocalStorage != nil { return mockedQueryObjects } return [:] } } @@ -215,11 +212,6 @@ internal struct LocalStorage { } } - static private var mockedQueryObjects: [String: [QueryObject]] { - guard let mockLocalStorage = MockLocalStorage else { return [:] } - return ["queryIdentifierMock": mockLocalStorage.compactMap({ try? QueryObject($0) })] - } - /** Fetch all local objects. From f887fe8615e2f77f9cc970eded4d2874424d6733 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Sun, 1 Jan 2023 16:47:31 +0100 Subject: [PATCH 72/73] codecov: Expand saveLocally --- .../ParseLocalStorageTests.swift | 74 ++++++++++++++++--- 1 file changed, 62 insertions(+), 12 deletions(-) diff --git a/Tests/ParseSwiftTests/ParseLocalStorageTests.swift b/Tests/ParseSwiftTests/ParseLocalStorageTests.swift index b7a6ce5eb..b65619fc8 100644 --- a/Tests/ParseSwiftTests/ParseLocalStorageTests.swift +++ b/Tests/ParseSwiftTests/ParseLocalStorageTests.swift @@ -154,18 +154,68 @@ final class ParseLocalStorageTests: XCTestCase { .useLocalStore() let query2 = GameScore.query("objectId" == ["yolo1", "yolo2"]) .useLocalStore() - - try score1.saveLocally(method: .save, - queryIdentifier: query1.queryIdentifier, - error: ParseError(code: .notConnectedToInternet, message: "")) - try score1.saveLocally(method: .save, - queryIdentifier: query1.queryIdentifier) - - try [score1, score2].saveLocally(method: .save, - queryIdentifier: query2.queryIdentifier, - error: ParseError(code: .notConnectedToInternet, message: "")) - try [score1, score2].saveLocally(method: .save, - queryIdentifier: query2.queryIdentifier) + + XCTAssertNoThrow(try score1.saveLocally(method: .save, + queryIdentifier: query1.queryIdentifier, + error: ParseError(code: .notConnectedToInternet, + message: ""))) + XCTAssertNoThrow(try score1.saveLocally(method: .save, + queryIdentifier: query1.queryIdentifier)) + + XCTAssertNoThrow(try [score1, score2].saveLocally(method: .save, + queryIdentifier: query2.queryIdentifier, + error: ParseError(code: .notConnectedToInternet, + message: ""))) + XCTAssertNoThrow(try [score1, score2].saveLocally(method: .save, + queryIdentifier: query2.queryIdentifier)) + + + + XCTAssertNoThrow(try score1.saveLocally(method: .create, + queryIdentifier: query1.queryIdentifier, + error: ParseError(code: .notConnectedToInternet, + message: ""))) + XCTAssertNoThrow(try score1.saveLocally(method: .create, + queryIdentifier: query1.queryIdentifier)) + + XCTAssertNoThrow(try [score1, score2].saveLocally(method: .create, + queryIdentifier: query2.queryIdentifier, + error: ParseError(code: .notConnectedToInternet, + message: ""))) + XCTAssertNoThrow(try [score1, score2].saveLocally(method: .create, + queryIdentifier: query2.queryIdentifier)) + + + + XCTAssertNoThrow(try score1.saveLocally(method: .replace, + queryIdentifier: query1.queryIdentifier, + error: ParseError(code: .notConnectedToInternet, + message: ""))) + XCTAssertNoThrow(try score1.saveLocally(method: .replace, + queryIdentifier: query1.queryIdentifier)) + + XCTAssertNoThrow(try [score1, score2].saveLocally(method: .replace, + queryIdentifier: query2.queryIdentifier, + error: ParseError(code: .notConnectedToInternet, + message: ""))) + XCTAssertNoThrow(try [score1, score2].saveLocally(method: .replace, + queryIdentifier: query2.queryIdentifier)) + + + + XCTAssertNoThrow(try score1.saveLocally(method: .update, + queryIdentifier: query1.queryIdentifier, + error: ParseError(code: .notConnectedToInternet, + message: ""))) + XCTAssertNoThrow(try score1.saveLocally(method: .update, + queryIdentifier: query1.queryIdentifier)) + + XCTAssertNoThrow(try [score1, score2].saveLocally(method: .update, + queryIdentifier: query2.queryIdentifier, + error: ParseError(code: .notConnectedToInternet, + message: ""))) + XCTAssertNoThrow(try [score1, score2].saveLocally(method: .update, + queryIdentifier: query2.queryIdentifier)) } } #endif From e34dd82f84da4eb014ba6f3a8d4850e917c3fda9 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Sun, 1 Jan 2023 16:54:32 +0100 Subject: [PATCH 73/73] codecov: Check error --- .../ParseLocalStorageTests.swift | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/Tests/ParseSwiftTests/ParseLocalStorageTests.swift b/Tests/ParseSwiftTests/ParseLocalStorageTests.swift index b65619fc8..d181667aa 100644 --- a/Tests/ParseSwiftTests/ParseLocalStorageTests.swift +++ b/Tests/ParseSwiftTests/ParseLocalStorageTests.swift @@ -119,6 +119,36 @@ final class ParseLocalStorageTests: XCTestCase { try LocalStorage.saveAll([score1, score2], queryIdentifier: query.queryIdentifier) } + func testSaveCheckObjectId() throws { + var score1 = GameScore(points: 10) + score1.points = 11 + score1.createdAt = Date() + score1.updatedAt = score1.createdAt + score1.ACL = nil + + var score2 = GameScore(points: 10) + score2.points = 22 + score2.createdAt = Date() + score2.updatedAt = score2.createdAt + score2.ACL = nil + + let query = GameScore.query(containedIn(key: "objectId", array: [score1, score2].map({ $0.objectId }))) + .useLocalStore() + XCTAssertNotEqual(query.queryIdentifier, "") + + do { + try LocalStorage.saveAll([score1, score2], queryIdentifier: query.queryIdentifier) + } catch { + XCTAssertTrue(error.equalsTo(.missingObjectId)) + } + + do { + try LocalStorage.save(score1, queryIdentifier: query.queryIdentifier) + } catch { + XCTAssertTrue(error.equalsTo(.missingObjectId)) + } + } + func testGet() throws { let query = GameScore.query("objectId" == "yolo") .useLocalStore() @@ -154,62 +184,56 @@ final class ParseLocalStorageTests: XCTestCase { .useLocalStore() let query2 = GameScore.query("objectId" == ["yolo1", "yolo2"]) .useLocalStore() - + XCTAssertNoThrow(try score1.saveLocally(method: .save, queryIdentifier: query1.queryIdentifier, error: ParseError(code: .notConnectedToInternet, message: ""))) XCTAssertNoThrow(try score1.saveLocally(method: .save, queryIdentifier: query1.queryIdentifier)) - + XCTAssertNoThrow(try [score1, score2].saveLocally(method: .save, queryIdentifier: query2.queryIdentifier, error: ParseError(code: .notConnectedToInternet, message: ""))) XCTAssertNoThrow(try [score1, score2].saveLocally(method: .save, queryIdentifier: query2.queryIdentifier)) - - - + XCTAssertNoThrow(try score1.saveLocally(method: .create, queryIdentifier: query1.queryIdentifier, error: ParseError(code: .notConnectedToInternet, message: ""))) XCTAssertNoThrow(try score1.saveLocally(method: .create, queryIdentifier: query1.queryIdentifier)) - + XCTAssertNoThrow(try [score1, score2].saveLocally(method: .create, queryIdentifier: query2.queryIdentifier, error: ParseError(code: .notConnectedToInternet, message: ""))) XCTAssertNoThrow(try [score1, score2].saveLocally(method: .create, queryIdentifier: query2.queryIdentifier)) - - - + XCTAssertNoThrow(try score1.saveLocally(method: .replace, queryIdentifier: query1.queryIdentifier, error: ParseError(code: .notConnectedToInternet, message: ""))) XCTAssertNoThrow(try score1.saveLocally(method: .replace, queryIdentifier: query1.queryIdentifier)) - + XCTAssertNoThrow(try [score1, score2].saveLocally(method: .replace, queryIdentifier: query2.queryIdentifier, error: ParseError(code: .notConnectedToInternet, message: ""))) XCTAssertNoThrow(try [score1, score2].saveLocally(method: .replace, queryIdentifier: query2.queryIdentifier)) - - - + XCTAssertNoThrow(try score1.saveLocally(method: .update, queryIdentifier: query1.queryIdentifier, error: ParseError(code: .notConnectedToInternet, message: ""))) XCTAssertNoThrow(try score1.saveLocally(method: .update, queryIdentifier: query1.queryIdentifier)) - + XCTAssertNoThrow(try [score1, score2].saveLocally(method: .update, queryIdentifier: query2.queryIdentifier, error: ParseError(code: .notConnectedToInternet,